@synchronized是否保证线程安全?

时间:2013-03-13 17:48:51

标签: ios objective-c macos cocoa synchronized

参考这个answer,我想知道这是正确的吗?

  

@synchronized不会使任何代码“线程安全”

当我试图找到支持此声明的任何文档或链接时,没有成功。

任何评论和/或答案都将受到赞赏。

为了更好的线程安全,我们可以使用其他工具,我知道。

6 个答案:

答案 0 :(得分:38)

@synchronized如果使用得当,确实可以使代码线程安全。

例如:

假设我有一个访问非线程安全数据库的类。我不想同时读取和写入数据库,因为这可能会导致崩溃。

所以我想说我有两种方法。 storeData:和一个名为LocalStore的单例类的readData。

- (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在您正在同步的对象上创建互斥锁。换句话说,如果任何代码想要访问@synchronized(self) { }块中的代码,则必须在同一块内运行的所有先前代码之后排队。

如果我们要创建不同的localStore对象,@synchronized(self)将仅单独锁定每个对象。这有意义吗?

这样想。你有一大群人在不同的行中等待,每行编号为1-10。您可以选择希望每个人等待的行(通过基于每行进行同步),或者如果您不使用@synchronized,则可以直接跳到前面并跳过所有行。第1行中的某个人不必等待第2行中的某个人完成,但第1行中的人确实必须等待他们前面的所有人完成。

答案 1 :(得分:22)

我认为问题的实质是:

  

是正确使用同步能够解决任何线程安全问题   问题

技术上是,但在实践中,建议学习和使用其他工具。


我会在不假设先前知识的情况下回答。

正确的代码是符合其规范的代码。一个好的规范定义

  • 约束状态的不变量,
  • 描述操作效果的前提条件和后置条件。

线程安全代码是由多个线程执行时保持正确的代码。因此,

  • 没有一系列操作可能违反规范。 1
  • 在多线程执行期间,不变量和条件将保持不需要客户端 2 进行额外同步。

高级别的要点是:线程安全要求在多线程执行期间规范成立。要实际编写代码,我们必须做一件事:规范对可变共享状态 3 的访问。有三种方法可以做到:

  • 阻止访问。
  • 使状态不可变。
  • 同步访问权限。

前两个很简单。第三个要求防止以下线程安全问题:

  • 存活
    • 死锁:两个线程阻止永久等待彼此释放所需的资源。
    • livelock :线程正忙着工作,但无法取得任何进展。
    • 饥饿:线程永远被拒绝访问所需的资源以取得进展。
  • 安全发布:必须同时向其他线程显示已发布对象的引用和状态。
  • 竞争条件竞争条件是输出取决于无法控制事件的时间的缺陷。换句话说,当获得正确的答案依赖于幸运时间时,会发生竞争状况。任何复合操作都可能遭遇竞争条件,例如:“check-then-act”,“put-if-absent”。一个示例问题是if (counter) counter--;,其中一个解决方案是@synchronize(self){ if (counter) counter--;}

为了解决这些问题,我们使用@synchronize,volatile,内存障碍,原子操作,特定锁,队列和同步器(信号量,障碍)等工具。

回到问题:

  

正确使用@synchronize能够解决任何线程安全问题   问题

技术上是的,因为上面提到的任何工具都可以使用@synchronize进行模拟。但这会导致表现不佳并增加与生活相关的问题的机会。相反,您需要为每种情况使用适当的工具。例如:

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读写锁,或创建带锁剥离的集合,或者无论情况需要什么。正确的答案取决于使用模式。无论如何,你应该在你的课程中记录你提供的线程安全保证。


1 即,查看无效状态的对象或违反前/后条件。

2 例如,如果线程A迭代集合X,并且线程B删除了一个元素,则执行崩溃。这是非线程安全的,因为客户端必须在X(synchronize(X))的内部锁上进行同步才能拥有独占访问权限。但是,如果迭代器返回集合的副本,则集合将变为线程安全。

3 不可变共享状态或可变非共享对象始终是线程安全的。

答案 2 :(得分:9)

通常,@synchronized可确保线程安全,但仅在正确使用时才能保证。以递归方式获取锁也是安全的,尽管我在答案中详细说明了here

有几种常见的方法可以使用@synchronized错误。这些是最常见的:

使用@synchronized确保原子对象的创建。

- (NSObject *)foo {
    @synchronized(_foo) {
        if (!_foo) {
            _foo = [[NSObject alloc] init];
        }
        return _foo;
    }
}

因为首次获取锁时_foo将为nil,所以不会发生锁定,并且多个线程可能会在第一次完成之前创建自己的_foo

每次使用@synchronized锁定新对象。

- (void)foo {
    @synchronized([[NSObject alloc] init]) {
        [self bar];
    }
}

我已经看到了这个代码,以及C#等效lock(new object()) {..}。由于它每次都试图锁定一个新对象,因此它总是被允许进入代码的关键部分。这不是某种代码魔术。它绝对没有确保线程安全。

最后,锁定self

- (void)foo {
    @synchronized(self) {
        [self bar];
    }
}

虽然本身不​​是问题,但如果您的代码使用任何外部代码或本身就是一个库,则可能是一个问题。虽然在内部将对象称为self,但它在外部具有变量名称。如果外部代码调用@synchronized(_yourObject) {...}并且您调用@synchronized(self) {...},则可能会发现自己处于死锁状态。最好创建一个内部对象来锁定不会暴露在对象外部的对象。在init函数中添加_lockObject = [[NSObject alloc] init];既便宜又简单,而且安全。

编辑:

我仍然会被问到有关这篇文章的问题,所以这里有一个例子说明为什么在实践中使用@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);
}

应该明白为什么会发生这种情况。锁定foolock将在前台VS后台线程上以不同的顺序进行调用。很容易说这是不好的做法,但如果Foo是一个库,则用户不太可能知道代码包含锁。

答案 3 :(得分:4)

单独使用@synchronized不会使代码线程安全,但它是编写线程安全代码时使用的工具之一。

对于多线程程序,通常需要将复杂结构保持在一致状态,并且您希望一次只能访问一个线程。常见的模式是使用互斥锁来保护访问和/或修改结构的关键代码段。

答案 4 :(得分:3)

@synchronizedthread safe机制。在此函数内编写的代码片段成为critical section的一部分,一次只能执行一个线程。

@synchronize隐式应用锁定,而NSLock明确应用锁定。

它只能确保线程安全,而不是保证。我的意思是你为你的车雇用专家司机,但它不能保证车不会遇到意外。然而,概率仍然是最轻微的。

GCD(大中央调度)中的伴侣是dispatch_once。 dispatch_once与@synchronized完成相同的工作。

答案 5 :(得分:1)

@synchronized指令是在Objective-C代码中动态创建互斥锁的便捷方式。

互斥锁的副作用:

  1. 死锁
  2. 饥饿
  3. 线程安全将取决于@synchronized块的使用情况。