マルチスレッド プログラミングでは、複数のスレッドが共有オブジェクトにクロス アクセスします。
var sum = 0
@Test
fun addition_isCorrect(){
for(i in 0..100){
Thread{
accumulate(1)
}.start()
}
Thread.sleep(3000)
println(sum)
}
fun accumulate(i: Int){
sum += i
}
上記の例では、複数のスレッドが共有変数 sum を操作し、各スレッドが合計を 1 ずつ増やしているため、期待される結果は 101 になるはずですが、上記のプログラムでは 101 を取得できない場合があり、結果は 100 、99、98 の可能性があります。など。これらの誤った結果。
次のコルーチンの別の例
@Test
fun hello() = runBlocking {
var coroutines = listOf<Job>()
var shareSum = 0
// 使用固定大小的线程池创建协程的执行上下文
// @DelicateCoroutinesApi
//public actual fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher {
// require(nThreads >= 1) { "Expected at least one thread, but $nThreads specified" }
// val threadNo = AtomicInteger()
// val executor = Executors.newScheduledThreadPool(nThreads) { runnable ->
// val t = Thread(runnable, if (nThreads == 1) name else name + "-" + threadNo.incrementAndGet())
// t.isDaemon = true
// t
// }
// return executor.asCoroutineDispatcher()
//}
val scope = CoroutineScope(newFixedThreadPoolContext(8, "sizeFixedThreadPool"))
// 在不阻塞当前线程的情况下启动一个新的协程,并将对该协程的引用作为Job返回。可以用job取消当前的协程
val job = scope.launch {
// 提醒 scope这个范围里有8条线程在执行
coroutines = 1.rangeTo(100).map {
// 创建100个协程
launch {
for(i in 1..100){
shareSum += 1
}
}
}
// 我们等待所以协程执行完成
coroutines.forEach {
// join() 会挂起协程,直到该作业完成
it.join()
}
}.join()
println(" 10000, $shareSum") // 10000, 9952
}
8 つのスレッドを持つコルーチン実行コンテキストを作成し、この実行コンテキストで 100 個のコルーチンを作成します。各コルーチンは、shareSum に 1 を追加します。その結果、正しい場合もあれば、正しくない場合もあります。
上記の 2 つの例でこれらのエラーが発生するのはなぜですか? これらのスレッドの作業プロセスは次のとおりです。
- まず、shareSum の現在の値を取得します。
- 次に、一時変数に保存し、各行の変数に 1 を追加します
- 最後に、この一時変数を shareSum に割り当てます
マルチスレッドの世界では、次の状況が発生する可能性があります。
- スレッドが shareSum の現在の値を取得し、別のスレッドが shareSum を取得する場合、これらのスレッドはすべて同じ shareSum を取得するため、これらのスレッドに 1 を追加した後、それらはすべて同じ値を shareSum に割り当てます。
- もう 1 つの例は、スレッドが shareSum を取得したばかりで、フォローアップ アクションを実行する準備ができたときに、一時変数の値を shareSum に保存する前に、別のスレッドが入ってきて、shareSum の値を取得し、1 を加算して、値が shareSum に保存され、最初のスレッドが 1 の追加を終了し、shareSum に保存された値が古い値になります (つまり、最新の shareSum に 1 を追加する必要があります)。
マルチスレッド操作で共有変数に問題を引き起こす可能性のある同様の状況が多数あります。これらの問題を解決するには、これらのスレッドの作業を同期するだけです。目的は、これらのスレッドが shareSum を操作するときに、最新の値を取得して 1 を追加する必要があることを確認することです。
揮発性キーワード
まず第一に、volatile キーワードは上記の問題を解決することはできませんが、ちなみにこれもみんなに共有されています。Java では volatile であり、kotlin で使用されますが@Volatile
、これはフィールドで使用されます。その役割は、メモリの可視性を提供し、読み取られるフィールドの値が CPU キャッシュ (つまり、CPU キャッシュ) ではなくメモリから取得されるようにすることです。したがって、このキーワードを持つフィールドについては、CPU がそれを読み取るときに、キャッシュ内の値を直接無視し、このフィールドの値をメモリから直接読み取ります。これにより、CPU はこのフィールドの最新の値を読み取る必要があります。
上記の問題に関しては、複数のスレッドが同じ値を読み取り、誤った結果を引き起こす状況があります。揮発性のテクニックは役に立ちません。シングルスレッドで有効です。ここでは拡大しません。
上記の問題を解決するために、各スレッドが shareSum の値を取得し、それを一時変数に保存して 1 を追加し、さらに shareSum に保存するというアトミックな保存操作が行われます。次のスレッドは始めることができます。各スレッドの操作がアトミックであることを確認すると、正しい結果が得られます。
解決策 1: 同期を使用する
例を修正します。
@Synchronized
fun accumulate(i: Int){
sum += i
}
2 番目の例を修正します。
@Test
fun hello() = runBlocking {
var coroutines = listOf<Job>()
// 使用固定大小的线程池创建协程的执行上下文
// @DelicateCoroutinesApi
//public actual fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher {
// require(nThreads >= 1) { "Expected at least one thread, but $nThreads specified" }
// val threadNo = AtomicInteger()
// val executor = Executors.newScheduledThreadPool(nThreads) { runnable ->
// val t = Thread(runnable, if (nThreads == 1) name else name + "-" + threadNo.incrementAndGet())
// t.isDaemon = true
// t
// }
// return executor.asCoroutineDispatcher()
//}
val scope = CoroutineScope(newFixedThreadPoolContext(8, "sizeFixedThreadPool"))
// 在不阻塞当前线程的情况下启动一个新的协程,并将对该协程的引用作为Job返回。可以用job取消当前的协程
val job = scope.launch {
// 提醒 scope这个范围里有8条线程在执行
coroutines = 1.rangeTo(100).map {
// 创建100个协程
launch {
for(i in 1..100){
accumulateShareSum()
}
}
}
// 我们等待所以协程执行完成
coroutines.forEach {
// join() 会挂起协程,直到该作业完成
it.join()
}
}.join()
println(" 10000, $shareSum") // 10000, 9952
}
var shareSum = 0
@Synchronized
fun accumulateShareSum(){
shareSum += 1
}
結果は毎回正しいです。気持ちいい!同期は、各スレッドが操作を完了した後、別のスレッドが操作に入ることができることを保証します。ここで言及することの 1 つは、Synchronized がメソッドに追加されているため、メソッド全体のアクセスが 1 つのスレッドに制限されていることです。次の同期方法を検討してください。
@Synchronized
fun accumulateShareSum(flag: Boolean){
if(flag) {
shareSum += 1
}
}
上記のメソッドは, 1 を加算する必要があるかどうかに関係なく, 任意の呼び出し元を同期します. これは実際にはあまり良いことではありません. より適切な同期ステートメントがあり, 共有変数を実際に操作する必要がある呼び出しを同期します:
@Synchronized
fun accumulateShareSum(flag: Boolean){
if(flag) {
synchronized(this){
shareSum += 1
}
}
}
解決策 2: アトミック プリミティブを使用する
実際には、すでにおなじみかもしれません. たとえばAtomicInteger
、 , AtomicReference
,AtomicBoolean
などは一般的なアトミック プリミティブです. これらは、開発者が使用できる多くのメソッドを提供し、アトミック操作を実現し、スレッド セーフを保証します. 上記の例のように:
var shareSum = AtomicInteger(0)
fun accumulateShareSum(){
shareSum.incrementAndGet()
}
解決策 3: ロック
ロックは、Synchronized 同期方法および同期言語よりも柔軟です。どこにでも現れる可能性があります。上記の問題を解決するために、再入可能ロックを使用するようになりました。
val reentrantLock = ReentrantLock()
var shareSum = 0
fun accumulateShareSum(){
reentrantLock.lock()
try {
shareSum += 1
} finally {
reentrantLock.unlock()
}
}
解決策 4: セマフォ
コードに直行しましょう:
val semaphore = Semaphore(1)
var shareSum = 0
fun accumulateShareSum(){
try {
semaphore.acquire()
shareSum += 1
} finally {
semaphore.release()
}
}
さらに、Java は多くの並行ツールとコレクション (HashTable、ConcurrentHashMap など) なども提供します。誰もが自由に調べることができます。CyclicBarrier と CountDownLatch はいくつかの同期ツールです。それを補いましょう。