让我们说我想让这段代码线程安全:
- (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];
}
}
答案 0 :(得分:48)
<击>哇。好的 - 我原来的绩效评估是错误的。让我变得愚蠢。
不是那么愚蠢。我的表现测试错了。固定。随着深入研究GCD代码。
更新:基准的代码可以在这里找到:https://github.com/bbum/StackOverflow希望现在是正确的。 :)
Update2:为每种测试添加了10个队列版本。
行。重写答案:
•@synchronized()
已存在很长时间了。它被实现为哈希查找以查找随后被锁定的锁。它“非常快” - 通常足够快 - 但在高争用下可能是一种负担(任何同步原语也是如此)。
•dispatch_sync()
不一定需要锁定,也不需要复制块。具体来说,在快速路径的情况下,dispatch_sync()
将直接在调用线程上调用块而不复制块。即使在 slowpath 的情况下,也不会复制该块,因为调用线程必须阻塞直到执行(调用线程被挂起,直到dispatch_sync()
之前的任何工作完成,然后线程恢复)。一个例外是主队列/线程上的调用;在这种情况下,块仍然没有被复制(因为调用线程被挂起,因此,使用堆栈中的块是正常的),但是有很多工作要在主队列上排队,执行,和然后恢复调用线程。
•dispatch_async()
要求复制块,因为无法在当前线程上执行,也不能阻止当前线程(因为块可能会立即锁定某些线程本地资源,该资源仅在dispatch_async()
之后的代码行上可用。虽然昂贵,dispatch_async()
将工作移出当前线程,允许它立即恢复执行。
最终结果 - dispatch_sync()
比@synchronized
快,但不是通常有意义的数量(在'12 iMac上,也不是'11 mac mini - 两者之间的#s非常不同,顺便说一句......并发的乐趣)。在非竞争情况下使用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()
与串行队列一起使用。没有任何开销 - 在竞争和非竞争情况下实际上都更快 - 并且使用队列更容易调试并且更容易在该工具中进行分析,并且调试器都具有用于调试队列的优秀工具(并且它们正在变得更好所有的时间)而调试锁可能是一种痛苦。
•将dispatch_async()
与不可变数据一起用于争用大量资源。即:
- (void) addThing:(NSString*)thing {
thing = [thing copy];
dispatch_async(_myQueue, ^{
[_myArray addObject:thing];
});
}
最后,使用哪一个来维护数组的内容并不重要。同步案例的争用成本非常高。对于异步情况,争用成本会下降,但可能会出现复杂性或奇怪的性能问题。
在设计并发系统时,最好保持队列之间的边界尽可能小。其中很大一部分是确保尽可能少的资源“生活”在边界的两边。
答案 1 :(得分:2)
我发现dispatch_sync()是一种不好的锁定方式,它不支持嵌套调用。
因此,您无法在串行Q上调用dispatch_sync,然后在具有相同Q的子例程中再次调用它。这意味着它的行为方式与@synchronized完全不同。
答案 2 :(得分:1)
确定, 我做了一些测试,结果如下:
锁定测试:意思是:2.48661,stdDev:0.50599
synchronized test:mean:2.51298,stdDev:0.49814
调度测试:平均值:2.17046,stdDev: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
答案 3 :(得分:-1)
有几件事情: 1)@Synchronize是某些显示器上的重锁版本(我个人更喜欢NSLock / NSRecursiveLock) 2)Dispatch_sync正在构建执行队列。
这两种方法都会在你的情况下产生类似的结果,但是对于这样一个简单的解决方案,如使收集线程更安全我更喜欢1。
为什么:
如果您有多个核心,那么多个线程可能同时工作。根据调度程序,他们将在监视器上锁定非常短的时间。
它比分配新块更轻,保留&#39;放入队列(这也是线程同步),并在工作队列准备就绪时执行。
在两种执行方式中都会有很大不同。
如果在某些时候您发现了大量使用集合,您可以考虑将更改锁定更改为读/写类型,如果您使用类似NSLock的类而不是sync_queue则更容易重构/更改。