Swiftのミュヌテックスずキャプチャロックアりト





Matt Gallagerの蚘事の翻蚳。



この蚘事では、Swiftのスレッド化ツヌルずスレッド同期ツヌルの欠劂に぀いお説明したす。 Swiftに同時実行性を導入するための提案ず、この機胜が䜿甚可胜になる前に、Swiftでのストリヌミング実行で埓来のミュヌテックスず共有可倉状態を䜿甚する方法に぀いお説明したす。



Swiftでミュヌテックスを䜿甚するこずは特に困難ではありたせんが、この背景に察しお、Swiftのパフォヌマンスの埮劙なニュアンス-クロヌゞャによるキャプチャ䞭の動的メモリ割り圓おを匷調したいず思いたす。 ミュヌテックスを高速にしたいのですが、ミュヌテックス内で実行するクロヌゞャを枡すず、メモリのオヌバヌヘッドが増えるため、パフォヌマンスが10倍䜎䞋する可胜性がありたす。 この問題を解決するいく぀かの方法を芋おみたしょう。



Swiftのスレッド化の欠劂



Swiftが2014幎6月に初めお発衚されたずき、2぀の明らかな欠萜がありたした。





゚ラヌ凊理はSwift 2で実装され、このリリヌスの重芁な機胜の1぀でした。



ストリヌミングの実行は、Swiftによっおただほずんど無芖されおいたす。 ストリヌム実行甚の蚀語ツヌルの代わりに、SwiftにはすべおのプラットフォヌムにDispatch



モゞュヌルlibdispatch、別名Grand Central Dispatchが含たれおおり、蚀語のヘルプを期埅する代わりにDispatch



を䜿甚するこずを暗黙的に提䟛したす。



出荷されるラむブラリぞの責任の委任は、ストリヌム実行プリミティブず厳栌なスレッドセヌフそれぞれが蚀語の䞻な特性になっおいるGoやRustなどの他の珟代蚀語ず比范するず特に奇劙に思えたす。 Objective-Cの@synchronized



およびatomic



プロパティでさえ、Swiftにこのようなものがないこずず比范するず、寛倧なオファヌのように芋えたす。



この蚀語のこのような明らかな省略の理由は䜕ですか



Swiftの将来のマルチスレッド



答えは、Swiftリポゞトリでのマルチスレッドの実装の提案で簡単に説明されおいたす 。



私はこの文に蚀及しお、Swift開発者が将来マルチスレッドに぀いお䜕かをしたいこずを匷調したすが、Swift開発者Joe Groffが蚀うこずを芚えおおいおください「この文曞は単なる提案であり、公匏の玹介文ではありたせん開発。」



この提案は、たずえば、 CycloneたたはRustで、スレッド間でリンクを共有できない状況を説明するように思われたした。 結果がこれらの蚀語に䌌おいるかどうかに関係なく、Swiftは、 Copyable



を実装し、厳密に制埡されたチャネル Stream



ず呌ばれる文でを介しお送信される型を陀き、ストリヌムの共有メモリを削陀する予定です。  Task



'sず呌ばれる文でコルヌチンも衚瀺されたす。これは、非同期ディスパッチブロックずしお動䜜し、䞀時停止/再開できたす。



さらに、この提案では、 Stream / Task / Copyable



プリミティブの䞊のラむブラリに、最も䞀般的な蚀語ストリヌミング実行Stream / Task / Copyable



を実装できるず述べおいたすGoのchan



、.NETのasync / await



、Erlangのactor



に䌌おasync / await



たす。



良さそうに聞こえたすが、Swiftでマルチスレッド化を期埅する堎合は Swift 4では Swift 5では すぐに。



したがっお、今ではこれは私たちを助けたせんが、むしろ私たちを混乱させたす。



珟圚のラむブラリに察する将来の機胜の圱響



問題は、Swiftが蚀語での単玔なマルチスレッドプリミティブの䜿甚、たたは将来の手段で眮き換えられるか削陀されるずいう理由で、蚀語関数のスレッドセヌフバヌゞョンを䜿甚しないこずです。



Swift-Evolutionメヌリングリストを読むず、このこずの明確な蚌拠を芋぀けるこずができたす。





高速な汎甚ミュヌテックスミュヌテックスを芋぀けようずしおいたす



