初めに
こんにちは。MYJLab Advent Calender 2023 17日目! 今日の担当はなぜか同期の@Kuroi_ccに召喚されて今年も書くことになった阿左見です。
例年の如くちょと遅れての投稿になります。ごめんなさい。
最近は社会人エンジニアとして渋谷らへんのメガベンチャー企業でソフトウェアエンジニアとして働いています。
SPA+マイクロサービスで作られているSaaSを開発していて、フロントエンド/バックエンド問わず書いてます。(プロダクトマネジメントも少し齧っていたりします)
昨日は後藤くんの記事で長期インターンについて書いてくれていました。
学生中に長期インターンで0→1開発をできている経験は素直に羨ましいなと思いながら読ませていただきました。
さて、本題の今日の記事の内容ですが、自分が普段書いているTypeScriptとKotlin,学生中特にお世話になったPythonの3つを題材に、各言語の非同期処理の特徴や思想についてまとめてみたいと思います。
大規模になればなるほど非同期で実装しなければならない場合が多くなると思うので今後の参考になれば幸いです。
非同期処理とは
特に言及するまでもないとは思いますが、何か時間がかかるタスクを実行している間に別のタスクを実行できるようにする仕組みのことを言います。
また、これとは別にプログラムを上から処理していく処理の仕方を同期処理と言います。
この非同期処理の思想はプログラミング言語間で異なる思想、アプローチによって実装されています。
コードを書くときに思想を理解しておくことで、その言語っぽい書き方で非同期処理を実装することができるかも(?)
TypeScript
思想
-
イベント駆動モデル
- イベントループを使用
特徴
-
コールバック
- 非同期処理の最も基本的な形式
- コールバック地獄に陥るかも
-
Promise
- 非同期処理をより管理しやすいオブジェクトとして表現する。
-
async/await
- 非同期処理を同期的なコードのように書くことができる構文
実装例
async function fetchData(): Promise<string> {
const response = await fetch('https://example.com/data');
const data = await response.json();
return data;
}
// 非同期処理中に同期的な処理を実行
async function processData() {
console.log('非同期処理を開始');
const data = await fetchData();
console.log('非同期処理が完了', data);
// ここで同期的な処理を行う
console.log('同期的な処理');
}
イベントループとは
シングルスレッド言語であるTypeScript(JavaScript)のメカニズムでは一度に一つのタスクしか処理できないところをsetTimeout
やsetInterval
,Promise
等を使うことによって長時間かかる処理をバックグラウンドで行いメインスレッドがブロックされることを防ぐための仕組み。
イベントループの動き
- コールスタック 現在実行中のコードがスタック形式で管理される。一つの関数が完了することでスタックから取り除かれる。
- タスクキュー 非同期操作が完了するとその結果はタスクキューに追加される
- イベントループはコールスタックが空になった時(実行中のタスクがない時)タスクキューにある次のタスクに移動して実行する
- 繰り返し
以上を読んで察している方もいるとは思いますが。TypeScript(JavaScript)はシングルスレッド言語ですが、並行処理のような挙動を実現してくれます。
Kotlin
思想
-
コルーチン
- コルーチンは軽量で中間と再開が可能なコードの単位
特徴
-
軽量
- 多数のコルーチンを低コストで実装が可能
-
管理が容易
- メインスレッド、バックグラウンドスレッドでの実行管理が簡単
実装例
suspend fun fetchData(): String {
delay(1000) // 非同期で待機
return "データ"
}
// 非同期処理中に同期的な処理を挟む
fun processData() = runBlocking {
println("非同期処理を開始")
val data = async { fetchData() }
println("非同期処理を待機中")
// ここで同期的な処理を行う
println("同期的な処理")
println("非同期処理が完了: ${data.await()}")
}
コルーチンについて
一旦処理を中断した後に続きから処理を開始することができる。
サブルーチンのような状態管理を(そこまで)意識する必要がない。
- コルーチンビルダー コルーチンを起動する関数。launch や async などがあります。
- サスペンド関数 コルーチンの実行を一時停止し、後で再開する関数。suspend キーワードでマークされます。
- コルーチンスコープ コルーチンの実行範囲を定義します。GlobalScope, CoroutineScope などがあります。
- ディスパッチャー コルーチンがどのスレッドで実行されるかを決定するもの。Dispatchers.Main, Dispatchers.IO, Dispatchers.Default などがあります。
- ジョブ コルーチンの実行状態を表し、管理するためのオブジェクト。
コルーチンの実装例
import kotlinx.coroutines.*
fun main() {
// CoroutineScopeを使って、新しいコルーチンスコープを定義
CoroutineScope(Dispatchers.Main).launch {
// 非同期処理を行うコルーチンを起動
val result = async(Dispatchers.IO) {
// 何らかの時間のかかる処理(例:データベース操作)
performLongRunningTask()
}
// 非同期処理の結果を待機し、取得
val data = result.await()
// メインスレッドで結果を処理(例:UIの更新)
updateUI(data)
}
}
// サスペンド関数の例
suspend fun performLongRunningTask(): String {
// ここで時間のかかる処理を行う
delay(10000)
return "Result"
}
fun updateUI(data: String) {
// UIを更新する処理
println("Data received: $data")
}
Python
思想
- asyncioとイベントループ Pythonのasyncioモジュールは、イベントループを使用して非同期処理を行います。このアプローチでは、非同期関数(async defで定義された関数)がイベントループによってスケジューリングされ、実行されます。
特徴
-
Async/Await
- 非同期処理を宣言的に記述するため、
Async/Await
構文を使用する
- 非同期処理を宣言的に記述するため、
-
イベントループ
- 非同期タスクのスケジューリングと実行を管理する
-
マルチスレッドもサポート
- JSと似ている面もありますが、マルチスレッドでの非同意処理もサポートします
実装例
import asyncio
async def fetchData():
await asyncio.sleep(1) # 非同期で待機
return 'データ'
# 非同期処理中に同期的な処理を挟む
async def processData():
print("非同期処理を開始")
data = await fetchData()
print("非同期処理が完了", data)
# ここで同期的な処理を行う
print("同期的な処理")
# asyncio.runを使用してイベントループを開始
asyncio.run(processData())
イベントループにおける非同期処理(asyncio)
- asyncioを用いた単一スレッドでの処理
-
async
を用いて定義された非同期関数を使用- この時、非同期関数はコルーチンである
実装例(aysncio)
import asyncio
async def async_task(name, delay):
print(f"Task {name} started")
await asyncio.sleep(delay)
print(f"Task {name} completed")
return f"Result of {name}"
async def main():
# タスクのスケジュール
task1 = asyncio.create_task(async_task("A", 2))
task2 = asyncio.create_task(async_task("B", 3))
# タスクの終了を待機
result1 = await task1
result2 = await task2
print(result1, result2)
# イベントループの実行
asyncio.run(main())
マルチスレッドの非同期処理
- threading ライブラリを使用
import threading
import time
def thread_task(name, delay):
print(f"Thread {name} started")
time.sleep(delay)
print(f"Thread {name} completed")
def main():
# スレッドの生成と開始
thread1 = threading.Thread(target=thread_task, args=("A", 2))
thread2 = threading.Thread(target=thread_task, args=("B", 3))
thread1.start()
thread2.start()
# スレッドの終了を待機
thread1.join()
thread2.join()
main()
比較
asyncio
I/Oバウンドなタスクに対して効率的で単一スレッドのためコンテキストスイッチによるオーバーヘッドが少ない
マルチスレッド
CPUバウンドやブロッキングI/Oのタスクに対して適している。
異なるスレッドで独立したタスクの実行が可能
個人的にはJSからプログラムを書き始めた人間なのでasyncioの方が考え方的には好きです
まとめ
今回は自分が普段使用している言語でを用いて、それぞれの非同期処理について比較してみました。
シングルスレッドのTypeScipt、コルーチンを使用しているKotlin,中間っぽい雰囲気のあるPythonといい感じにまとまったかなとは思っています。もう少し分量を増やしたかったところですが、仕事が繁忙期すぎてcahtGPTに頼りながら書いてしまいました。
内容が間違っている部分とかありましたら、コメントください。
久しぶりの記事でのアウトプット、また非同期処理について再学習できるいい機会でした。楽しかったです!!
P.S.
久しぶりに淵野辺行きたいので飲み会あったら呼んでね!