わお。OK -- 私の最初の業績評価は完全に間違っていました。私を愚かに着色してください。
それほど愚かではありません。私のパフォーマンステストは間違っていました。修理済み。GCD コードを深く掘り下げます。
更新: ベンチマークのコードは次の場所にあります: https://github.com/bbum/StackOverflow うまくいけば、今は正しいです。:)
Update2: 各種類のテストの 10 キュー バージョンを追加しました。
わかった。答えを書き直す:
• <code>@synchronized() は長い間使用されてきました。これは、ロックされるロックを見つけるためのハッシュ ルックアップとして実装されます。これは「かなり高速」であり、一般的には十分に高速ですが、競合が多い場合は負担になる可能性があります (他の同期プリミティブと同様)。
•dispatch_sync()
必ずしもロックを必要とせず、ブロックをコピーする必要もありません。具体的には、高速パスの場合、dispatch_sync()
ブロックをコピーせずに、呼び出し元のスレッドでブロックを直接呼び出します。スローパスの場合でも、呼び出し元のスレッドは実行されるまでブロックする必要があるため、ブロックはコピーされません (呼び出し元のスレッドは、前にある作業がdispatch_sync()
完了するまで中断され、スレッドが再開されます)。1 つの例外は、メイン キュー/スレッドでの呼び出しです。その場合、ブロックはまだコピーされません (呼び出し元のスレッドが中断されているため、スタックからのブロックを使用しても問題ありません)。ただし、メイン キューへのエンキュー、実行、およびその後、呼び出しスレッドを再開します。
• <code>dispatch_async() は、現在のスレッドで実行することも、現在のスレッドをブロックすることもできないため、ブロックをコピーする必要がありました (コード行でのみ使用可能になるスレッド ローカル リソースをブロックがすぐにロックする可能性があるため)。コストはかかりますが、作業を現在のスレッドから移動し、すぐに実行を再開できるようにします。dispatch_async()
dispatch_async()
最終結果 --dispatch_sync()
よりも高速です@synchronized
が、一般的に意味のある量ではありません ('12 iMac と '11 mac mini では、2 つの間の # は非常に異なりますが、同時実行の喜びです)。使用dispatch_async()
は、競合しない場合の両方よりも遅くなりますが、それほどではありません。ただし、リソースが競合している場合は、「dispatch_async()」を使用すると大幅に高速になります。
@synchronized uncontended add: 0.14305 seconds
Dispatch sync uncontended add: 0.09004 seconds
Dispatch async uncontended add: 0.32859 seconds
Dispatch async uncontended add completion: 0.40837 seconds
Synchronized, 2 queue: 2.81083 seconds
Dispatch sync, 2 queue: 2.50734 seconds
Dispatch async, 2 queue: 0.20075 seconds
Dispatch async 2 queue add completion: 0.37383 seconds
Synchronized, 10 queue: 3.67834 seconds
Dispatch sync, 10 queue: 3.66290 seconds
Dispatch async, 2 queue: 0.19761 seconds
Dispatch async 10 queue add completion: 0.42905 seconds
上記を一粒の塩で考えてください。これは、実際の一般的な使用パターンを表していないという点で、最悪の種類のマイクロベンチマークです。「作業単位」は次のとおりで、上記の実行時間は 1,000,000 回の実行を表しています。
- (void) synchronizedAdd:(NSObject*)anObject
{
@synchronized(self) {
[_a addObject:anObject];
[_a removeLastObject];
_c++;
}
}
- (void) dispatchSyncAdd:(NSObject*)anObject
{
dispatch_sync(_q, ^{
[_a addObject:anObject];
[_a removeLastObject];
_c++;
});
}
- (void) dispatchASyncAdd:(NSObject*)anObject
{
dispatch_async(_q, ^{
[_a addObject:anObject];
[_a removeLastObject];
_c++;
});
}
(_c は、各パスの開始時に 0 にリセットされ、最後にテスト ケースの数に対して == であるとアサートされ、コードが時間を吐き出す前に実際にすべての作業を実行していることを確認します。)
競合していない場合:
start = [NSDate timeIntervalSinceReferenceDate];
_c = 0;
for(int i = 0; i < TESTCASES; i++ ) {
[self synchronizedAdd:o];
}
end = [NSDate timeIntervalSinceReferenceDate];
assert(_c == TESTCASES);
NSLog(@"@synchronized uncontended add: %2.5f seconds", end - start);
競合する 2 つのキューの場合 (q1 と q2 はシリアル):
#define TESTCASE_SPLIT_IN_2 (TESTCASES/2)
start = [NSDate timeIntervalSinceReferenceDate];
_c = 0;
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
dispatch_apply(TESTCASE_SPLIT_IN_2, serial1, ^(size_t i){
[self synchronizedAdd:o];
});
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
dispatch_apply(TESTCASE_SPLIT_IN_2, serial2, ^(size_t i){
[self synchronizedAdd:o];
});
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
end = [NSDate timeIntervalSinceReferenceDate];
assert(_c == TESTCASES);
NSLog(@"Synchronized, 2 queue: %2.5f seconds", end - start);
上記は、ワークユニットのバリアントごとに単純に繰り返されます (トリッキーなランタイムの魔法は使用されていません。copypasta FTW!)。
それを念頭に置いて:
•@synchronized()
見た目が気に入った場合に使用します。実際には、コードがその配列で競合している場合は、おそらくアーキテクチャの問題があります。 注:@synchronized(someObject)
オブジェクトが内部で@synchronized(self)
!
•dispatch_sync()
必要に応じてシリアル キューを使用します。オーバーヘッドはありません。実際には、競合する場合と競合しない場合の両方でより高速です。キューを使用すると、デバッグが容易になり、プロファイリングが容易になります。Instruments と Debugger には、キューをデバッグするための優れたツールがあります (そして、それらは改善されています)。常に)ロックのデバッグは面倒な場合があります。
•dispatch_async()
競合が激しいリソースの不変データとともに使用します。すなわち:
- (void) addThing:(NSString*)thing {
thing = [thing copy];
dispatch_async(_myQueue, ^{
[_myArray addObject:thing];
});
}
最後に、配列の内容を維持するためにどちらを使用するかは問題ではありません。同期の場合、競合のコストは非常に高くなります。非同期の場合、競合のコストは大幅に削減されますが、複雑さや奇妙なパフォーマンスの問題が発生する可能性は大幅に高まります。
並行システムを設計するときは、キュー間の境界をできるだけ小さく保つことが最善です。その大部分は、境界の両側で「生きている」リソースをできるだけ少なくすることです。