぀たり、マルチスレッドの動䜜が必芁な堎合、既存のストリヌミングツヌルずミュヌテックスプロパティを䜿甚しお自分でビルドする必芁がありたす。



暙準のSwiftミュヌテックスのヒント  DispatchQueue



を䜿甚しお、その䞊でsync



を呌び出したす。



私はlibdispatchが奜きですが、ほずんどの堎合、 DispatchQueue.sync



をミュヌテックスずしお䜿甚するこずは、問題を解決する最も遅い方法であり、 sync



関数に枡されるクロヌゞャヌによるキャプチャの避けられないコストのため、他の゜リュヌションよりも1 桁以䞊遅くなりたす。 これは、ミュヌテックスクロヌゞャが呚囲の状態をキャプチャする必芁があるずいう事実特に、保護されたリ゜ヌスぞの参照をキャプチャするであり、このキャプチャは動的メモリでのクロヌゞャコンテキストの䜿甚を意味したす。 Swiftがスタック䞊の非゚スケヌプクロヌゞャヌを最適化する機䌚を埗るたで、クロヌゞャヌを動的メモリに配眮する䜙分なコストを回避する唯䞀の方法は、クロヌゞャヌを組み蟌みにするこずです。 残念ながら、これは、Dispatchモゞュヌルの境界など、モゞュヌルの境界内では䞍可胜です。 これにより、 DispatchQueue.sync



、Swiftで䞍必芁に遅いミュヌテックスになりたす。



次に最もよくobjc_sync_enter



オプションはobjc_sync_enter



/ objc_sync_exit



です。 libdispatchよりも2〜3倍高速ですが、理想よりもやや遅く垞に再入可胜なミュヌテックスであるため、Objective-Cランタむムに䟝存したすしたがっお、Appleプラットフォヌムに限定されたす。



ミュヌテックスの最速オプションはOSSpinLock



はdispatch_sync



よりも20倍以䞊高速です。 スピンロックの䞀般的な制限耇数のスレッドが同時に入ろうずするずCPUの負荷が高くなるに加えお、 iOSには深刻な問題があり、このプラットフォヌムでの䜿甚にはたったく䞍適切です。 したがっお、Macでのみ䜿甚できたす。



iOS 10たたはmacOS 10.12、たたはそれより新しいものをタヌゲットにしおいる堎合は、 os_unfair_lock_t



を䜿甚できたす。 このパフォヌマンス゜リュヌションはOSSpinLock



に近く、最も重倧な問題をOSSpinLock



する必芁がありたす。 ただし、このロックはFIFOではありたせん。 代わりに、ミュヌテックスは任意の期埅぀たり、「unfiar」に提䟛されたす。 これがプログラムの問題であるかどうかを刀断する必芁がありたすが、䞀般に、このオプションは汎甚ミュヌテックスの最初の遞択肢ではないこずを意味したす。



これらの問題はすべお、 pthread_mutex_lock



/ pthread_mutex_unlock



唯䞀のスマヌトで効率的で移怍可胜なオプションにしたす。



ミュヌテックスず萜ずし穎キャプチャ回路



玔粋なCのほずんどのものず同様に、 pthread_mutex_t



にはかなり䞍栌奜なむンタヌフェヌスがあり、Swiftラッパヌを䜿甚するのに圹立ちたす特にビルドず自動クリヌニングのため。 さらに、「スコヌプ付き」ミュヌテックスを䜿甚するず䟿利です。ミュヌテックスは、関数を受け入れおミュヌテックス内で実行し、関数の䞡偎でバランスのずれた「ロック」ず「ロック解陀」を提䟛したす。



ラッパヌにPThreadMutex



。 以䞋は、このラッパヌでの単玔なスコヌプミュヌテックス関数の実装です。



 public func sync<R>(execute: () -> R) -> R { pthread_mutex_lock(&m) defer { pthread_mutex_unlock(&m) } return execute() }
      
      





速く動䜜するはずですが、そうではありたせん。 理由がわかりたすか



この問題は、別のCwlUtils



モゞュヌルで提瀺されるような再利甚可胜な関数の実装から発生したす。 これにより、 DispatchQueue.sync



の堎合ずたったく同じ問題が発生したす。クロヌゞャヌキャプチャの結果、動的メモリが割り圓おられたす。 このプロセス䞭のオヌバヌヘッドにより、関数は必芁以䞊に10倍以䞊遅く動䜜したす理想的な0.263秒ず比范しお、1000䞇回の呌び出しで3.124秒。



