@synchronized是否保证线程安全?
参考这个答案 ,我想知道这是正确的吗?
@synchronized不会使任何代码“线程安全”
正如我试图find任何文件或链接来支持这个声明,没有成功。
任何意见和/或答案将不胜感激。
为了更好的线程安全,我们可以去其他工具,这是我所知道的。
如果使用正确, @synchronized
使代码线程安全。
例如:
比方说,我有一个类访问非线程安全的数据库。 我不想同时读写数据库,因为这可能会导致崩溃。
所以我们可以说我有两种方法。 storeData:readData和一个名为LocalStore的单例类。
- (void)storeData:(NSData *)data { [self writeDataToDisk:data]; } - (NSData *)readData { return [self readDataFromDisk]; }
现在,如果我将这些方法中的每一个分派到他们自己的线程上,如下所示:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[LocalStore sharedStore] storeData:data]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[LocalStore sharedStore] readData]; });
有可能是我们会崩溃。 但是,如果我们将storeData和readData方法更改为使用@synchronized
- (void)storeData:(NSData *)data { @synchronized(self) { [self writeDataToDisk:data]; } } - (NSData *)readData { @synchronized(self) { return [self readDataFromDisk]; } }
现在这个代码将是线程安全的。 需要注意的是,如果我删除了一个@synchronized
语句,那么代码将不再是线程安全的。 或者,如果我要同步不同的对象,而不是self
。
@synchronized在你正在同步的对象上创build一个互斥锁。 所以换句话说,如果任何代码想要访问@synchronized(self) { }
块中的代码,它将不得不排在所有先前在同一块内运行的代码之后。
如果我们要创build不同的localStore对象,那么@synchronized(self)
将只locking每个对象。 那有意义吗?
像这样想。 你有一大堆人分开等待,每一行都编号为1-10。 你可以select你希望每个人等待的线(通过每行同步),或者如果你不使用@synchronized
你可以直接跳到前面,跳过所有的线。 第1行的人不必等待第2行的人完成,但第1行的人必须等待他们前面的每个人完成。
我认为问题的实质是:
是否正确使用同步能够解决任何线程安全的问题?
技术上是的,但在实践中,build议学习和使用其他工具。
我会回答,而不会假设以前的知识。
正确的代码是符合其规范的代码。 一个好的规范定义
- 不变约束状态,
- 描述操作效果的先决条件和后置条件。
线程安全的代码是由多个线程执行时保持正确的代码。 从而,
- 没有任何操作顺序可能违反规范。 1
- 不变式和条件将在multithreading执行期间保持,而不需要客户端2进行额外的同步。
高层次的关键点是:线程安全要求在multithreading执行期间规范成立。 为了实际编码,我们必须做一件事:规范对可变共享状态的访问3 。 有三种方法可以做到这一点:
- 防止访问。
- 使状态不变。
- 同步访问。
前两个很简单。 第三个需要防止以下线程安全问题:
- 活跃度
- 死锁 :两个线程永久阻塞,等待对方释放所需的资源。
- 活锁 :线程忙于工作,但无法取得任何进展。
- 饥饿 :一个线程永远被拒绝获得它所需要的资源以取得进展。
- 安全发布 :发布对象的引用和状态必须同时对其他线程可见。
- 竞赛条件竞赛条件是输出取决于不可控事件的时间的缺陷。 换句话说,当得到正确的答案依赖于幸运的时机时,竞赛状况就会发生。 任何复合操作都会遇到竞争状态,例如“check-then-act”,“put-if-absent”。 一个例子的问题是,
if (counter) counter--;
,其中一个解决scheme是@synchronize(self){ if (counter) counter--;}
。
为了解决这些问题,我们使用@synchronize
,volatile,内存障碍,primefaces操作,特定锁,队列和同步器(信号量,障碍)等工具。
回到问题:
是否正确使用@synchronize能够解决任何线程安全的问题?
技术上来说,是因为上面提到的任何工具都可以用@synchronize
来模拟。 但是这会导致performance不佳,并增加生存相关问题的机会。 相反,您需要针对每种情况使用适当的工具。 例:
counter++; // wrong, compound operation (fetch,++,set) @synchronize(self){ counter++; } // correct but slow, thread contention OSAtomicIncrement32(&count); // correct and fast, lockless atomic hw op
在链接问题的情况下,您确实可以使用@synchronize
或者GCD读写锁,或者创build一个带有locking剥离的集合,或者任何情况所需的。 正确的答案取决于使用模式。 无论如何,你应该在你的课堂上logging你提供的线程安全保证。
1 也就是说,看到对象处于无效状态或违反前/后条件。
2 例如,如果线程A迭代了一个集合X,而线程B删除了一个元素,则执行会崩溃。 这是非线程安全的,因为客户端必须在X的内部locking( synchronize(X)
)上进行synchronize(X)
才能具有独占访问权限。 但是,如果迭代器返回集合的副本,则集合将变为线程安全的。
3 不可变共享状态或可变非共享对象始终是线程安全的。
一般来说, @synchronized
保证线程安全,但只有在正确使用的情况下。 recursion地获得锁也是安全的,尽pipe在我的答案中有详细的限制。
有几种常见的方法来使用@synchronized
错误。 这些是最常见的:
使用@synchronized
来确保创buildprimefaces对象。
- (NSObject *)foo { @synchronized(_foo) { if (!_foo) { _foo = [[NSObject alloc] init]; } return _foo; } }
由于_foo
在第一次获取锁时将为零,所以不会发生locking,并且多个线程可能会在第一次完成之前创build自己的_foo
。
每次使用@synchronized
locking一个新的对象。
- (void)foo { @synchronized([[NSObject alloc] init]) { [self bar]; } }
我已经看到这个代码相当多,以及C#等效lock(new object()) {..}
。 由于它每次都试图locking一个新的对象,它总是被允许进入代码的关键部分。 这不是某种代码魔法。 它确实没有丝毫的安全性。
最后,lockingself
。
- (void)foo { @synchronized(self) { [self bar]; } }
如果你的代码使用任何外部代码,或者本身就是一个库,它本身并不是一个问题,但这可能是一个问题。 在内部,该对象被称为self
,它在外部具有variables名称。 如果外部代码调用@synchronized(_yourObject) {...}
并调用@synchronized(self) {...}
,则可能发现自己处于死锁状态。 最好创build一个内部对象来locking,而不会暴露在对象之外。 添加_lockObject = [[NSObject alloc] init];
在你的init函数里面是便宜,简单,安全的。
编辑:
我仍然被问到关于这个post的问题,所以这里是一个例子,为什么在实践中使用@synchronized(self)
是一个坏主意。
@interface Foo : NSObject - (void)doSomething; @end @implementation Foo - (void)doSomething { sleep(1); @synchronized(self) { NSLog(@"Critical Section."); } } // Elsewhere in your code dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); Foo *foo = [[Foo alloc] init]; NSObject *lock = [[NSObject alloc] init]; dispatch_async(queue, ^{ for (int i=0; i<100; i++) { @synchronized(lock) { [foo doSomething]; } NSLog(@"Background pass %d complete.", i); } }); for (int i=0; i<100; i++) { @synchronized(foo) { @synchronized(lock) { [foo doSomething]; } } NSLog(@"Foreground pass %d complete.", i); }
明白为什么会发生这种情况。 lockingfoo
和lock
在前台VS后台线程上以不同的顺序调用。 这很容易说这是不好的做法,但是如果Foo
是一个库,用户不太可能知道代码包含一个锁。
单独使用@synchronized不会使代码线程安全,但它是编写线程安全代码所使用的工具之一。
对于multithreading程序,通常情况下,您希望以一致的状态维护一个复杂的结构,并且一次只需要一个线程访问。 常见的模式是使用互斥体来保护访问和/或修改结构的代码的关键部分。
@synchronized
是thread safe
机制。 在这个函数中写的一段代码成为critical section
的一部分,一次只能执行一个线程。
@synchronize
隐式应用锁,而NSLock
明确应用它。
它只保证线程安全,不保证。 我的意思是你为你的汽车雇佣了专家司机,但是并不能保证汽车不会遇到事故。 然而,可能性仍然是丝毫的。
GCD
(grand central dispatch)中的伙伴是dispatch_once
。 dispatch_once执行与@synchronized
相同的工作。
@synchronized
指令是在Objective-C代码中快速创build互斥锁的一种便捷方式。
互斥锁的副作用:
- 死锁
- 饥饿
线程安全将取决于@synchronized
块的使用。