31

このコードをスレッドセーフにしたいとしましょう:

- (void) addThing:(id)thing { // Can be called from different threads
    [_myArray addObject:thing];
}

GCD は、これを達成するための好ましい方法のようです。

- (void) addThing:(id)thing { 
    dispatch_sync(_myQueue, ^{  // _myQueue is serial.
        [_myArray addObject:thing];
    });    
}

従来の方法に比べてどのような利点がありますか?

- (void) addThing:(id)thing {
    @synchronized(_myArray) {
        [_myArray addObject:thing];
    }
}
4

4 に答える 4

51

わお。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];
    });    
}

最後に、配列の内容を維持するためにどちらを使用するかは問題ではありません。同期の場合、競合のコストは非常に高くなります。非同期の場合、競合のコストは大幅に削減されます、複雑さや奇妙なパフォーマンスの問題が発生する可能性は大幅に高まります。

並行システムを設計するときは、キュー間の境界をできるだけ小さく保つことが最善です。その大部分は、境界の両側で「生きている」リソースをできるだけ少なくすることです。

于 2013-07-11T17:54:29.050 に答える
1

わかりました、さらにいくつかのテストを行いました。結果は次のとおりです。

ロック テスト: 平均:2.48661、標準偏差:0.50599

同期テスト: 平均:2.51298、標準偏差:0.49814

ディスパッチ テスト: 平均:2.17046、標準偏差:0.43199

だから私は間違っていました、私の悪い:(誰かがテストコードに興味があるなら、それはここで役に立ちます:

static NSInteger retCount = 0;

@interface testObj : NSObject
@end

@implementation testObj

-(id)retain{
    retCount++;
    return [super retain];
}
@end

@interface ViewController : UIViewController{
    NSMutableArray* _a;
    NSInteger _c;
    NSLock* lock;
    NSLock* thlock;
    dispatch_queue_t _q;
}

- (IBAction)testBtn:(id)sender;
@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
}

-(NSTimeInterval)testCase:(SEL)aSel name:(NSString*)name{
    _a = [[NSMutableArray alloc] init];
    retCount = 0;
    //Sync test
    NSThread* th[10];
    for(int t = 0; t < 10;t ++){
        th[t] = [[NSThread alloc] initWithTarget:self selector:aSel object:nil];
    }

    NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
    for(int t = 0; t < 10;t ++){
        [th[t] start];
    }
    NSInteger thCount = 1;
    while(thCount > 0){
        thCount = 0;
        for(int t = 0; t < 10;t ++){
            thCount += [th[t] isFinished] ? 0 : 1;
        }
    }
    NSTimeInterval end = [NSDate timeIntervalSinceReferenceDate];
    NSLog(@"%@: %2.5f, retainCount:%d, _c:%d, objects:%d", name, end-start, retCount, _c, [_a count]);
    [_a release];
    for(int t = 0; t < 10;t ++){
        [th[t] release];
    }
    return end-start;
}

-(void)syncTest{
    for(int t = 0; t < 5000; t ++){
        [self synchronizedAdd:[[[testObj alloc] init] autorelease] ];
    }
}

-(void)dispTest{
    for(int t = 0; t < 5000; t ++){
        [self dispatchSyncAdd:[[[testObj alloc] init] autorelease] ];
    }
}

-(void)lockTest{
    for(int t = 0; t < 5000; t ++){
        [self lockAdd:[[[testObj alloc] init] autorelease] ];
    }
}


- (void) synchronizedAdd:(NSObject*)anObject
{
    @synchronized(self) {
        [_a addObject:anObject];
        _c++;
    }
}

- (void) dispatchSyncAdd:(NSObject*)anObject
{
    dispatch_sync(_q, ^{
        [_a addObject:anObject];
        _c++;
    });
}

- (void) lockAdd:(NSObject*)anObject
{
    [lock lock];
        [_a addObject:anObject];
        _c++;
    [lock unlock];
}

- (double)meanOf:(NSArray *)array
{
    double runningTotal = 0.0;

    for(NSNumber *number in array)
    {
        runningTotal += [number doubleValue];
    }

    return (runningTotal / [array count]);
}

- (double)standardDeviationOf:(NSArray *)array
{
    if(![array count]) return 0;

    double mean = [self meanOf:array];
    double sumOfSquaredDifferences = 0.0;

    for(NSNumber *number in array)
    {
        double valueOfNumber = [number doubleValue];
        double difference = valueOfNumber - mean;
        sumOfSquaredDifferences += difference * difference;
    }

    return sqrt(sumOfSquaredDifferences / [array count]);
}

-(void)stats:(NSArray*)data name:(NSString*)name{
    NSLog(@"%@: mean:%2.5f, stdDev:%2.5f", name, [self meanOf:data], [self standardDeviationOf:data]);
}

- (IBAction)testBtn:(id)sender {
    _q = dispatch_queue_create("array q", DISPATCH_QUEUE_SERIAL);
    lock = [[NSLock alloc] init];
    NSMutableArray* ltd = [NSMutableArray array];
    NSMutableArray* std = [NSMutableArray array];
    NSMutableArray* dtd = [NSMutableArray array];
    for(int t = 0; t < 20; t++){
        [ltd addObject: @( [self testCase:@selector(lockTest) name:@"lock Test"] )];
        [std addObject: @( [self testCase:@selector(syncTest) name:@"synchronized Test"] )];
        [dtd addObject: @( [self testCase:@selector(dispTest) name:@"dispatch Test"] )];
    }
    [self stats: ltd name:@"lock test"];
    [self stats: std name:@"synchronized test"];
    [self stats: dtd name:@"dispatch Test"];
}
@end
于 2013-07-16T07:38:22.033 に答える
-1

いくつかあります:1)@Synchronizeは一部のモニターでのロックの重いバージョンです(個人的にはNSLock/NSRecursiveLockを好みます)2)Dispatch_syncは実行キューを構築しています。

どちらのアプローチでも同様の結果が得られますが、コレクションをスレッドセーフにするなどの単純なソリューションの場合は、1.

どうして:

  • 複数のコアがある場合、複数のスレッドが同時に動作する可能性があります。スケジューラによっては、モニター上で非常に短時間ロックされます。

  • 新しいブロックを割り当て、キューに入れる「もの」を保持し(これもスレッド同期されます)、作業キューの準備ができたら実行するよりもはるかに軽量です。

  • どちらのアプローチでも、実行の順序は大きく異なります。

  • ある時点でコレクションの使用量が多いことに気付いた場合は、sync_queue の代わりに NSLock のようなクラスを使用すると、ロックを読み取り/書き込みタイプに変更することを検討できます。これは、リファクタリング/変更がはるかに簡単です。

于 2013-07-11T19:59:02.720 に答える