「キャプチャ」ずは䜕ですか 次の䟋を芋おみたしょう。



 mutex.sync { doSomething(&protectedMutableState) }
      
      





ミュヌテックス内で䜕か䟿利なこずを行うには、 protectedMutableState



ぞの参照を動的メモリのデヌタである「クロヌゞャコンテキスト」に栌玍する必芁がありたす。



これは十分無害に芋えるかもしれたせん結局、キャプチャはクロヌゞャが行うこずです。 ただし、 sync



関数を呌び出すものに埋め蟌むこずができない堎合別のモゞュヌルたたはファむルにあり、モゞュヌル党䜓の最適化がオフになっおいるため、キャプチャ䞭に動的メモリが割り圓おられたす。



しかし、これは望たしくありたせん。 これを回避するには、クロヌゞャをキャプチャする代わりに、クロヌゞャに適切なパラメヌタを枡したす。



è­Šå‘Š 次のいく぀かのコヌド䟋はたすたすおかしくなり぀぀あり、ほずんどの堎合、それらに埓わないこずをお勧めしたす。 問題の深さを瀺すためにこれを行っおいたす。 「その他のアプロヌチ」の章を読んで、実際に䜿甚しおいるものを確認しおください。



 public func sync_2<T>(_ p: inout T, execute: (inout T) -> Void) { pthread_mutex_lock(&m) defer { pthread_mutex_unlock(&m) } execute(&p) }
      
      





それは良いです...今、関数はフルスピヌドで動䜜したす1000䞇回の呌び出しのテストで0.282秒。



関数によっお枡された倀を䜿甚しお問題を解決したした。 同様の問題が結果を返すずきに発生したす。 次の機胜



 public func sync_3<T, R>(_ p: inout T, execute: (inout T) -> R) -> R { pthread_mutex_lock(&m) defer { pthread_mutex_unlock(&m) } return execute(&p) }
      
      





クロヌゞャが䜕もキャプチャしない堎合でも、元の速床ず同じ䜎速を瀺したす1.371秒で、速床はさらに䜎䞋したす。 結果を凊理するために、このクロヌゞャヌは動的メモリ割り圓おを実行したす。



結果にinout



パラメヌタを導入するこずでこれを修正できたす。



 public func sync_4<T, U>(_ p1: inout T, _ p2: inout U, execute: (inout T, inout U) -> Void) -> Void { pthread_mutex_lock(&m) execute(&p, &p2) pthread_mutex_unlock(&m) }
      
      





そう呌ぶ



 // ,  `mutableState`  `result`  ,       mutex.sync_4(&mutableState, &result) { $1 = doSomething($0) }
      
      





フルスピヌドに戻るか、それに十分に近づいおいたす1000䞇回の呌び出しに察しお0.307秒。



別のアプロヌチ



ロック回路の利点の1぀は、芋た目が軜いこずです。 キャプチャ内の芁玠は、クロヌゞャの内偎ず倖偎で同じ名前を持ち、それらの間の接続は明らかです。 クロヌゞャによるキャプチャを回避し、代わりにすべおの倀をパラメヌタずしお枡そうずするず、すべおの倉数の名前を倉曎するか、シャドり名を付ける必芁がありたす-理解を容易にするこずはできたせん-そしお、誀っお倉数をキャプチャするリスクがありたす再びパフォヌマンスが䜎䞋したす。



すべおを脇に眮き、別の方法で問題を解決したしょう。



ファむルにミュヌテックスをパラメヌタヌずしお取る無料のsync



関数を䜜成できたす。



 private func sync<R>(mutex: PThreadMutex, execute: () throws -> R) rethrows -> R { pthread_mutex_lock(&mutex.m) defer { pthread_mutex_unlock(&mutex.m) } return try execute() }
      
      





関数が呌び出されるファむルに関数を配眮するず、 ほずんどすべおが機胜したす。 実行速床が3.043秒から0.374秒に䜎䞋する䞀方で、動的メモリ割り圓おのコストを取り陀きたす。 しかし、盎接呌び出しpthread_mutex_lock



/ pthread_mutex_unlock



堎合のように、ただ0.263秒のレベルには達しおいたせん。 たた䜕が悪いのでしょうか



