Kotlinを使用する場合、通常@Synchronized
はスレッド間の同期を使用します。したがって、コルチンに不慣れな多くの学生は@Synchronized
、「コルチン間の同期」を実現するためにサスペンド機能にビューを追加します。これは効果的ですか?
1.协程+同期?
通常、コロチンは並列タスクの実行に役立ちます。
suspend fun doSomething(i: Int) {
println("#$i enter critical section.")
// do something critical
delay(1000)
println("#$i exit critical section.")
}
fun main() = runBlocking {
repeat(2) {
i ->
launch(Dispatchers.Default) {
println("#$i thread name: ${
Thread.currentThread().name}")
doSomething(i)
}
}
}
ログから、2つのタスクのenter
合計がexit
並行して出力され、順序がないことがわかります。
#0 thread name: DefaultDispatcher-worker-1
#1 thread name: DefaultDispatcher-worker-2
#0 enter critical section.
#1 enter critical section.
#1 exit critical section.
#0 exit critical section.
次に、@Synchronized
試してみてください。
@Synchronized
suspend fun doSomething(i: Int) {
println("#$i enter critical section.")
// do something
delay(1000)
println("#$i exit critical section.")
}
fun main() = runBlocking {
repeat(2) {
i ->
launch(Dispatchers.Default) {
println("#$i thread name: ${
Thread.currentThread().name}")
doSomething(i)
}
}
}
#0 thread name: DefaultDispatcher-worker-2
#0 enter critical section.
#1 thread name: DefaultDispatcher-worker-1
#1 enter critical section.
#0 exit critical section.
#1 exit critical section.
通常の機能の場合、Synchronizedの追加により、2つのスレッドを順番に実行する必要がありますが、上記のログは、Synchronizedが追加されているかどうかに関係なく、中断された機能の場合、それらは並行して実行されます(enter
、exit
同時に出力されます)。
書き方を変えて、サスペンド関数内にSynchronizedを追加してみましょう。
val LOCK = Object()
suspend fun doSomething(i: Int) {
synchronized(LOCK) {
println("#$i enter critical section.")
// do something
delay(1000) // <- The 'delay' suspension point is inside a critical section
println("#$i exit critical section.")
}
}
fun main() = runBlocking {
repeat(2) {
i ->
launch(Dispatchers.Default) {
println("#$i thread name: ${
Thread.currentThread().name}")
doSomething(i)
}
}
}
次のコンパイルエラーが表示されます。
"The 'delay' suspension point is inside a critical section"
2.日常の同期にはMutexが必要です
オンライン実験は、それSynchronized
が通常の同期のシーンでは使用できないことを証明しており、通常の同期を使用する必要がありますMutex
val mutex = Mutex()
suspend fun doSomething(i: Int) {
mutex.withLock {
println("#$i enter critical section.")
// do something
delay(1000) // <- The 'delay' suspension point is inside a critical section
println("#$i exit critical section.")
}
}
fun main() = runBlocking {
repeat(2) {
i ->
launch(Dispatchers.Default) {
println("#$i thread name: ${
Thread.currentThread().name}")
doSomething(i)
}
}
}
#0 thread name: DefaultDispatcher-worker-1
#1 thread name: DefaultDispatcher-worker-2
#1 enter critical section.
#1 exit critical section.
#0 enter critical section.
#0 exit critical section.
3.サスペンド機能の性質
Synchronoizedが無効なのはなぜですか?これには、サスペンド機能の実装から答えを見つける必要があります
一般的なフロントエンド通信シナリオを想像してみてください。
- リモートアクセス
token
- トークンに基づいて作成
post
- クライアントディスプレイ
通常の書き込みでは、CPS
(継続受け渡しスタイル)を使用する必要があります。これは、単にコールバックです。
class Item()
class Post()
//1 .获取token
fun requestToken(callback: (String) -> Unit) {
// ... remote service
callback("token")
}
//2. 创建post
fun createPost(token: String, item: Item, callback: (Post) -> Unit) {
// ... remote service
callback(Post())
}
//3. 显示
fun processPost(post: Post) {
// do post
}
fun postItem(item: Item) {
requestToken {
token ->
createPost(token, item) {
post ->
processPost(post)
}
}
}
サスペンド機能を使用して同じロジックを実装する場合:
class Item()
class Post()
suspend fun requestToken(): String {
// get token from api
return "token"
}
suspend fun createPost(token: String, item: Item): Post {
// create post
return Post()
}
fun processPost(post: Post) {
// do post
}
suspend fun postItem(item: Item) {
val token = requestToken()
val post = createPost(token, item)
processPost(post)
}
サスペンド関数を使用すると、CPSによってもたらされたテンプレートコードを取り除くことができますが、その本質はCPSの構文上の砂糖にすぎません。デコンパイル後のサスペンド関数は、関数を完了するためのコールバックに依存します。
// kotlin
suspend fun createPost(token: String, item: Item): Post {
... }
// Java/JVM
Object createPost(String token, Item item, Continuation<Post> cont) {
... }
Continuation
実際にはコールバックです
interface Continuation<in T> {
val context: CoroutineContext
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
上記の例postItem
の一連のサスペンド関数呼び出しは、コンパイル解除後の複数のコールバックのネストと同等ですが、コルーチンは、ネストを回避するためにラベル+再帰呼び出しを使用します。
suspend fun postItem(item: Item, label: Int) {
switch (label) {
case 0:
val token = requestToken()
case 1:
val post = createPost(token, item)
case 2:
processPost(post)
}
}
この一連の呼び出しはステートフルであるため、定義ThisSM
は現在の状態を保存し、インターフェイスをThisSM
実装Continuation
しresume
ます。独自の状態を更新した後、再開フローを介して次の処理ステージに渡され、いわゆるステートマシンモデルが実装されます。
fun postItem(item: Item, cont: Continuation) {
val sm = cont as? ThisSM ?: object : ThisSM {
val initialCont = cont
fun resume() {
postIem(null, this)
}
}
switch (sm.label) {
case 0:
sm.item = item
sm.label = 1
requestToken(sm)
case 1:
val item = sm.item
val token = sm.result as String
sm.label = 2
createPost(token, item, sm)
case 2:
processPost(post)
sm.initialCont.reusme()
}
}
より多くのサスペンド機能、参照は復号化される可能性があります修飾子Kotlinサスペンドコルーチン
4.同期が無効な理由
とはいえ、Synchronizedがサスペンド機能に対して無効なのはなぜですか?
最初の例を見てください
@Synchronized
suspend fun doSomething(i: Int) {
println("#$i enter critical section.")
// do something
delay(1000)
println("#$i exit critical section.")
}
コンパイル解除後はこんな感じ
@Synchronized
fun doSomething(i: Int, cont: Continuation) {
val sm = cont as? ThisSM ?: ThisSM {
... }
switch (sm.label) {
case 0:
println("#$i enter critical section.")
sm.label = 1
delay(1000, sm)
case 1:
println("#$i exit critical section.")
}
}
delay
呼び出し後、doSomething
関数は終了してSynchronized
無効になり、遅延も非同期呼び出しであり、後続のdoSomethingはロックの影響を受けなくなります。したがって、ログにシリアルのみthread name
をenter
保持し、パラレル出力でenter
ありexit
続けます