【问题标题】:Does @synchronized guarantees for thread safety or not?@synchronized 是否保证线程安全?
【发布时间】:2013-03-01 19:48:30
【问题描述】:

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

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

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

任何 cmets 和/或答案将不胜感激。

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

【问题讨论】:

  • 对于它的价值(你知道我对此的立场),我相信如果你正确使用@synchronized,它可以确保线程安全。当我读到它时,这个答案是说如果你滥用它(例如引用错误的同步令牌),你的代码将不是线程安全的。但我认为几乎所有同步技术都可以这样说,如果你使用不正确,你的代码将不是线程安全的。我认为lawicko的回答在其他方面都很好,但我认为他在@synchronized中夸大了情况。无论如何,有更好的方法来确保线程安全。
  • 我在这里只是想看看,如果一些大佬用一些有效的例子来回答,但是我可能会失去一些观点,但这对我和其他人都有帮助。
  • @synchronize 创建锁。它不会创建线程安全。它是您工具箱中实现线程安全的工具之一。它不是开箱即用的线程安全的原因是您仍然面临问题(包括死锁)。有更好的方法来确保线程安全。你需要它做什么?也许我们可以提供帮助。
  • 是的,@synchronized 不会让你的代码神奇地线程安全。不过,正确使用和实施可以使您的代码线程安全(尽管其他方法通常更可取)。

标签: ios objective-c macos cocoa synchronized


【解决方案1】:

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

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

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

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

因为第一次获得锁时_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 是一个库,用户不太可能知道代码包含一个锁。

【讨论】:

  • this _lockObject = [[NSObject alloc] init] init 函数。这就是你需要做的所有事情吗?或者您是否需要在每个要锁定的方法中使用 _lockObject?
  • 你能再解释一下这个僵局吗?死锁是否总是由线程 1 造成的:Lock(A);锁 (B);和线程 2 做:Lock(B);锁(A)。 @synchronized (self) 与此有什么关系?
  • 块内的[self bar]代码是否也同步了?例如,那个方法在子调用中又调用了 30 多个方法,它们都被认为是在临界区吗?
  • 在该部分中的代码执行的整个过程中都持有锁。
  • 我同意杰的观点。 @synchronized(self) 和死锁有什么关系? @synchronized 使用递归锁。如果某些外部代码在您的 self 对象上使用了 @synchronized,这有什么问题吗?您能否提供一个导致死锁的实际代码示例?谢谢!
【解决方案2】:

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

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

它只保证线程安全,不保证。我的意思是你为你的车雇了一个专业的司机,但它仍然不能保证汽车不会发生事故。但是概率仍然很小。

GCD(grand central dispatch) 的同伴是dispatch_once。 dispatch_once 与@synchronized 做同样的工作。

【讨论】:

  • 答案有点晚了,但是驱动程序示例很不错:) +1
  • “保证”和“保证”的意思完全一样。我想你的意思是说:“它只是帮助你编写线程安全代码的工具,但它并不能保证线程安全。”
  • dispatch_once 肯定不会和@synchronized 做同样的事情。 dispatch_once 只执行代码 ONCE 和 ONCE,因此得名。
【解决方案3】:

我认为问题的本质是:

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

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


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

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

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

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

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

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

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

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

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

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

回到问题:

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

【讨论】:

    【解决方案4】:

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

    互斥锁的副作用:

    1. 死锁
    2. 饥饿

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

    【讨论】:

    • 感谢您的回答,我知道这个问题会对我们中的许多人有所帮助,因为简而言之,我们大多数人都知道@sync 使线程安全,不是吗? :)
    【解决方案5】:

    @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 行的人必须等待他们前面的每个人都完成。

    【讨论】:

    • 这都是很好的信息并且是正确的,但是,我只想补充一点,通常使用 @synchronized 或传统锁不再是 Apple 推荐的线程安全策略。随着争用的增加,排队操作的效率要高得多。
    • 是的,我的意思是在我的示例中理想情况下,您将有一个串行队列来执行所有数据库操作。但显然我想展示 @synchronized 可以派上用场的地方。特别是如果您不知道人们将从哪个线程调用您的课程。
    【解决方案6】:

    @synchronized 本身并不能使代码线程安全,但它是编写线程安全代码时使用的工具之一。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-04-10
      • 1970-01-01
      • 2018-12-07
      • 1970-01-01
      • 2015-03-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多