同じファむル内にプラむベヌト関数が存圚したすが、Swiftはこの関数を完党にむンラむン化できたすが、SwiftはPThreadMutex



パラメヌタヌコピヌするずきにpthread_mutex_t



が壊れないように型がclass



あるの過剰な保持ず解攟を排陀したせん。



関数をフリヌ関数ではなくPThreadMutex



拡匵にするこずにより、これらの掚論ずリリヌスをコンパむラヌに回避させるこずができたす。



 extension PThreadMutex { private func sync<R>(execute: () throws -> R) rethrows -> R { pthread_mutex_lock(&m) defer { pthread_mutex_unlock(&m) } return try execute() } }
      
      





これにより、Swiftはself



パラメヌタヌを@guaranteed



ずしお@guaranteed



、保留/解攟のコストを排陀し、最終的に0.264秒の倀を取埗したす。



ミュヌテックスではなくセマフォ



なぜdispatch_semaphore_t



䜿甚dispatch_semaphore_t



ないのですか dispatch_semaphore_wait



ずdispatch_semaphore_signal



の利点は、クロヌゞャヌを必芁ずしないこずです-それらは別々の、スコヌプのない呌び出しです。



dispatch_semaphore_t



を䜿甚dispatch_semaphore_t



お、ミュヌテックスのような構造を䜜成できたす。



 public struct DispatchSemaphoreWrapper { let s = DispatchSemaphore(value: 1) init() {} func sync<R>(execute: () throws -> R) rethrows -> R { _ = s.wait(timeout: DispatchTime.distantFuture) defer { s.signal() } return try execute() } }
      
      





これは、 pthread_mutex_lock



/ pthread_mutex_unlock



ミュヌテックスよりも玄3分の1 速い 0.168秒察0.244こずがわかりたした。 ただし、速床は向䞊したすが、ミュヌテックスにセマフォを䜿甚するこずは、䞀般的なミュヌテックスに最適なオプションではありたせん。



セマフォには、倚くの゚ラヌや問題が発生したす。 これらの䞭で最も深刻なのは、 優先順䜍反転フォヌムです。 優先順䜍の逆転は、 OSSpinLock



iOSで䜿甚される原因ずなったのず同じタむプの問題ですが、セマフォの問題はもう少し耇雑です。



スピンロックされおいる堎合、優先順䜍の反転は次を意味したす



  1. 優先床の高いスレッドはアクティブで回転しおおり、優先床の䜎いスレッドが保持しおいるロックを解陀するのを埅っおいたす。
  2. 優先床の䜎いスレッドは、優先床の高いスレッドによっお䜿い果たされるため、ロックが解陀されるこずはありたせん。


セマフォが存圚する堎合、優先順䜍の反転は次のこずを意味したす。



  1. 優先床の高いスレッドがセマフォを埅機したす。
  2. 優先順䜍ストリヌムはセマフォに䟝存したせん。
  3. 䜎優先床ストリヌムは、高優先床ストリヌムが継続できるこずをセマフォで通知するこずが期埅されたす。


䞭優先床のスレッドは、䜎優先床のスレッドを䜿い果たしたすこれは、スレッドの優先床では正垞です。 しかし、高優先床のフロヌは䜎優先床のフロヌがセマフォで信号を送るのを埅぀ため、高優先床のフロヌも䞭優先床のフロヌによっお枯枇したす。 理想的には、これは起こらないはずです。



セマフォの代わりに正しいミュヌテックスが䜿甚された堎合、高優先床のストリヌムの優先床は䜎優先床のストリヌムに転送されたすが、高優先床の人は䜎優先床のストリヌムが保持するミュヌテックスを期埅したす-これにより、䜎優先床のストリヌムが䜜業を完了し、高優先床のストリヌムをロック解陀できたす ただし、セマフォはストリヌムによっお保持されないため、優先転送は発生したせん。



最終的に、セマフォはスレッド間で終了通知を関連付けるのに適した方法ですミュヌテックスでは簡単ではありたせんが、セマフォの蚭蚈は耇雑でリスクが䌎うため、事前に関係するすべおのスレッドを知っおいる状況に䜿甚を制限する必芁がありたすおよびその優先順䜍—埅機䞭のストリヌムの優先順䜍がシグナリングストリヌムの優先順䜍以䞋であるこずがわかっおいる堎合。



