Objective-C の atomicity について調べてみる
PROGRAM
table.testcase th
{
white-space: nowrap;
}
table.testcase td
{
white-space: nowrap;
}
Objective-C の atomicity について調べてみる
Objective-C のプロパティには、属性として atomic や nonatomic というキーワードが指定できるようになっています。
これらを省略した場合は atomic が指定されたのと同じで、この "atomic" という単語を直訳すると "原子性" とか "不可分" とか、そういった意味合いになります。
このキーワードを知った当初には、次のような意味合いがあると認識していたのですけど、どうやらちょっと思っていたのと違うようです。
- atomic は、マルチスレッド環境でスレッドセーフを実現する、らしい。
- atomic プロパティを @synthesize で実装すると、setter や getter を @synchronized (self) で括ったのと同じになる、らしい。
それならつまり、単一スレッドでの使用を前提としないクラスを作成するときは、プロパティは atomic にして、メソッドも一連の処理を @synchronized (self) で括ってあげることで、スレッドセーフになるのだろうと思っていたのですけど…。
Objective-C のプログラミングに慣れるにつれて、それだけでは知識として到底不足している様子なことが判ってきました。
なんといっても、実行速度の問題
Objective-C のプロパティの属性が既定では atomic になってることから、スレッドセーフなクラスを作るのが基本なのだろうと思ったことが、間違いの始まりだったように思います。
そして、プロパティの atomic がクラスの整合性を保つためのロックと思い込んでしまったこと、atomic を自動実装すると @synchronized (self) でブロックしてくれるという情報を鵜呑みにしたことが、さらに間違いを大きくしました。
あるメソッドを実行すると、いくつかのインスタンス変数を書き換えながら、答えを出すクラスがあったとします。
インスタンスがいろんなスレッドで使用されるなら、それならメソッドを実行している途中に、他のスレッドから計算途中のインスタンス変数をプロパティで取得されないようにしないといけません。
スレッドセーフだからプロパティ atomic 属性を指定する、計算途中を取得されないようにメソッドの実装を @synchronized (self) でブロックする、そんな発想でいろいろなクラスを実装していたら、大変なことになってきました。
というのも、実行速度が目立って遅くなってきました。
簡単なプログラムを組んでいるときは気が付きにくいのですけど、実際に複数のスレッドを立ち上げてそれらを連携して処理するようになると、ちょっとしたところで明らかな処理の重さが目立ってきます。
遅さも UI 周りを操作していると顕著で、明らかにギクシャクした感じになります。
ブロックが不要なところを見極めてそこは @synchronized (self) を削除したり、可能であればインスタンスで値を設定した後はそれを変更させないようにしてブロック不要なクラスにしたりしてみましたけど、挽回できるほどの改善は見られません。
実行スレッドに制約を作る
そしてある日のこと、敢えて 1 つのスレッドで処理するようにすれば、ロックが不要になることを知りました。
解っていたつもりなのですけど、複数のスレッドを performSelector:onThread: を使って行き来していると、いつの間にかメインスレッドでも同時に 2 つの命令が実行できてしまいそうな感覚になって、ついつい忘れてしまってました。
各処理を、複数のスレッドで相互に実行するよりも、可能な限りそれぞれ 1 つのスレッド上で実行すれば、ロックしなければならない場面はなくなりはしないにせよ、今までがひどすぎたため、相当削減できるはずです。
そうして NSThread と CFRunLoopRun 周りをひと通りを理解して、スレッドをちゃんと意識したプログラムに修繕する準備が整いました。
atomic の思い違いに驚愕する
いざ、スレッドを意識した修繕にあたるにしても、どうしてもスレッドをまたいで制御しないといけないところはあるはずで、そうした時には今まで通り、スレッドセーフを意識してコードを書かないといけない場面が出てくるはずです。
これまで atomic や @synchronized (self) を決まりごとのように使っていただけで、理解できていないところがあったので、せっかく修繕するのだから自分なりに理解しようとしてみたところ、思っていたのとは全く違う動作をすることが判明しました。
これらの存在を知ったときに調べて見つけた、atomic を付けたプロパティを @synthesize で実装すると @synchronized (self) で括られるというお話、これが本当なのか実際に試してみたところ、なんとぜんぜん違いそうなことが判りました。
あるスレッドで @synchronized (self) でブロックしているメソッドを実行途中に、別のスレッドから atomic なプロパティを参照するという方法でテストしたのですけど、メソッドの処理が終わるまで待たされるだろうという予想とは裏腹に、プロパティ参照が瞬時に終わってしまったのでした。
実はどうやら atomic と @synchronized (self) は違うものをブロックしていた様子です。
つまりいままで、互いに待ちもしない atomic と @synchronized (self) で同期をとるという無駄なコードが随所に記されていて、しかもそれがクラスの矛盾を保護することもなく、単に速度を落とすためだけに実装されていたことになります。
改修前の再勉強
しかし、それならどうして atomic というキーワードが存在するのでしょう。
atomic と同等のブロック機構が存在しているのかと思って調べてみても、そういうものはどうやらなさそうな感じです。
そうだとすれば、メソッドの実行途中のインスタンス変数を、プロパティ経由で読み取られるのを禁止するには、プロパティを @synthesize ではなく自前で実装して、内部で @synchronized (self) によるブロックをしなければいけないことになります。
それ自体はできるのですけど、次の疑問が何故かどうしても拭えません。
- プロパティでは atomic が既定の属性になっている。(既定値)
- Xcode 4.4 から @synthesize を省略しても自動実装されるようになった。(既定値)
- atomic + @synthesize によるロックと同等の機能が提供されていない。
自分にはどうにも、Xcode が導く既定値と、クラスを矛盾から保護するためにプロパティを自分で実装して @synchronized (self) を書くという行為が、逆行したもののような気がしてなりませんでした。
Xcode に逆行する、それはすなわち、推奨されていないことと等価でしょう。
じゃあ、どうして推奨されていないのか、別の上手なやり方があるのか、そう思いながらもなかなか回答にはたどり着けません。
この答えが見えないままスレッドを整理する修繕を行って、果たして良いものができるだろうか。
でも、悩んでみても答えが見えてくる気がしなくて。
そんな中、ふと思ったことがありました。
今の実装は atomic と @synchronized (self) とが等価であることを期待して組んだプログラム。それが期待と違ったことを知って、そこで迷子になっている。
それなら、期待通りのプログラムに直したら、どうなるのだろう。
そもそもスレッドを頻繁に跨ぐプログラム自体が良くないことは見えてきたので、現状を期待通りに直すのも賢くないとは思ったのですけど、もしかするとそれによって何か見えるんじゃないかと思って、気は進まないながらも試してみることにしました。
@synthesize で実装していた atomic なプロパティを全て手動で実装して、内部で @synchronized (self) によるブロックに統一します。
その結果、どうしようもないくらいの速度低下に見舞われました。
複数スレッドからのアクセスを想定して、atomic を原則とし、メソッドの呼び出しとプロパティの呼び出しとの間に整合性をとるというスタイルが、明らかに間違っていると確信できた瞬間でした。
それでも atomic というキーワードの存在意義には、たどり着けてはいないのですけど。
むしろ、@synchronized で実装したときに、それ自体しか保護できないロックに何の価値があるのか。@synchronized で実装するということは、インスタンス変数とプロパティとが 1 対 1 でミス日ついているだけの実装です。
それを保護するという atomic という存在が、このときはまだ、不思議でなりませんでした。
コーディングの方針として
それでもだんだんとイメージが湧いてきたので、それならどういうスタイルでコーディングするのが良いか、迷いながらも整理してみることにしました。
- 原則は、単一スレッドでの使用を想定する。
- つまり、nonatomic を原則として、基本は @synchronized (self) しない。
- いちど値をセットしたらそれを書き換えなくても成り立つクラスなら、プロパティを全て readonly にして、ロックしなくても複数スレッドでの利用にも耐えられるようにしておくと楽。
- 複数スレッド間で機能を呼び出す必要があるクラスに限り、atomic や @synchronized を使って、速度を犠牲にする代わりにスレッドセーフを実現する。
だいたいの方向性としては、これで良さそうな気がします。
ただ、そう思い描いてはみたものの、いざそういう方針で行こうとするには、まだまだ理解が足りないところがたくさん残っているような気がします。
- atomic と @synchronized (self) が違うなら atomic は何をしているのだろう。
- atomic でスレッドセーフを実現するならメソッドでも同じブロックができないと意味がなさそうなのに。
- それなら atomic に何の価値があるのだろうか。
- そもそも、普通は複数スレッドでの実行を考えないなら、なぜ nonatomic ではなく atomic が既定値になっているのだろう。
そんな考えが頭の中を駆け巡って、とてもとても、心地よくプログラムを組めるどころではなさそうでした。
原子性の意味
そう思って "atomic" や "スレッドセーフ" をキーワードに調べを進めていたところ、C++ にも std:atomic というキーワードが用意されていることを知りました。
それを頼りに少しずつ知識を深めて行くと、スレッドセーフではない例として「32 ビット環境で 64 ビットの long long 型の値を扱うとき」というケースに行き着きました。
32 ビット環境では、64 ビットの値を読み書きするときに、実は 32 ビットずつ 2 回で扱うそうです。
そのため、あるスレッドで 64 ビットの変数に値を代入している「途中」の状態が存在します。そのまさに「途中」で、別のスレッドからその変数を読み取ると、32 ビットの片方だけしか書き換えられていない状態で、値を取得できてしまうそうです。
そのプログラムでやっていることは、つまり long long 型の変数 a で、あるスレッドで "a = 1" と "a = -1" とを交互に書き込んで、別のスレッドでそれを読んで表示するだけでしたけど、このときに 1 や -1 以外の値が取得できることが説明されていました。
これだけでも、マルチスレッドによる矛盾が生じることがあるんですね。
この矛盾を保護することが、すなわち原子性 (atomic) であるということを、ようやく理解できました。
自分はどうやらスレッドセーフを、もっとソフトウェア寄りの事象として思い込んでいたようです。
ちなみに C 言語の構造体 (struct) についても、この考え方が適用できるみたいです。
構造体 1 つで 1 つの変数として扱う都合、それを書き換えている途中を別スレッドから読み書きされないようにするかどうか、それが atomic か nonatomic かの判断の境目になってくるようでした。
複雑な構造体でも、単純な long long でも、1 つのデータが矛盾なく読み書きできるかどうか、とても肝心なところですね。
クラスという機能の集合体の整合性を保護する @synchronized (self) とは違う、もっと深い根本的なところを保護する役目が atomic にはありました。
Objective-C の atomicity を検証する
atomic の存在意義が理屈で捉えられるようになってきて、自分でも検証できそうな気がしてきたので、Objective-C での atomic について検証してみることにしました。
検証方法
検証方法は、次のような感じにしてみます。
- メインスレッドで「値」を持つインスタンスを作成します。
このインスタンスを複数のスレッドで操作します。 - スレッド A を立ち上げて、そこに「値」を持つインスタンスを渡します。
スレッド内で atomic プロパティで値を取得、書き換えた値を atomic プロパティで設定します。
これを、スレッドがキャンセルされるまで繰り返します。 - スレッドを B を立ち上げて、そこに「値」を持つインスタンスを渡します。
スレッド内で nonatomic プロパティで値を取得、書き換えた値を nonatomic プロパティで設定します。
これを、スレッドがキャンセルされるまで繰り返します。 - スレッドを C を立ち上げて、そこに「値」を持つインスタンスを渡します。
スレッド内で atomic プロパティで値を取得、書き換えた値を、プロパティが扱うインスタンス変数に直接代入します。
これを、スレッドがキャンセルされるまで繰り返します。 - スレッドをもうひとつ立ち上げて、そこで 50,000 回のテストループを回します。
テストは、スレッド A, B, C のそれぞれで書き換えている値を取得し、内容を画面に表示します。
ループが終了したら、スレッド A, B, C をキャンセルします。
要は、atomic と nonatomic で書き換えている値を、atomic や nonatomic で取得した時に値が壊れるかどうかを調べます。
なお、「値」の扱いは、プロパティから取得した値をいったんローカル変数に取り出して、ローカル変数の値を編集した後、その値をプロパティへ代入演算子「 = 」を使って代入しています。
ブロックした時としないときとの速度差も気になったので、50,000 回のループの間に、スレッド A, B, C のそれぞれで何回「値」を書き換えられたかについても調べてみました。
検証結果
その結果、次のような動きが見えてきました。
01.「値」が構造体で、プロパティを @synthesize で制御する場合
まずは、プロパティの原子性を atomic や nonatomic で制御する方法です。
プロパティの実装を @synthesize で自動実装することで、Objective-C が想定している原子性の制御を確認できます。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic @synthesize で自動実装 |
atomic @synthesize で自動実装 |
あり (OK) |
1,592,311 回/秒 |
struct | nonatomic @synthesize で自動実装 |
nonatomic @synthesize で自動実装 |
なし (OK) |
7,152,764 回/秒 |
struct | atomic @synthesize で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
2,071,805 回/秒 |
結果はこのように、atomic で制御されたプロパティだけが、値を矛盾させることなく完了しました。
実行にかかった時間は 27.96 秒です。
これが原子性を保護する (atomic) か、保護しない (nonatomic) かの違いということですね。
処理速度も興味深いところで、50,000 回の表示ループの中で、原子性を保つ atomic プロパティの書き換え回数よりも nonnatomic プロパティの書き換え回数の方が 4.4 倍以上も高速でした。
02.「値」が構造体で、プロパティを独自実装(@synthesize 無し)で制御する場合
次に気になったのが、プロパティを独自実装した時の atomic や nonatomic の挙動です。
@sinthesize で自動実装したときに atomic にはロックが自動で補われるというのなら、@synthesize しなかった場合は、自分でロックを実装しなければいけないかどうかが気になります。
そこで、プロパティ宣言は 01 と同じまま、実装だけを @synthesize に頼らずに実装してみることにしました。
この時、あえて @sychronized (self) は使用しないで、セッターでは純粋にインスタンス変数に値を代入し、ゲッターではインスタンス変数の値を return するだけの実装にします。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で @synchronized (self) なし |
atomic 独自実装で @synchronized (self) なし |
なし (NG) |
5,061,582 回/秒 |
struct | nonatomic 独自実装で @synchronized (self) なし |
nonatomic 独自実装で @synchronized (self) なし |
なし (OK) |
5,373,648 回/秒 |
struct | atomic 独自実装で @synchronized (self) なし |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
9,601,773 回/秒 |
結果は atomic も nonatomic も同じくらいの実行回数になりました。
実行時間は 32.40 秒です。
そして、期待通りと言いますか、今回の場合だと atomic の場合でも値に矛盾が発生することがありました。
このことから、プロパティの属性として atomic を指定していたとしても、@synthesize で自動実装しない限りは、ロック処理が前後につけられることはないようでした。
つまり、たとえば「スレッドセーフだけどロックは要らない」ような場面があるとしたら、属性に atomic を付けて明示した上で、プロパティを自分で実装すればいいことになります。
逆に、自分で atomic プロパティを実装するときには、自分でロックを実装しなければいけません。
■ プロパティへのアクセスは意外と負荷がかかる?
ところで、今回はどのケースでもロックが実装されなかったようなので、どれも同じくらいの速度になってもよさそうですけど、書き込みがインスタンス変数への直接代入というケースで、実行回数に大きな伸びが見られました。
その差は 1.9 倍くらいと、なかなかの具合です。
つまり、プロパティを通してインスタンス変数に書き込むのと、インスタンス変数に値を直接書き込むのとでは、ロックに関係なくこれくらいの速度差が生まれるということなのかもしれません。
頻繁な繰り返し処理で、速度を出したいところでは、インスタンス変数に直接アクセスするかどうかもカギになりそうですね。
03.「値」が構造体で、atomic, nonatomic プロパティが扱うインスタンス変数を直接操作する場合
それなら、プロパティが扱うインスタンス変数に直接値を読み書きしたらどうなるでしょう。
プロパティは @synthesize で自動実装します。値の確認はプロパティを介して行いますが、値の読み書きを繰り返すスレッドでは、インスタンス変数 (ivar) を直接読み書きします。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 検証時は @synthesize で自動実装のを使用 但し、書き換えループでは ivar 直接アクセス |
atomic インスタンス変数に直接代入 |
なし (NG) |
9,827,918 回/秒 |
struct | nonatomic 検証時は @synthesize で自動実装のを使用 但し、書き換えループでは ivar 直接アクセス |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
13,128,735 回/秒 |
struct | atomic 検証時は @synthesize で自動実装のを使用 但し、書き換えループでは ivar 直接アクセス |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
12,182,242 回/秒 |
結果は、大幅な速度アップになったようです。
実行にかかった時間は 34.43 秒と、それほど変わらなかったのですけど、実行できた回数が nonatomic のときでテスト 01 と比べて 1.8 倍くらい伸びています。
もっとも、読み書き用のスレッドでインスタンス変数を直接操作しているため、別のスレッドからいくら atomic 指定のプロパティを介してアクセスしても値は壊れてしまっています。
また、インスタンス変数を直接アクセスしてしまうと、派生クラスを作るときに融通が利きにくくなったりもするので、使いどころは注意しないといけないですけど、nonatomic で大丈夫なところで速度を出したい場面などでは、上手に使いたいところですね。
04.「値」が構造体で、プロパティのゲッターを @synthesize で自動実装、セッターは @synchronized (self) で制御する場合
ところで、本当に @synchronized (self) と atomic + @synthesize とが、関係ないことを確認してみます。
プロパティは読み取り専用で定義して、ゲッターだけを @synthesize 任せの atomic で制御します。プロパティのセッターは、別メソッドとして実装し @synchronized (self) を使って排他制御を行います。
atomic + @synthesize のロックと @synchronized (self) とが別ものであるなら、原子性が保てないはずです。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic @synthesize で自動実装 |
atomic 独自実装で @synchronized (self) あり |
なし (NG) |
700,081 回/秒 |
struct | nonatomic @synthesize で自動実装 |
nonatomic 独自実装で @synchronized (self) なし |
なし (OK) |
4,827,551 回/秒 |
struct | atomic @synthesize で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
3,601,352 回/秒 |
結果は、atomic 指定のプロパティでも、値に矛盾が見られました。
やはり atomic を @synthesize でロックしても、@synchronized (self) でそれをブロックすることはできないようです。
このように、atomic を @synthesize で実装した時に @synchronized (self) によるロックが自動的にされるという情報を鵜呑みにしていると、原子性は保てていないわ、速度は遅いわで、最悪の状態になることが判りました。
つまり自分は、最悪のコードを書いていた訳ですね。
ところで、今回ブロックに @synchronized (self) を使用しました。
今回は atomic のロックと @synchronized (self) と、ちぐはぐなロックで効果がなかったわけですけど、それにしても atomic テストで実行できた読み書き回数が、これまでよりも妙に少ないことが気になります。
ちなみに、実行にかかった時間は 28.59 秒でした。こちらはそれほど変わりませんね。
05.「値」が構造体で、プロパティを独自実装(@synthesize 有り)で制御する場合
そこで、全ての atomic 制御を @synthesize 任せではなく、独自に実装してみることにします。
ロックは @synchronized (self) を使って、atomic 指定のプロパティの、ゲッターとセッターの入り口から出口までをブロックします。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で @synchronized (self) あり |
atomic 独自実装で @synchronized (self) あり |
あり (OK) |
115,722 回/秒 |
struct | nonatomic 独自実装で @synchronized (self) なし |
nonatomic 独自実装で @synchronized (self) なし |
なし (OK) |
13,478,114 回/秒 |
struct | atomic 独自実装で @synchronized (self) あり |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
204,752 回/秒 |
実に nonatomic が atomic の 116.5 倍も高速という結果になりました。
テストにかかる実行時間は 27.32 秒と、@synthesize のときとそれほど変わりませんでした。そして、今回の @synchronized (self) でも @synthesize のときのように、値の矛盾を起こさないようにロックすることはできました。
何度か実行してみてもこのような感じの開きになるので、再確認にはなりますけど atomic を @synthesize して自動実装されたときのロックが @synchronized (self) ではないことが想像できます。
ここから、出来る限り @synchronized (self) による自前の排他制御ではなく、@synthesize が生成するロックを使う工夫をした方がいいと思われます。
06.「値」が構造体で、プロパティを独自実装(NSLock)で制御する場合
それなら、もう一つのロックの方法である NSLock を使う場合はどうでしょう。
インスタンス変数に 1 つの NSLock を用意して、それを 使って atomic 指定のプロパティのゲッターとセッターの入り口から出口までをブロックします。
@synchronized で self を指定したのと同じように、同じ 1 つのインスタンス変数をつかって、合計 3 箇所をロックします。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で NSLock あり |
atomic 独自実装で NSLock あり |
あり (OK) |
199,455 回/秒 |
struct | nonatomic 独自実装で NSLock なし |
nonatomic 独自実装で NSLock なし |
なし (OK) |
11,477,306 回/秒 |
struct | atomic 独自実装で NSLock あり |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
393,610 回/秒 |
結果は、@synchronized (self) を使った場合と比べてわずかに高速化したかどうか、といった感じのようです。
検証が終わるまでの時間が 1.5 秒ほど伸びたものの、atomic のときに計算した数が 1.8 倍に増加しています。逆に nonatomic の実行回数が 0.9 倍に減少しましたけど、他のスレッドの効率が良くなった分、それに足を引っ張られたのかもしれません。
ともあれ、クラス全体の整合性をとりたい場合は @synchronized (self) を使うより、NSLock を使った方が動作が速そうな気がします。
ただ、後で知ったのですけど @synchronized (self) が採用するロック方式は NSRecursiveLock と同じ再帰的ロックになるようです。これについてはまた後で検証することにします。
07.「値」が構造体で、プロパティを独自実装(個別の NSLock)で制御する場合
ところで、プロパティ単体の原子性の保護であれば、何も 2 つのプロパティで同じ NSLock を使う必要はないはずです。
もしかして @synthesize で実装する方が圧倒的に速いのは、そういったロックの仕方の違いなのかもしれません。
そこで、atomic なプロパティそれぞれで使用する NSLock インスタンスを用意して、それぞれを独立してロックさせてみることにしました。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で NSLock (A) あり |
atomic 独自実装で NSLock (A) あり |
あり (OK) |
816,172 回/秒 |
struct | nonatomic 独自実装で NSLock なし |
nonatomic 独自実装で NSLock なし |
なし (OK) |
8,710,430 回/秒 |
struct | atomic 独自実装で NSLock (B) あり |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
1,390,638 回/秒 |
試してみると、1 秒あたりの実行回数としては、全体でひとつの NSLock を使った場合と比べて atomic で 4.0 倍と、大幅な改善がみられました。
ただ、今回とても気になったのが、全体としての実行速度の遅さです。
実行が終わるのに 104.39 秒、全体でひとつの NSLock を使ったときの 29.04 秒と比べて 3.5 倍も遅くなってしまいました。
同じ 50,000 回のチェックが終わるまでの時間が長くなったということは、チェック時にプロパティを読み込む時の、ロックを取得するための待ち時間が増えたということになるのでしょうか…。
それとも、もしかすると各スレッドの処理能力が向上したため、その分 CPU が忙しくなって、50,000 回の監視処理を終えるまでの時間が長くなってしまったのかもしれません。
08.「値」が構造体で、プロパティを独自実装(個別の @synchronized)で制御する場合
プロパティごとに NSLock ではなく NSObject を用意して、それを @synchronized でブロックした場合はどうなるでしょう。
今回は、各プロパティ用のロック用のインスタンスとして NSLock の代わりに NSObject を生成して、それを @synchronized の引数に指定します。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で @syncrhonized (A) あり |
atomic 独自実装で @syncrhonized (A) あり |
あり (OK) |
476,905 回/秒 |
struct | nonatomic 独自実装で @syncrhonized なし |
nonatomic 独自実装で @syncrhonized なし |
なし (OK) |
9,203,034 回/秒 |
struct | atomic 独自実装で @syncrhonized (B) あり |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
837,022 回/秒 |
結果は、実行終了までにかかった時間は 98.39 秒だったので、それほど変わらない感じです。
ただ、その間に実行できた回数が、atomic で 0.5 倍に減少しています。nonatomic のときの実行回数に大きな差は見られなかったので、もしかすると @synchronized よりも NSLock の方が高速に動作するのかもしれないです。
09.「値」が構造体で、プロパティを独自実装(個別の NSLock)で制御する場合 & 読み書きのループを毎回 0.0000001 秒スリープ
ところで、あまりにも実行速度の遅さが目立ったので、試しとして読み書きのループのところでわずかな時間、スレッドをスリープさせるとどうなるか見てみます。
まずは NSLock を使ってプロパティをそれぞれロックする場合です。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で NSLock (A) あり |
atomic 独自実装で NSLock (A) あり |
あり (OK) |
71,745 回/秒 |
struct | nonatomic 独自実装で NSLock なし |
nonatomic 独自実装で NSLock なし |
なし (OK) |
74,883 回/秒 |
struct | atomic 独自実装で NSLock (B) あり |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
75,423 回/秒 |
結果は、終了するまでの時間は 11.52 秒になりました。
それだけ見れば 8.5 倍ほど速くなっていますけど、1 秒間に実行できた回数も大幅に減少しています。ループの合間にスリープを入れているのだから、実行回数が減少してくれて当然ですね。
それでいて終了までの時間が短くなっているのは、スリープによって負荷が軽減された分、50,000 回の監視処理が速く終わったのだと思われます。
10.「値」が構造体で、プロパティを独自実装(個別の @synchronized)で制御する場合 & 読み書きのループを毎回 0.0000001 秒スリープ
スリープを入れると余力がでるなら、この時の方が NSLock と @synchronized の速度差が確認しやすいかもしれません。
そこで、先ほどの NSLock の部分を @synchronized に換えてチェックしてみます。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で @syncrhonized (A) あり |
atomic 独自実装で @syncrhonized (A) あり |
あり (OK) |
64,906 回/秒 |
struct | nonatomic 独自実装で @syncrhonized なし |
nonatomic 独自実装で @syncrhonized なし |
なし (OK) |
72,563 回/秒 |
struct | atomic 独自実装で @syncrhonized (B) あり |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
70,638 回/秒 |
結果は、終了までの時間は 13.19 秒で、NSLock でブロックした場合に比べて 1.1 倍ほど時間がかかっています。
1 秒間に実行できた回数も NSLock のときと比べて 1 割ほど減少しているので、やはり @synchronized の方が NSLock よりも若干遅くなると思っていいのかもしれません。
11.「値」が構造体で、プロパティを独自実装(個別の semaphore)で制御する場合
ロックにはもう一つ、セマフォというものが利用できるので、これを使ってみたいと思います。
セマフォは、それぞれのプロパティ毎に dispatch_semaphore_create(1) で作成して、dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) で待つ形で実装してみます。
それでは、まずは読み書きスレッドの合間にスリープを挟まない場合を試してみます。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で dispatch_semaphore_wait (A) あり |
atomic 独自実装で dispatch_semaphore_wait (A) あり |
あり (OK) |
1,869,343 回/秒 |
struct | nonatomic 独自実装で dispatch_semaphore_wait なし |
nonatomic 独自実装で dispatch_semaphore_wait なし |
なし (OK) |
7,224,221 回/秒 |
struct | atomic 独自実装で dispatch_semaphore_wait (B) あり |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
3,067,934 回/秒 |
実行が完了するまでにかかった時間は、36.59 秒になりました。
これまで、@synchrnonized や NSLock を使ってプロパティを個別にブロックしたときの実行時間が、それぞれ 104.39 秒と 98.39 秒だったので、それと比べて随分の短縮が図れたようです。
そして 1 秒あたりの実行回数は、それらを大きく上回る結果になりました。NSLock とは 2.2 倍、@synchronized とは 3.9 倍という結果です。
@synthesize による自動実装と比べても 1.1 倍ほど処理回数が増えたので、自動実装と同等の性能を出したい場合にはこれがいいかもしれません。
12.「値」が構造体で、プロパティを独自実装(個別の semaphore)で制御する場合 & 読み書きのループを毎回 0.0000001 秒スリープ
もうひとつ、セマフォでロックして、読み書きのループで毎回スレッドをスリープさせるコードも試してみます。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で dispatch_semaphore_wait (A) あり |
atomic 独自実装で dispatch_semaphore_wait (A) あり |
あり (OK) |
75,524 回/秒 |
struct | nonatomic 独自実装で dispatch_semaphore_wait なし |
nonatomic 独自実装で dispatch_semaphore_wait なし |
なし (OK) |
72,471 回/秒 |
struct | atomic 独自実装で dispatch_semaphore_wait (B) あり |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
73,565 回/秒 |
結果は、処理時間 12.92 秒でした。
実行時間も実行回数も、NSLock や @synchronized とスリープを併用した場合と比べてほとんど変わりない様子です。
このことから、待ち時間なしの繰り返し処理みたいな場合でなければ、どれを使ってもそれほど大きな影響はないかもしれません。
逆に、局所的な for ループなどで、その中で atomic プロパティに頻繁にアクセスする場合には、@synthesize による自動実装かセマフォによるロックが活きてくるかもしれないですね。
13.「値」が構造体で、プロパティを独自実装(共通の semaphore)で制御する場合
ついでに、各プロパティで共通のセマフォを使ってロックする方法で試してみます。
セマフォは、ひとつだけ dispatch_semaphore_create(1) で作成して、dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) で待つ形で実装してみます。
今回は、読み書きループにはスリープを挟みません。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で dispatch_semaphore_wait あり |
atomic 独自実装で dispatch_semaphore_wait あり |
あり (OK) |
1,077,884 回/秒 |
struct | nonatomic 独自実装で dispatch_semaphore_wait なし |
nonatomic 独自実装で dispatch_semaphore_wait なし |
なし (OK) |
6,199,894 回/秒 |
struct | atomic 独自実装で dispatch_semaphore_wait あり |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
1,638,054 回/秒 |
実行にかかった時間は 33.25 秒でした。
ロックの種類が違うだけで、期待される効果は @synchronized (self) と同じと思うのですけど、実行時間が 6 秒ほど長くなったものの、1 秒あたりの実行回数が 9.3 倍向上しました。
セマフォはインスタンス変数に持たせているので、これなら @synthesize の自動実装と違って、メソッドを含めた複数個所の整合性を保つ用途にも使えそうですね。
実行回数はさすがに、プロパティを個別に保護する @synthesize による自動実行には及びませんけど、全体を保護できるロックでこれくらいの数値が出るのは嬉しいところです。
構造体以外についても検証してみる
ここまででもだいぶ解りましたけど、もう少し知識を深めておくために、他のケースについても調べてみます。
atomic + @synthesize と @synchronized (self) の違いは判ったので、ここでは atomic + @synthesize による実装を中心に見て行きたいと思います。
14.「値」が long long で、プロパティを @synthesize で制御する場合
今回の実験は 64 ビット変数の値が壊れることがあるということを知ったことがきっかけなので、その long long の原子性について検証してみたいと思います。
テスト環境は 64 ビット CPU 搭載の Mac mini Early 2009 ですけど、そこの iPhone 6.0 シミュレーター上でテストしているので、もしかするとデータに矛盾が出てきてくれるかもしれません。
実装としては、偶数回目の代入のときには -1 を、奇数回目の代入のときには 1 を代入して、それらの処理とは別に別スレッドから値を読み込んで、それが 1 や -1 以外になることが無いかを監視します。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
long long | atomic @synthesize で自動実装 |
atomic @synthesize で自動実装 |
あり (OK) |
2,395,152 回/秒 |
long long | nonatomic @synthesize で自動実装 |
nonatomic @synthesize で自動実装 |
なし (OK) |
7,367,794 回/秒 |
long long | atomic @synthesize で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
4,034,622 回/秒 |
このように、64 ビットの Mac でも iPhone 6.0 シミュレーター上であれば、プロパティを介して nonatomic な値を読み書きした場合だけ、矛盾を生じることが解りました。
15.「値」が long long で、プロパティのセッターを @synthesize で制御し、ゲッターをロックなしの独自実装にする場合
続いて、セッターは @synthesize で保護するけれど、ゲッターはロックなしで実装するという、少しちぐはぐな実装を試してみます。
具体的な実装としては、プロパティをいったん atomic で定義して @synthesize で実装したクラスから派生クラスを作成して、ゲッターだけをオーバーライドしてインスタンス変数をそのまま返す形をとってみます。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
long long | atomic オーバーライドして atomic を無効化 |
atomic @synthesize で自動実装 |
なし (NG) |
3,317,762 回/秒 |
long long | nonatomic @synthesize で自動実装 |
nonatomic @synthesize で自動実装 |
なし (OK) |
6,471,977 回/秒 |
long long | atomic @synthesize で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
4,138,025 回/秒 |
検証してみると、期待通り atomic プロパティが矛盾するようになりました。
16.「値」が long で、プロパティを @synthesize で制御する場合
それなら 32 ビットの long 型ならどうなるかと思って検証してみました。
今どきのものであれば 32 ビットを 1 回で読み書きできそうなものなので、これなら nonatomic であっても矛盾しないかもしれません。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
long | atomic @synthesize で自動実装 |
atomic @synthesize で自動実装 |
あり (OK) |
7,480,657 回/秒 |
long | nonatomic @synthesize で自動実装 |
nonatomic @synthesize で自動実装 |
なし (SAFE) |
6,974,549 回/秒 |
long | atomic @synthesize で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (SAFE) |
5,332,935 回/秒 |
結果は atomic も nonatomic も矛盾なしです。
現在の Objective-C の long 型は 32 ビットなので、このサイズなら複数スレッドからの同時アクセスでも影響を受けないのかもしれませんね。
そしてもう一つ、面白いことがでてきました。
long を対象とした atomic での操作と nonatomic での操作では、1 秒間に実行できた回数に、大きな差が見られなくなりました。
何度か実行してみても同じような数値だったので、もしかするとコンパイラが、今回の long 型の場合には原子性を確保する処理は必要ないとしてくれているのかもしれないですね。
もしそうだとすれば、この変数は何ビットだから nonatomic 指定にしても安全で速いかもとかいった配慮は不要で、原子性を保ちたければ atomic にするという意識でコーディングすることができそうです。
17.「値」が Objective-C インスタンスで、strong プロパティを @synthesize で制御する場合、クラスインスタンス再利用
ここからは、値が Objective-C インスタンスである場合を見てみます。
処理としては構造体での検証のときと同じですけど、構造体ではなくて id 型でインスタンスを持つ場合です。今回はあえて構造体と似せるために、インスタンス変数を @public で定義して、直接参照できるようにしてみます。
プロパティのオーナーシップは、まずは strong で試してみます。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
Objective-C インスタンス |
atomic @synthesize で自動実装 |
atomic @synthesize で自動実装 |
あり (NG, 値の矛盾もあり) |
15,367 回/秒 |
Objective-C インスタンス |
nonatomic @synthesize で自動実装 |
nonatomic @synthesize で自動実装 |
なし (OK, 値の矛盾もあり) |
40,553 回/秒 |
Objective-C インスタンス |
atomic @synthesize で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (OK, 値の矛盾もあり) |
15,292 回/秒 |
結果は、思ったよりも正しく動くことが多かったですけど、それでも何度も試してみると値が矛盾する場合があることが確認できました。
クラスインスタンスの場合は atomic 指定の場合であっても、インスタンスが持つ値が矛盾することがあります。
これは、あくまでもクラスインスタンスではポインタだけが保護されるので、そのインスタンスが持つ変数に値を代入するところは保護の対象にならないためです。
また、ごく稀に EXC_BAD__ACCESS で落ちることもありました。
これは何故か atomic でも発生しました。プロパティの取得だけなら atomic 指定なので問題ないはずなのですけど、どういう訳か "error: can't allocate region" というエラーで強制終了されてしまいます。
もしかすると今回は、クラスインスタンス 1 つを使いまわしていることと、インスタンス変数を直接アクセスしていることも、何か関係するのかもしれません。
そんな感じで、期待通りの動作が確認できないものの、それとは別の、プロパティが Objective-C クラスで strong 指定のときの atomic キーワードで保護される内容についてみて行きます。
bjective-C クラスのインスタンスの場合、原子性が保たれるのはあくまでも受け渡しをするポインターであって、それが持つ値については関与しないところに注意が必要です。
■ atomic, strong 時のゲッターは retain & autorelease で保護される
まず atomic, strong なプロパティのゲッターを呼び出してみると、retain & autorelease が呼ばれます。
これはつまり、retain して参照先のオートリリースプールに入れるまでの処理を不可分で実行しています。これによって、参照先が確実にインスタンスを確保することができます。
■ nonatomic, strong 時のゲッターは retain & autorelease されない
nonatomic なプロパティでは、ゲッターで取得したときには何も呼ばれませんでした。
■ strong で生成されるセッターについて
セッターは、retain プロパティの場合は atomic でも nonatomic でも、代入時にインスタンスが retain されます。
ただし、既に代入されているインスタンスをセットしようとした場合は何もしないようでした。
■ strong インスタンスの保護はまた別の問題
あくまでも @synthesize による実装では、保護されるのはクラスインスタンスのポインターだけなので、そのクラスが扱うプロパティについては実装先で保護する必要があります。
また、プロパティやメソッドを操作したときに、クラス全体としての整合性(一貫性)を保証するには NSLock や @synchronized (self) などといった、ブロックの仕組みが必要になってきます。
18.「値」が Objective-C インスタンスで、strong プロパティを @synchronized (self) で制御する場合、クラスインスタンス再利用
ついでにちょっと、@synthesize ではなく @synchronized (self) で保護した場合の速度の違いを見ておきます。
先ほどのテストのセッターとゲッターをオーバーライドして、@synchronized (self) でロックをするように実装します。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
Objective-C インスタンス |
atomic @synchronized (self)で手動実装 |
atomic @synchronized (self) で手動実装 |
あり (NG, 値の矛盾もあり) |
6,990 回/秒 |
Objective-C インスタンス |
nonatomic @synchronized (self)で手動実装 |
nonatomic @synchronized (self) で手動実装 |
なし (OK, 値の矛盾もあり) |
24,769 回/秒 |
Objective-C インスタンス |
atomic @synchronized (self)で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (OK, 値の矛盾もあり) |
9,152 回/秒 |
結果は、@synthesize による自動実装のときよりも 2 倍ほど遅くなりました。
やはりクラスインスタンスの場合でも、@synchronized (self) の方が速度は遅くなるようです。
また、こちらのテストも相変わらず、atomic でも nonatomic でも、稀に EXC_BAD_ACCESS (error: can't allocate region) で落ちる様子でした。
落ちる個所も、クラスインスタンスのポインターをインスタンス変数から読み取って return する行で発生したりすることがあったので、プロパティの atomic だけでは保護できない何かがあるのか、もっと何か根本的なバグがあるのかもしれません。
値として使用しているクラスのインスタンス変数を同時にアクセスしているところも気になるところではありますけれど。
19.「値」が Objective-C インスタンスで、strong プロパティを @synchronized (self) で制御する場合、クラスインスタンスを編集前に複製
先ほどのテストが予期しないエラーで落ちてしまうことがあるのが気にかかりつつ、ともあれ、スレッドセーフを考える上で、もう一つ意識しなければいけないところがあります。
プロパティの atomic が保護するのはあくまでもポインターなので、その先を同時に編集してしまうと、せっかく retain & autorelease でインスタンスの生存が保護されても、とたんに矛盾した結果になってしまいます。
そこから保護するには、プログラマーの側で、取得したポインターをローカル変数に複製してから編集して、編集が終わったインスタンスのポインターで、プロパティのポインターを置き換えるという作業が必要です。
つまり、同時アクセスを考慮する場合は、先ほどのテストのようなインスタンスを直接書き換える方法ではなく、インスタンスをコピーして書き換える方法の方が、きっと適切と言えるでしょう。
そういうことで、今度はインスタンスをコピーしてから編集する方法でテストしてみます。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
Objective-C インスタンス |
atomic @synchronized (self) で手動実装 |
atomic @synchronized (self) で手動実装 |
あり (OK, 値の矛盾もなし) |
4,270 回/秒 |
Objective-C インスタンス |
nonatomic @synchronized (self) で手動実装 |
nonatomic @synchronized (self) で手動実装 |
なし (NG, 解放済みの場合あり) |
11,180 回/秒 deallocated instance |
Objective-C インスタンス |
atomic @synchronized (self) で手動実装 |
nonatomic インスタンス変数に直接代入 |
なし (NG, 解放済みの場合あり) |
5,113 回/秒 deallocated instance |
今度は、自分のスレッドの中で自分だけで値を編集するので、値も矛盾することがなくなりました。
そしてもうひとつ重要なこととして、nonatomic の方はあっというまに EXC_BAD_ACCESS で落ちてしまいました。
今回の EXC_BAD_ACCESS は解放済みのインスタンス (deallocated instance) に触れてしまったために発生するもので、これはきっと、監視スレッドが nonatomic プロパティ経由でインスタンスを参照した直後に、読み書きスレッドがそのインスタンスを使い終わってしまったために起こっています。
このような、別のスレッドでまだ使用するインスタンスを解放してしまうという事態を防ぐ工夫が、@synthesize で実装した atomic プロパティには生成されます。
先ほども触れましたが、具体的にはゲッターで、retain & autorelease をロックの中で実行することで、インスタンスの解放を防いでいます。
20.「値」が Objective-C インスタンスで、strong プロパティを @synthesize で制御する場合、クラスインスタンスを編集前に複製(ARC 環境)
先ほどのテストとほとんど同じですが、プロパティを @synthesize による自動実装をした場合についても、参考として調べてみます。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
Objective-C インスタンス |
atomic @synthesize で自動実装 |
atomic @synthesize で自動実装 |
あり (OK, 値の矛盾もなし) |
7,036 回/秒 |
Objective-C インスタンス |
nonatomic @synthesize で自動実装 |
nonatomic @synthesize で自動実装 |
なし (NG, 解放済みの場合あり) |
9,676 回/秒 deallocated instance |
Objective-C インスタンス |
atomic @synthesize で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (NG, 解放済みの場合あり) |
7,083 回/秒 deallocated instance |
やはり処理できる回数は @synthesize の方が多くなりますね。
それ以外の様子については、@synchronized (self) を使って実装していた場合と同じです。
21.「値」が Objective-C インスタンスで、copy プロパティを @synthesize で制御する場合、クラスインスタンスを編集前に複製(Non-ARC 環境)
だいたい様子が見えてきましたが、ARC 環境で実行していると retain や release などが自動で実行されるため、動きが捉えにくいところがありました。
そこで、Non-ARC でコーディングした同等のクラスでも実験してみます。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
Objective-C インスタンス |
atomic @synthesize で自動実装 |
atomic @synthesize で自動実装 |
あり (OK, 値の矛盾もなし) |
8,057 回/秒 |
Objective-C インスタンス |
nonatomic @synthesize で自動実装 |
nonatomic @synthesize で自動実装 |
なし (NG, 解放済みの場合あり) |
12,079 回/秒 |
Objective-C インスタンス |
atomic @synthesize で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (NG, 解放済みの場合あり) |
10,554 回/秒 |
結果は、ARC 環境のときと違って、nonatomic の場合でもあっという間に強制終了されるようなことはなくなりました。
そうそう落ちない感じですけど、だからといって安全になったわけではないので注意が必要です。
ARC は細やかにメモリ管理をしてくれるので、タイミングがシビアになるのかもしれないですね。
ARC に対応させたらよく落ちるようになったという場合は、プロパティの atomicity が影響してたりするかもしれないです。
22.「値」が Objective-C インスタンスで、copy プロパティを @synthesize で制御する場合
続いて、プロパティで指定できる copy についても調べてみます。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
Objective-C インスタンス |
atomic @synthesize で自動実装 |
atomic @synthesize で自動実装 |
あり (OK, 値の矛盾もなし) |
5,362 回/秒 |
Objective-C インスタンス |
nonatomic @synthesize で自動実装 |
nonatomic @synthesize で自動実装 |
なし (NG, 解放済みの場合あり) |
6,838 回/秒 deallocated instance |
Objective-C インスタンス |
atomic @synthesize で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (NG, 解放済みの場合あり) |
6,745 回/秒 deallocated instance |
プロパティの属性が copy の場合も、retain のときとほとんど同じです。
今回も読み書きのループでインスタンスを copy しているので、メモリ管理が頻繁になる都合、nonatomic の場合はすぐに強制終了されてしまいます。
今回は新たに copy でのプロパティ操作を見てみたので、そのときの Objective-C インスタンスの保護のされ方についても整理してみます。
■ atomic, copy 時のゲッターは retain & autorelease で保護される
まず atomic, copy なプロパティのゲッターも、strong のときと同様、retain & autorelease が呼ばれます。ゲッターのときはあくまでも retain で、copy にはなりません。
このようにして、参照先が確実にインスタンスを確保できるように配慮してくれます。
■ nonatomic, copy 時のゲッターは retain & autorelease されない
nonatomic なプロパティでも、strong と同様、ゲッターで取得したときには何も呼ばれませんでした。
■ copy で生成されるセッターについて
セッターは、retain プロパティの場合は atomic でも nonatomic でも、代入時にインスタンスが copy されます。
strong のでは既に代入済みのインスタンスをセッターに渡すと無視されましたが、copy の場合は渡されたインスタンスの copy をとって設定する処理が必ず行われます。
■ copy のインスタンスの保護はまた別の問題
copy でも、あくまでも @synthesize による実装では、保護されるのはクラスインスタンスのポインターだけなので、そのクラスが扱うプロパティについては実装先で保護する必要があります。
ただ、copy に対応しているクラスの場合、copy メソッドでインスタンスの複製を取ることができるので、プロパティからインスタンスを取得したら copy で複製してから編集して、編集後にセッターに複製済みのクラスを渡すということを徹底することで、インスタンスの内容が矛盾するような状況を防げます。
このあたりの操作は、構造体が atomic で保護される様子と似ていますね。
23.「値」が Objective-C インスタンスで、weak プロパティを @synthesize で制御する場合
続いて weak の場合を見てみます。
weak には、格納時にはインスタンスは retain されませんが、どこかでそのインスタンスが解放されると、格納されているポインターが nil になるという性質があります。
マルチスレッドの場合でなくても、どこかで解放されてしまった後にそのインスタンスを使ってしまっても、不正アクセスで強制終了されるようなことがないので、delegate での使用にとても向いています。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
Objective-C インスタンス |
atomic @synthesize で自動実装 |
atomic @synthesize で自動実装 |
あり (OK, 解放済みは nil) |
9,261 回/秒 |
Objective-C インスタンス |
nonatomic @synthesize で自動実装 |
nonatomic @synthesize で自動実装 |
なし (OK, 解放済みは nil) |
8,995 回/秒 |
Objective-C インスタンス |
atomic @synthesize で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (OK, 解放済みは nil) |
9,266 回/秒 |
実験してみると、さすが weak の威力は絶大な様子です。
atomic でも nonatomic でも、タイミングによって nil が取得できますけど、これは、保持していたインスタンスが既に解放されていたためであって、weak としては期待通りです。
weak プロパティ自体は retain しないため、このように外部のインスタンス管理に依存するのですが、それに振り回されることなく捌ける様は流石でした。
今回は新たに weak でのプロパティ操作を見てみたので、そのときの Objective-C インスタンスの保護のされ方についても整理してみます。
プロパティの属性が weak の場合は、retain や copy とはまた違った動きを見せました。
■ weak で生成されるセッターについて
セッターは retain も copy もしませんけど、裏方ではきっと weak のための根回しがされているのでしょう。
■ weak 時のゲッターは autorelease だけされる様子
そして不思議なのがゲッターです。ARC のせいもあって確かなことが言いにくいのですけど、ゲッターの中では retain されず、なぜか autorelease だけが実行される様子です。
ここに限らず __weak 指定の変数にインスタンスを代入したときにも autorelease だけが呼ばれる挙動を示すようなので、もしかすると weak のシステム内部のどこかで retain されていたりするのかもしれないですね。
でももしそうだとすると、今回のテストで使った派生クラスの retain メソッドが呼ばれていないので、retain で独自の実装をしている場合は weak 環境で動作不良を起こすこともあるかもしれません。
そんな感じで、いまいちわかっていませんが、atomic でも nonatomic でも、weak のゲッターはこのような動きになるようです。
24.「値」が Objective-C インスタンスで、assign プロパティを @synthesize で制御する場合(Non-ARC 環境)
weak での動作で少しわからないところがあったので、ARC がなかったころの retain しない assign プロパティでの動作を見てみることにします。
retain などの制御が ARC の影響を受けないように Non-ARC で実装してみます。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
Objective-C インスタンス |
atomic @synthesize で自動実装 |
atomic @synthesize で自動実装 |
あり? (NG, 解放済みの場合あり) |
19,323 回/秒 |
Objective-C インスタンス |
nonatomic @synthesize で自動実装 |
nonatomic @synthesize で自動実装 |
なし (NG, 解放済みの場合あり) |
19,502 回/秒 |
Objective-C インスタンス |
atomic @synthesize で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (NG, 解放済みの場合あり) |
19,566 回/秒 |
結果、atomic でも nonatomic でも、解放済みのインスタンスを取得できてしまうことがありました。
atomic でもそうなる理由は、assign が保持するインスタンスは retain されないためで、それだけでなく、インスタンスを取得するときも retain & autorelease されないため、使用中にインスタンスが解放されないことがあるためです。
実行回数が atomic も nonatomic もほぼ同じになるあたり、どちらも完全に 32 ビット(ブロック不要)のアドレスを読み書きするだけという様子がうかがえますね。
ちなみに、Non-ARC での assign の場合、クラスインスタンスのプロパティやメソッドにアクセスしなければ、直ちにプログラムが強制終了されることはありませんでした。
これは、ARC が気を利かせて retain や release をしないせいで、ポインタだけ貰ってアクセスしないで済んでいるためのようです。
Objective-C クラスのインスタンス変数を @public で定義しておくと、解放済みのインスタンスからもそのインスタンス変数をエラーなく参照出来たりするみたいで、dealloc で解放済みかどうかのフラグを設定しておけば、解放済みかを判定できるという裏技的なこともできるようでした。
解放済みインスタンスのインスタンス変数をアクセスできるかどうかは未定義だと思うので、やらない方がいいでしょうけど。
さて、assign 指定での Objective-C インスタンスの保護のされ方について整理すると、次のような感じです。
■ assign で生成されるセッターについて
assign のセッターでは retain も copy もしません。assign が示す通りの実装です。
■ assign 時のゲッターも何もしない
そしてゲッターも、assign の場合は何もしません。
何もせずに、保持しているインスタンスのアドレスだけを素直に return します。
ただ、これって Objective-C のインスタンスを保護していると言えるのでしょうか。
atomic でもこの動作なのですけど、この実装ではロックの中で retain & autorelease をしてくれないので、明らかにタイミングによってはインスタンスが解放されてしまいます。
いつどこで解放されてもおかしくないインスタンスを扱うわけですから、それでいいのかもしれないですけど。
そんな面からも、同時アクセスから保護するような場合には、assign は向いていない様子です。
マルチスレッドで retain しないでインスタンスを管理するなら weak を使用するのが良さそうでした。
25.「値」が Objective-C インスタンスで、assign プロパティを @synthesize で制御する場合(ARC 環境)
念のため、ARC 環境での assign の動きについても見てみます。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
Objective-C インスタンス |
atomic @synthesize で自動実装 |
atomic @synthesize で自動実装 |
あり? (NG, 解放済みの場合あり) |
7,543 回/秒 deallocated instance |
Objective-C インスタンス |
nonatomic @synthesize で自動実装 |
nonatomic @synthesize で自動実装 |
なし (NG, 解放済みの場合あり) |
10,785 回/秒 deallocated instance |
Objective-C インスタンス |
atomic @synthesize で自動実装 |
nonatomic インスタンス変数に直接代入 |
なし (NG, 解放済みの場合あり) |
7,981 回/秒 deallocated instance |
結果は、さんざんなものでした。
ARC が気を利かせて即座に retain するものだから、atomic も nonatomic も即座に強制終了されてしまいます。
プロパティで扱うインスタンスも、assign のため、どこかで retain されることもなく、ARC があっという間に解放してくれるため、複数スレッドからのアクセスにすぐに潰れてしまう感じでした。
もちろん、今回のテストと相性がとても悪いだけで、実装方法を工夫すれば実用もできるでしょうけど、ARC ならどんな場面でも weak を使った方が良さそうに思えました。
追加の検証
これまでの検証結果を見ながら思いを巡らせているうちに、いくつか足りない情報があったので追加で検証してみました。
26.「値」が Objective-C インスタンスで、weak プロパティを独自実装(共通の NSLock)で制御する場合
weak プロパティの実装を @synthesize に頼らない場合に、どういう風に実装したらより近くなるのだろうと思って調べたときの実装です。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
Objective-C インスタンス |
atomic 独自実装で NSLock あり |
atomic 独自実装で NSLock あり |
あり (OK, 解放済みは nil) |
8,841 回/秒 |
Objective-C インスタンス |
nonatomic 独自実装で NSLock なし |
nonatomic 独自実装で NSLock なし |
なし (OK, 解放済みは nil) |
13,407 回/秒 |
Objective-C インスタンス |
atomic 独自実装で NSLock あり |
nonatomic インスタンス変数に直接代入 |
なし (OK, 解放済みは nil) |
9,405 回/秒 |
検証 23 と比べて実行回数が下がるのはさておき、独自にプロパティを実装してみると、ゲッターで例えば次のように内部で変数に一回受ける実装と @synthesize による方法とでは、動作にわずかな違いが見られました。
- (DataClass*)value
{
__weak result;
[_lock lock];
result = _value;
[_lock unlock];
return result;
}
このようにすると、ローカル変数 result に格納した時に 1 回、weak の作用と思われる autorelease が呼ばれた後、戻り値を変数で受けたもう 1 回、autorelease が呼ばれるような感じでした。
@synthesize で自動実装した場合には、一連の動作の中で weak のものと思われる autorelease の呼び出しが 1 度だけになります。
内部で受ける変数を __strong にしてみたり __unsafe_unretained にしてみても、@synthesize とは僅かに違う動作を見せるようです。
これを自動実装のときに似せようとした場合、次のように @try-@catch による実装に行き着きました。
- (DataClass*)value
{
[_lock lock];
@try
{
}
@finally
{
[_lock unlock];
}
}
このように、変数で受けずにインスタンス変数を直接 return するのが、@synthesize の実装に近い動きをするようです。
return した後にロックが解除されるように、@try-@finally を使って、スコープを抜けたタイミングで unlock するようにしています。
何もわざわざここまで @synthesize の挙動に似せる必要はないでしょうけど、参考として調べてみたくなったのでやってみました。
27.「値」が構造体で、プロパティを独自実装(共通の pthread mutex)で制御する場合(@try-@catch なし)
ロック方法に pthread mutex というのがあるのを知り、それを使ったロックも試してみることにしました。
今回は、構造体の値に対して、プロパティで共通の mutex を使用します。mutex の種類は PTHREAD_MUTEX_NORMAL を指定しました。
@synchronized (self) と比較する場合は PTHREAD_MUTEX_RECURSIVE の方が適切のようですけど、この時はその違いが判っていなかったので PTHREAD_MUTEX_NORMAL での検証です。
mutex を pthread_mutex_init(_mutex, NULL) で実装します。
そして、読み書きループにはスリープを挟みません。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で pthread_mutex_lock あり |
atomic 独自実装で pthread_mutex_lock あり |
あり (OK) |
260,385 回/秒 |
struct | nonatomic 独自実装で pthread_mutex_lock なし |
nonatomic 独自実装で pthread_mutex_lock なし |
なし (OK) |
14,790,370 回/秒 |
struct | atomic 独自実装で pthread_mutex_lock あり |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
501,189 回/秒 |
実行にかかった時間は 25.47 秒でした。
これまでの検証と比べると、dispatch_semaphore の方が 4.1 倍も速くなりました。逆に @synchronized (self) と比べると 0.4 倍と最も遅く、NSLock と比べると 0.7 倍の速度になりました。
これらはプロパティ間で共通のロックを使用しているのでより遅くなっているとは思いますが、@synthesize による実装の方が 6.1 倍速いところを見ても、可能であれば @synthesize に任せてしまうのがいちばん良さそうです。
28.「値」が構造体で、プロパティを独自実装(共通の pthread mutex)で制御する場合(@try-@catch あり)
@synchronized 以外のロックを使う場合、例外が想定される場面では @try-@finally で括る必要が出てきそうです。
その時にどれくらいの速度低下が見られる可能性があるか、先ほどの pthread mutex を @try-@catch を使うプログラムに、調べてみることにします。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で pthread_mutex_lock あり |
atomic 独自実装で pthread_mutex_lock あり |
あり (OK) |
203,680 回/秒 |
struct | nonatomic 独自実装で pthread_mutex_lock なし |
nonatomic 独自実装で pthread_mutex_lock なし |
なし (OK) |
13,246,130 回/秒 |
struct | atomic 独自実装で pthread_mutex_lock あり |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
362,664 回/秒 |
結果は、実行にかかった時間は 28.30 秒。
1 秒間に実行できた回数は、@try-@finally を使わなかった場合に比べて 0.8 倍程度に低下しています。
これくらいの速度低下なら、必要なところでは積極的に @try を使い、必要ないところを見極めて @try を使わない、そんな感じの実装で問題なさそうな気がしました。
29.「値」が構造体で、プロパティを独自実装(共通の NSRecursiveLock)で制御する場合
最後になって、Objective-C クラスとして提供されるロックには NSLock のほかにも NSRecursiveLock と NSConditionLock の 2 つが存在していることを知りました。
そのうちの NSRecursiveLock は、pthread mutex の PTHREAD_MUTEX_RECURSIVE と同等の再帰的ロックであるということで、ようやくロックには細やかな性質の違いが存在することに気が付きました。
どうやら @synchronized もこの再帰的ロックに該当するようです。
細かいお話は ミューテックスによる排他制御を行う ですることにして、ここではこの NSRecursiveLock の速度について検証しておくことにします。
値の型 | 読み込み時 | 書き込み時 | 期待する原子性 (指定通りか) |
1 秒間に 実行できた回数 |
---|---|---|---|---|
struct | atomic 独自実装で NSRecursiveLock あり |
atomic 独自実装で NSRecursiveLock あり |
あり (OK) |
175,189 回/秒 |
struct | nonatomic 独自実装で NSRecursiveLock なし |
nonatomic 独自実装で NSRecursiveLock なし |
なし (OK) |
15,706,507 回/秒 |
struct | atomic 独自実装で NSRecursiveLock あり |
nonatomic インスタンス変数に直接代入 |
なし (OK) |
331,134 回/秒 |
実行にかかった時間は 28.42 秒と、NSLock のときとほとんど一緒になりました。
1 秒間に実行できた回数は 0.9 倍弱に低下しています。一般に PTHREAD_MUTEX_DEFAULT (FAST) の方が PTHREAD_MUTEX_RECURSIVE よりも処理が軽量になるそうなので、その影響が出ているのでしょう。
ともあれこの程度であれば、用途に合わせて NSLock か NSRecursiveLock かを選んで使って行けそうです。
Objective-C の atomicity を検証して見えてきたこと
これまでの atomicity の調べで、ようやく選んで使えるくらいの予備知識が得られたような気がします。
今回の検証を始めてみるまで、ロックにいくつかの種類があることだけは知ってましたけど、細やかな違いまでは意識が届いていませんでした。
実行速度も大切なところでしょうけれど、そういった動きの違いも知っておかないと、ちゃんとしたプログラムは到底書けなそうです。
とりあえず、これまでの調査で得られた成果物のリンクを掲載しておきます。
- @property の atomic キーワードについて
- @synchronized による排他制御を行う
- ミューテックスによる排他制御を行う
- セマフォによる排他制御を行う
- NSLock による排他制御を行う
- NSRecursiveLock による排他制御を行う
後はこれらを実際に選んで使って行きながら、感覚を掴んで行けそうな気がします。
複数スレッドを考慮したときの考え方
他にも今回の調査の中で、複数スレッドからのアクセスを考慮したときのプログラミングの仕方で思うことがあったので、良さそうなものをわかる範囲で箇条書きにしてみます。
- 複数スレッドからアクセスする必要があるかを考える。(単一のスレッドで賄えるならそのようにする/単一スレッドであれば同時アクセスされる心配はない)
- 複数スレッドから同時アクセスされる可能性のあるプロパティは、int 型であっても atomic 属性を指定する。(long long でも同時アクセスで壊れる場合がある/保護が不要なデータ型かをコンパイラが判断して最適化してくれる様子)
- プロパティで atomic を指定しても、@synthesize を使わなければ、ディフォルトでは保護されない。(自分でロックして保護する必要がある)
- プロパティに atomic を指定すると、@synthesize が構造体もまるごと保護してくれるため、まとまったデータを矛盾なく扱うのに便利(構造体のコピーによる負荷はかかる)
- Objective-C インスタンスを扱うプロパティは、非 ARC 環境ではロック内で retain & autorelease することで、インスタンスの生存を保証できる。ARC 環境ではロック内で直接 return するか、ロックの内側で、ロックの外側にある __strong 変数に受ける。(retain する前に、他のスレッドで解放されてしまうのを防ぐのが重要)
- Objective-C インスタンスを扱うプロパティで、単一スレッドで編集するプログラムの場合は、必ずローカル変数に copy してから編集する。そして編集が終わってから、プロパティにそのインスタンスをセットする。(クラスが持つ複数のプロパティが矛盾するのを防ぐ/複数スレッドでの同時書き込みからは保護できない)
- コピー可能なインスタンスを扱うプロパティでは、copy 属性を指定する。(代入後に値が変更されるのを防ぐ/NSData 等の重たいクラスは別)
- 可能であれば、編集できないクラス (Immutable Class) を使用する。(変更されないクラスなら、インスタンス変数の保護が不要になる)
- ARC 環境では assign は使わない。(atomic 指定がスレッドセーフにならない/weak を使うことで、どこかのスレッドで解放されたときに nil が得られる)
- クラス全体の整合性を保ちたい場合は、@synchronized, mutex, semaphore, NSLock, NSRecursiveLock などのロックを使う。プロパティも含めて保護する必要がある場合は @synthesize に頼らず自分で実装する。(atomic 属性だけではクラスの整合性を保てない/atomic 属性はあくまでもプロパティが正しくインスタンス変数を返せるかが問題)
- 同一スレッドから 2 回以上、同じものをロックする必要がある場合には、再帰的ロックを使用する。(安直なデッドロックを回避する)
- ひとつのロックで広範囲をロックしすぎない。(極端な速度低下を招く)
- 複数のスレッドから呼び出さないプロパティは nonatomic を指定する。(単一スレッドからしかアクセスできないことが明確になる/@synthesize したときに不必要な保護の処理を入れずに済む)
- 排他制御で速度を求めるなら dispatch_semaphore がもっとも速い。手軽に組むなら @synchronized が最も楽で見通しが良い。インスタンス変数と 1 対 1 で関連付けられたプロパティで他でブロックする必要がないときは atomic + @synthesize が確実かつ最速。(速度を求める場面でなければ @synchronized が簡単そう)
とりあえず、こんなあたりを意識しながらプログラミングを行えば、勝手が見えてきそうな気がします。
読み取り専用のクラスにするのも効果あり
先ほども少し触れましたが、クラスを読み取り専用 (Immutable) にすることでも、複数スレッドからのアクセスに関する配慮を簡単にすることができそうです。
複数スレッドからのアクセスでクラスの整合性が崩れるのは、プロパティや内部で使うインスタンス変数への書き込みが発生したときにその途中のデータを読まれてしまうためなので、インスタンス生成時に値を設定したらその後は読み取り専用とすることで、そのような状況を発生させないようにできます。
ローカルスコープでインスタンスを生成して、生成後にそのインスタンスを atomic プロパティを通して設定すれば、書き込み中に別のスレッドから読まれることがなくなります。
書き込み可能なインスタンスも、編集はローカル変数にコピーしてから
ある 1 つのスレッドでクラスの内容を書き換える必要がある場合には、atomic プロパティでインスタンスを取得したら、編集を行う前にそのインスタンスを copy します。そして、編集が終わってから、編集したインスタンスを atomic プロパティ経由で設定します。
そうすることで、編集の途中という状態がローカルスコープに限定されるため、その途中のインスタンスを他のスレッドに参照されることはなくなります。
たとえプロパティに copy 属性が設定されていても、ゲッターから取得するときには retain されるだけなので、必ず複製してから編集するようにします。
なお、プロパティの copy 属性では、Mutable クラスをプロパティに設定した後で、その代入前のインスタンスを操作されたときに内容が変わらないように保護できます。
この copy 属性で保護する方法は NSString を受け取るプロパティでよく使われていて、そこに派生クラスである NSMutableString を代入されても、代入後に値を変更されるようなことを防ぎます。
特に readonly プロパティで有効ですけど、readwrite プロパティでも、NSMutableString の使いまわしで間違えて値が壊さないようにする方法としても使えると思います。
いずれにしても、複数のスレッドからプロパティを参照させる場合には、上記とは別にプロパティを atomic にするのを忘れないようにします。
特に Objective-C インスタンスの場合は、クリティカルセクションで retain & autorelease することでインスタンスの生存を保証できるようになるので、@synthesize で自動実装しない場合は、そのようになるようにプログラムを組む必要があります。
Lock-free という選択肢もあるらしい
他にも、複数スレッドによる同時アクセスから領域を保護するための方法として、Lock-free という考え方があるそうです。
Lock-free は難しそうだったので、勉強するのはまた必要になったときにしようと思いますが、考え方としては、共有メモリのポインタを使って現在の値を取得して、ローカルメモリで値を編集し、それを新しい値とするために共有メモリのポインタをアトミックに書き換えるという流れになるようです。
この、古い値と新しい値とを切り替える時に、CAS(コンペア・アンド・スワップ)というアトミックな仕組みを使うそうです。
CAS も単純な話ではなく、複数個所で同時書き換えが行われた場合の矛盾を吸収するために、期待した値が得られなかった場合に計算を再挑戦するという仕組みを上手く使うのだそうです。
またこのとき、共有メモリには古い情報と新しい情報とが共存していないといけないタイミングがあり、そのメモリの管理方法が一つの課題になるそうです。任意のタイミングで一括してメモリ解放処理をするガーベージコレクションと相性がいいとか、メモリを上手に再利用することで解放処理を回避するとか、テクニックがあるようですけど、こちらも理解が追い付きませんでした。
もしかすると Objective-C のオートリリースプールの考え方も、Lock-free に向いているのかもしれません。
他にも、リオーダーという問題があって、CPU が最適化のために命令の実行順序を書き換えることが、矛盾を招く結果につながることがあるそうです。そういったことを防ぐためにメモリーバリアーという発想があるようですけど、わかるようでわからないような感じです。
とにかくこの辺りの理屈が理解できないので、とりあえず当分 Lock-free はお預けです。
Lock-free queue など、なんだか魅力的な単語も見受けられたので、このあたりの理屈が見えるようになったら、可能性がもっと広がるように思うのですけれどね。それがどこまで役に立つかは、必要性も含めて解らないところですけど。
とりあえず今は、今回学んだ mutex や semaphore を使って出来るところまでやってみるのが今の自分には必要そうです。