こんにちは。LINEアプリ開発本部のS_Shimotoriです。
私が所属しているLINEアプリ開発本部では、iOS Study SessionというiOSに関するトピックの勉強会を毎週行っています。聞き手は各分野の専門 家から実践的な知識を得ることができ、発表者は開発時の注意点や推しの技術などを広く周知できる場です。共有された内容は業務で大いに活用され、チーム全体の技術力の向上につながります。
具体的な発表内容は当番になったメンバーが自由に選びます。ベストプラクティスの共有はもちろん、最近実装した機能の説明、業務と関係なく"やってみた"、今流行りの技術の紹介など、内容は多岐にわたります。今年度に入ってからは、想像以上に手の込んだ既読のロジック、Device Attestationの仕組み、WWDC25で公開された新情報などを学びました。どれもわかりやすい説明で、他者の視点から新しい発見を得られるものばかりでした。
私にも発表の当番がまわってきたので、「Synchronization」というタイトルでSynchronizationフレームワークに関する発表を行いました。本記事ではその発表内容を一部抜粋してご紹介します。
Synchronizationとは
AppleプラットフォームにおいてSynchronizationと名のつくものは次の2つがあり、今回の主役は1つ目の方です。しかし2つ目も無関係ではありません。
- Synchronization | Apple Developer Documentation(Synchronizationフレームワーク)
- Synchronization | Apple Developer Documentation(osフレームワークのSynchronization API Collection)
Synchronizationフレームワークは昨年発表された言語機能で、 Atomic と Mutex の2つの機構が用意されています。とても小さなフレームワークですが2024年時点の最新の言語機能で実装されています 。
iOS Study Sessionは質疑応答込みで最長1時間の持ち時間しかないため、両者の概要と要点、そして Mutex 登場に至るまでのロックの歴史に絞って紹介しました。
題材
Atomic と Mutex は、どちらもデータを保護して排他制御するための仕組みです。二者とアクターの動作の違いを説明するため、『SE-0410 Low-Level Atomic Operations ⚛︎』で紹介されている次のサンプルソースコードを利用しました。
import Synchronization
import Dispatch
let counter = Atomic<Int>(0)
DispatchQueue.concurrentPerform(iterations: 10) { _ in
for _ in 0 ..< 1_000_000 {
counter.wrappingAdd(1, ordering: .relaxed)
}
}
print(counter.load(ordering: .relaxed))
上記のソースコードは Atomic をうまく利用しているので、最終的に counter はぴったり1,000万になります。
しかし counter を Int にすると、つまり Atomic<Int>(0) を 0 に wrappingAdd(_:ordering:) を += 1 にしてSwift 5言語モードで実行すると、 counter は決して1,000万になりません。インクリメントの計算は「現在の counter の値を読む」「読み取った値に1を足す」「計算結果を counter に書き込む」という複数のステップで構成されており、その間に別のスレッドが counter の値を書き換えてしまうためです。
そのため Atomic などに頼らなければ上記のコードは意図した通りに動作しません。
Atomic と Mutex の概要
Atomic<Value> はデータ長が1ワード以下のオブジェクトを保護できます。これはアトミック操作がCPU依存であるためで、行える操作も加算やビット演算などに限られます。複数の Atomic を組み合わせて使う場合はメモリオーダリングへの十分な理解も必要です。
次の図は Atomic を使用した場合の様子を図で表したものです。 counter への読み書きステップ(wrappingAdd(_:ordering:))は"アトミック"なので、 counter の値変化を表すグラフ上では小さな点で表現しています。
Mutex<Value> はどんなオブジェクトも排他制御できます。スレッドを長時間ロックする可能性があることに注意です。一方でアクターと異なり再入可能性が発生しません。
次の図は Mutex を使用した場合の様子を図で表した ものです。二つ目のスレッドが自分の順番が来るまでロックされています。
Atomic も Mutex もとてもプリミティブな機構です。使用時に await で待機する必要がありません。要するに同期関数から使うことができます。
そうは言っても、基本的にはSwift concurrencyで安全な非同期処理を実現するので Atomic や Mutex の出番は多くありません。Swift concurrency対応をしたいがアクターに置き換えられない、非同期関数を使用したくない、というケースであれば、一般的なiOSアプリでも Mutex を使う機会があるかもしれません。
それでも私がSynchronizationフレームワークについて調べたのは、チームメンバーから並行処理の本を紹介されたときにサンプルコードをSwiftで書いてみようと思ったことがきっかけです。そして社内の勉強会で取り上げようと思ったのは、iOS版のLINEでわずかながらアトミックの機構を使っていること(しかしアクターで十分足りそう!)、iOSアプリエンジニア歴の長いチームメンバーからロックの歴史に対して何かコメントをもらえるのではないかと期待したためです。私のiOSアプリ開発歴はロックの歴史と比較すると浅いので、過去の経緯を知るためにみんなの知恵を借りようと考えました。
ロックの歴史
Mutex はロックの一種です。内部では os_unfair_lock が使われています。これはosフレームワークのSynchronization API Collectionに収録されており、私たちも使うことができます。次のソースコードは Mutex が os_unfair_lock を使っている瞬間です。
public init() {
value = _Cell(os_unfair_lock())
}
しかしそうなると、なぜ新たなロックを再発明したのかという疑問が浮かびます。そこで発表の後半では過去のさまざまなロックを紹介しました。
NSLock や OSSpinLock はiOSの歴史の中ではとても古いロックです。特に OSSpinLock は優先順位の逆転に関する問題を抱えており、iOS 10以降deprecatedとなっています。発表を聞いてくれたチームメンバーのコメントによれば、deprecatedになった当時はコミュニティにかなりの混乱を引き起こしたということです。
Mutex につながるロックとして重要なのが、上記で引用したソースコードでも使われている os_unfair_lock とそれをSwift用に改良した OSAllocatedUnfairLock です。 OSAllocatedUnfairLock も Mutex も内部では os_unfair_lock を使っていますが、不用意にコピーされないようにする仕組みが洗練されていっています。
let legacyUnfairLock = os_unfair_lock()
let copiedLegacyUnfairLock = legacyUnfairLock // os_unfair_lockは構造体であるため、これはコピーされて誕生した別物。バグの温床
let unfairLock = OSAllocatedUnfairLock()
let sameUnfairLock = unfairLock // OSAllocatedUnfairLockはクラスであるため、これはunfairLockと同一オブジェクトを指す
let mutex = Mutex(0)
let invalidMutex = mutex // Mutexは構造体でありながらnon copyableであるため、ここでコンパイルエラーになる
os_unfair_lock() と OSAllocatedUnfairLock はDarwin環境でしか使えませんが、 Mutex はLinuxやWindows環境では各OSのロックを使うようになっているためどの環境でも使えます。
Synchronizationクイズ
try! Swift Tokyo 2025のスポンサーブースで出題したSynchronizationのクイズをチームメンバーにも出題しました。ぜひ皆さんも考えてみてください。
【第1問】Atomic<Value> の Value になれない型、つまり Atomic で保護できない型は次のうちどれでしょうか。Bool / Int? / UnsafePointer? / Never
【第2問】
次の4つのうち Sendable なものを全て選んでください。Atomic<Int> / Atomic<UnsafePointer<Int>> / Mutex<Int> / Mutex<UnsafePointer<Int>>
解答に行く前に……
try! Swift Tokyo 2025については次の記事で紹介しています。
それではクイズの解答です。
【第1問】
答え: Int?
解説:Atomic はCPU依存であるため、64ビット環境ではデータ長が64ビットまでのものしか扱えません。選択肢の中では Int? が制限を超えてしまっています。 Int だけでめいっぱい、さらに Optional か否かを表現しようとすると制限オーバーになります。UnsafePointer? はヌルポインタを 0 で表現するため Optional を表現するための追加のデータを必要とせず、よって制限に収まります。
【第2問】
答え: Atomic<Int> Mutex<Int> Mutex<UnsafePointer<Int>> の3つ
解説:Atomic<UnsafePointer<Int>> のみ、 Int へのアクセスを排他制御できていないので Sendable になってはいけません。Mutex<Value> はその性質から中身によらず Sendable です。だからこそSwift concurrency対応の選択肢のひとつに挙がります。
おわりに
LINEアプリ開発本部には様々な興味関心や経歴を持つメンバーがいるので、それぞれの観点からSynchronizationに対する感想とエピソードをもらうことができ、有意義な発表となりました。
Atomic も Mutex も従来から存在する仕組みで、Synchronizationフレームワークは小規模なものに思えますが、紐解いてみると様々な工夫があり利用時の制約にもきちんと理由があることが判明します。とてもプリミティブな機構なので、利用する際はじっくり検討 をしましょう。SynchronizationフレームワークはiOSであれば18.0以降から利用可能です。これまで使ってきたアトミック(swift-atomics等)をSynchronization版に置き換えるか、アクターを使うように切り替えるか、などの検討をするとよいでしょう。
iOS版のLINEの推奨環境は2025年7月時点でiOS 17.0以上です。iOS 17のサポート終了を待ってSynchronizationフレームワークのものに置き換えるのか、この機会に別の仕組みへ変更するか、私の発表がよい議論につながることを期待しています。