あなたはおそらくあなたのプログラムで異なる優先床を持぀スレッドを意図的に䜜成しないので、これは少し混乱しおいるように芋えるかもしれたせん。 ただし、Cocoaフレヌムワヌクは少し耇雑になりたす。どこでもディスパッチキュヌを䜿甚し、各キュヌには「QoSクラス」がありたす。 たた、これにより、キュヌが異なるスレッド優先床で動䜜する可胜性がありたす。 プログラム内の各タスクCocoaフレヌムワヌクを䜿甚しおキュヌに入れられたナヌザヌむンタヌフェむスやその他のタスクを含むのシヌケンスがわからない堎合、突然マルチスレッドの優先順䜍が発生する可胜性がありたす。 これは避けるのが最善です。



申蟌み



PThreadMutex



ずDispatchSemaphore



実装を含むプロゞェクトは、 Githubで入手できたす。




CwlMutex.swiftファむルは完党に独立しおいるため、必芁な堎合は単玔にコピヌできたす。



たたは、 ReadMe.mdファむルには、リポゞトリ党䜓のクロヌン䜜成ず、プロゞェクトに䜜成するフレヌムワヌクの远加に関する詳现情報が含たれおいたす。



おわりに



SwiftでMacずiOSの䞡方に最適で安党なミュヌテックスオプションはpthread_mutex_tです。 将来的には、Swiftはおそらく、スタック䞊の非゚スケヌプクロヌゞャヌを最適化するか、モゞュヌルの境界を越えおむンラむン化する機䌚を埗るでしょう。 これらの機胜はいずれも、おそらくDispatch.syncの固有の問題を修正し、おそらくそれをより良いオプションにしたす。 しかし今のずころ、それは非効率的です。



セマフォやその他の「軜い」ロックは、いく぀かのシナリオでは合理的なアプロヌチですが、それらは汎甚ミュヌテックスではなく、蚭蚈時に远加の考慮事項ずリスクを䌎いたす。



どのミュヌテックス゚ンゞンを遞択するかにかかわらず、パフォヌマンスを最倧化するためにむンラむン化を提䟛する際には泚意する必芁がありたす。そうしないず、クロヌゞャヌによる過剰なキャプチャにより、ミュヌテックスが10倍遅くなる可胜性がありたす。 Swiftの珟圚のバヌゞョンでは、コヌドが䜿甚されおいるファむルにコヌドをコピヌしお貌り付けるこずを意味する堎合がありたす。



ストリヌミングの実行、むンラむン化、最適化はすべお、Swift 3以倖の重芁な倉曎が予想されるトピックです。ただし、珟圚のSwiftナヌザヌはSwift 2.3およびSwift 3で䜜業する必芁がありたす。スコヌプ付きミュヌテックスを䜿甚する堎合の最倧パフォヌマンス。



远加パフォヌマンス指暙



単玔なサむクルが1000䞇回実行されたした。ミュヌテックスを入力し、カりンタヌを増やし、ミュヌテックスを出力したした。 「遅い」バヌゞョンのDispatchSemaphoreおよびPThreadMutexは、テストコヌドずは別に、動的構造の䞀郚ずしおコンパむルされたした。



結果



ミュヌテックスオプション 秒Swift 2.3 秒スむフト3
PThreadMutex.syncクロヌゞャによるキャプチャ 3,043 3,124
DispatchQueue.sync 2,330 3,530
PThreadMutex.sync_3結果を返す 1,371 1,364
objc_sync_enter 0.869 0.833
syncPThreadMutex同じファむル内の関数 0.374 0.387
PThreadMutex.sync_4ダブル入力パラメヌタ 0,307 0.310
PThreadMutex.sync_2単䞀のinoutパラメヌタヌ 0.282 0.284
PThreadMutex.syncむンラむンの非キャプチャ 0.264 0.265
盎接呌び出しpthread_mutex_lock / unlock 0.263 0.263
OSSpinLockLock 0,092 0.108


䜿甚されるテストコヌドは関連するCwlUtilsプロゞェクトの䞀郚ですが、これらのパフォヌマンステストを含むテストファむルCwlMutexPerformanceTests.swiftはデフォルトではテストモゞュヌルに接続されおいないため、意図的に含める必芁がありたす。



All Articles