33

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

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

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

对此的任何评论和/或答案将不胜感激。

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

4

6 回答 6

41

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

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

像这样想。您有一大群人在不同的队伍中等待,每条线路的编号为 1-10。您可以选择希望每个人在哪一行等待(通过按每行同步),或者如果您不使用@synchronized,您可以直接跳到前面并跳过所有行。第 1 行的人不必等待第 2 行的人完成,但第 1 行的人必须等待他们前面的每个人完成。

于 2013-03-13T18:10:17.713 回答
22

我认为问题的本质是:

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

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


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

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

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

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

  • 任何操作顺序都不能违反规范。1
  • 在多线程执行期间,不变量和条件将保持不变,而无需客户端2进行额外的同步。

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

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

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

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

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

回到问题:

正确使用@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 不可变共享状态或可变非共享对象始终是线程安全的。

于 2013-03-13T18:32:45.373 回答
11

通常,@synchronized保证线程安全,但仅在正确使用时。递归获取锁也是安全的,尽管我在此处的回答中详细说明了一些限制。

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

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

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

因为在第一次获得锁时将为 nil,所以不会发生锁定,并且多个线程可能会在第一次完成之前_foo创建自己的。_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是库,用户不太可能知道代码包含锁。

于 2013-10-17T16:06:41.097 回答
4

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

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

于 2013-03-13T18:10:43.110 回答
3

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

@synchronize隐式应用锁而NSLock显式应用它。

它只保证线程安全,而不是保证。我的意思是您为您的汽车聘请了一位专业的司机,但这并不能保证汽车不会发生事故。然而,概率仍然是微乎其微的。

GCD它在(大中央调度)中的同伴是dispatch_once。dispatch_once 与 to 做同样的工作@synchronized

于 2015-01-08T09:08:30.757 回答
1

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

互斥锁的副作用:

  1. 死锁
  2. 饥饿

线程安全将取决于@synchronized块的使用。

于 2013-03-14T07:06:28.807 回答