【问题标题】:Failing to see how @synchronized works with asynchronous blocks看不到 @synchronized 如何与异步块一起工作
【发布时间】:2026-01-06 01:05:02
【问题描述】:

假设我有一个类似下面的函数,每秒调用 30 次(这段代码是我试图理解的由 Apple 创建的更复杂代码的简化。Apple 原始代码的想法是这样的:就像我说的 doSomethingWithImage 每秒调用 30 次。所以,整个方法有 1/30 秒的时间来做所有事情,但想象一下 doSomethingThatWillTakeTime 花费的时间比预期的要多,doSomethingWithImage 被再次调用,而 doSomethingThatWillTakeTime 是已经被执行。如果是这种情况,代码必须删除该图像并且什么都不做。

// code...
@synchronized (self);
  [self doSomethingWithImage:image];
}

// ...

// call under @synchronized( self )
- (void)doSomethingWithImage:(UIImage *)image
{
    self.myImage = image;

    [self executeBlockAsync:^{ 

        UIImage *myImage = nil;

        @synchronized( self ) 
        {
            myImage = self.myImage; 
            if (myImage) { 
                self.myImage = nil; // drop the image
            }
        }

        if ( myImage ) { 
            [self doSomethingThatWillTakeTime:myImage];  
        }
    }];
}


 (void)executeBlockAsync:(dispatch_block_t)block
{
    dispatch_async( _myQueue, ^{
            callbackBlock();
    } );
}

我的问题是看这段代码我看不出它是如何做到的。我不知道正在执行哪些行以使其发生。可能我不明白@synchronized 在这方面的作用,究竟是什么被锁定。

【问题讨论】:

  • 您可以查看the code here。见方法captureOutput: didOutputSampleBuffer:fromConnection:

标签: ios objective-c asynchronous objective-c-blocks


【解决方案1】:

@synchronized 唯一要做的就是确保“线程安全”,与一些对象或变量同步交互。具体来说,它确保您不会从一个线程访问这些资源,而它们可能会在另一个线程的其他地方发生变异。如果一个线程在 @synchronized(self) {...} 块内,而另一个线程遇到了自己的 @synchronized(self) {...} 块,则后面的代码在另一个线程完成其 @synchronized 块之前不会开始运行。

这只是Threading Programming Guide: Synchronization 中列出的众多同步技术之一。 (或者您也可以使用专用的 GCD 队列(串行队列或使用读写器模式的并发队列)来管理 Concurrency Programming Guide: Migrating Away from Threads: Eliminating Lock-based Code 中所述的同步。但这完全是一个单独的主题。)

在您shared with uscaptureOutput:didOutputSampleBuffer:fromConnection: 方法中,他们只是使用@synchronized 来确保_recordingStatus_recorder 在您从另一个线程中使用它们时不会在一个线程中更改线。他们通过确保与这些变量的所有交互(无论是在一个线程中读取还是在另一个线程中更新)都发生在他们自己的 @synchronized { ... } 块中来实现这一点。

但请注意,@synchronized 不提供线程安全交互之外的任何功能用途。所有关于“我是否应该添加框架”的逻辑都由该代码中简单的if 语句决定。 @synchronized 指令与此无关。它们只是为了确保线程安全。

【讨论】:

    【解决方案2】:

    这并不是代码“丢弃图像并且什么都不做”。使用“drop”这个词可能有点误导。如果有新图像,则会覆盖尚未交付给代理的图像。

    发生的事情是这样的:

    • 图像 1 存储在self.myImage
    • 一个异步任务被排入串行调度队列以处理映像 1。
    • 当这个任务执行时,它会获取当前的self.myImage,然后清除self.myImage
    • 如果拾取了非零图像,则将其传递给doSomethingThatWillTakeTime

    稍后,

    • 图片2存储在self.myImage
    • 一个异步任务被排入串行调度队列以处理图像 2
    • 由于是串行调度队列,如果第一个任务没有完成,这个任务会等待。

    现在说,

    • 图像 3 存储在 self.myImage 中 - 请注意,这不会检查 self.myImage 是否为 nil,它只是覆盖它,因此如果图像 2 从未被处理过,则它已被“丢弃”
    • 一个异步任务排入串行调度队列以处理图像 3,但它也必须等待
    • 现在,当第一个任务完成时,将执行第二个任务。它将从 self.myImage 获取值,现在是图像 3 并清除 self.myImage
    • 图像 3 被传递给doSomethingThatWillTakeTime
    • 第二个任务完成后,第三个任务将开始。
    • 它将从self.myImage 得到nil 什么都不做。

    所以你可以看到图像 2 是如何被丢弃的,因为它已经过时了;图 3 已经到了,所以处理图 2 没有意义。

    基本上,这段代码实现了一个大小为 1 的circular buffer

    【讨论】:

      【解决方案3】:

      关键是executeBlockAsync,它又调用dispatch_async。因此,真正的工作被分派到后台队列,该队列很可能配置为按顺序工作。 doSomethingWithImage 立即返回,永远不会占用 1/30s。

      所以调度保证图像处理是串行完成的。只要图像处理的平均时间不超过 1/30 秒,这种方法就可以工作。单个图像可能需要更长的时间。如果持续时间更长,队列将被填满。

      我不明白@synchronized 在这段代码中做了什么。仅当有更多代码(此处未显示)使用相同的 @synchronized 块时才有意义。

      【讨论】:

      • 您可以查看the code here。见方法captureOutput: didOutputSampleBuffer:fromConnection:
      • 是的,队列配置为串行工作。是的,还有更多 @synchronized( self ) 块。我不完全理解代码流,但似乎它们不是来自 doSomethingWithImage: 而是来自其他事件。
      • @synchronised 确保对self.myImage 的访问(设置或获取)一次仅发生在单个线程上。如果一个线程已经拥有self 上的锁,那么另一个线程将等待直到锁可用
      • @Paulw11 - 好的,但我不明白如何再次调用doSomethingWithImage 不会执行并删除图像。换句话说,我看不到第二次调用doSomethingWithImage 是如何识别出另一个仍在运行的。-
      • 图像在对doSomethingWithImage的调用中没有被处理。它只包含它的代码。但是,^{ ... } 包围的代码块在后台队列中异步执行。并且该队列被配置为串行工作。因此可以确保它不会同时处理两个图像。