从 Objective-C 块中修改存储在实例变量中的信号量

Modifying a semaphore stored in an instance variable from within an Objective-C block

Apple 提供了一个 CPU and GPU Synchronization 示例项目,展示了如何在 CPU 和 GPU 之间同步对共享资源的访问。为此,它使用存储在实例变量中的信号量:

@implementation AAPLRenderer
{
  dispatch_semaphore_t _inFlightSemaphore;
  // other ivars
}

然后在另一个方法中定义此信号量:

- (nonnull instancetype)initWithMetalKitView:(nonnull MTKView *)mtkView
{
    self = [super init];
    if(self)
    {
        _device = mtkView.device;

        _inFlightSemaphore = dispatch_semaphore_create(MaxBuffersInFlight);

        // further initializations
    }

    return self;
}

MaxBuffersInFlight定义如下:

// The max number of command buffers in flight
static const NSUInteger MaxBuffersInFlight = 3;

最后,信号量的使用如下:

/// Called whenever the view needs to render
- (void)drawInMTKView:(nonnull MTKView *)view
{
    // Wait to ensure only MaxBuffersInFlight number of frames are getting processed
    //   by any stage in the Metal pipeline (App, Metal, Drivers, GPU, etc)
    dispatch_semaphore_wait(_inFlightSemaphore, DISPATCH_TIME_FOREVER);

    // Iterate through our Metal buffers, and cycle back to the first when we've written to MaxBuffersInFlight
    _currentBuffer = (_currentBuffer + 1) % MaxBuffersInFlight;

    // Update data in our buffers
    [self updateState];

    // Create a new command buffer for each render pass to the current drawable
    id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
    commandBuffer.label = @"MyCommand";

    // Add completion hander which signals _inFlightSemaphore when Metal and the GPU has fully
    //   finished processing the commands we're encoding this frame.  This indicates when the
    //   dynamic buffers filled with our vertices, that we're writing to this frame, will no longer
    //   be needed by Metal and the GPU, meaning we can overwrite the buffer contents without
    //   corrupting the rendering.
    __block dispatch_semaphore_t block_sema = _inFlightSemaphore;
    [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer)
    {
        dispatch_semaphore_signal(block_sema);
    }];

    // rest of the method
}

我在这里没能理解的是这条线的必要性

__block dispatch_semaphore_t block_sema = _inFlightSemaphore;

为什么要把实例变量复制到一个局部变量中,并用__block标记这个局部变量。如果我只是删除那个局部变量而不是写

[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer)
{
    dispatch_semaphore_signal(_inFlightSemaphore);
}];

似乎 也可以。我还尝试用 __block 标记实例变量,如下所示:

__block dispatch_semaphore_t _bufferAccessSemaphore;

这用 Clang 编译,似乎 也能正常工作。但是因为这是为了防止竞争条件,所以我想确定它有效。

所以问题是为什么 Apple 会创建标有 __block 的本地信号量副本?是否真的有必要,或者直接访问实例变量的方法是否同样有效?

作为旁注,this SO 问题的答案表明无法使用 __block 标记实例变量。答案是根据 gcc,但如果不应该这样做,为什么 Clang 会允许这样做?

这里重要的语义区别是当你直接在块中使用ivar时,块会强引用self。通过创建一个引用信号量的局部变量,只有信号量被块捕获(通过引用),而不是 self,减少了保留循环的可能性。

至于 __block 限定符,您通常会使用它来指示局部变量在引用块中应该是可变的。但是,由于信号量 变量 不会因对 signal 的调用而发生变化,因此限定符在这里并不是绝对必要的。不过,从样式的角度来看,它仍然有用,因为它强调了变量的生命周期和用途。

关于为什么一个ivar可以被限定__block,

why would Clang allow this if it shouldn't be done?

也许正是因为在块中捕获一个 ivar 意味着强烈捕获 self。有或没有 __block 限定符,如果您在块中使用 ivar,则可能 运行 存在保留循环的风险,因此使用限定符不会产生额外的风险。最好使用局部变量(顺便说一句,可以是 __weakself 的引用,就像对 ivar 的 __block 限定引用一样容易)是明确和安全的.

我认为 warrenm 是正确的 为什么要使用局部变量而不是 ivar(及其对 self 的隐式引用)。 +1

但是您询问了为什么在这种情况下局部变量会标记为 __block。作者本可以这样做以使 his/her 意图明确(例如,指示变量将超过方法的范围)。或者他们可能为了效率而这样做(例如,为什么要制作指针的新副本?)。

例如,考虑:

- (void)foo {
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(4);
    NSLog(@"foo: %p %@", &semaphore, semaphore);

    for (int i = 0; i < 10; i++) {
        dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"bar: %p %@", &semaphore, semaphore);
            [NSThread sleepForTimeInterval:1];
            dispatch_semaphore_signal(semaphore);
        });
    }
}

虽然这些都使用相同的信号量,但在没有 __block 的情况下,每个分派的块将获得自己的指向该信号量的指针。

但是,如果您将 __block 添加到该局部 semaphore 变量的声明中,并且每个分派的块将使用将位于堆上的相同指针(即 &semaphore每个块都是一样的)。

这里没有意义,恕我直言,这里只有一个块被调度,但希望这能说明 __block 限定符对本地变量的影响。 (显然,__block 限定符的传统用法是在更改相关对象的值时使用,但这与此处无关。)

... marking instance variables with __block can't be done [in gcc] ... but why would Clang allow this if it shouldn't be done?

关于为什么 __block for ivars 没有错误,正如引用的答案所说,它基本上是多余的。我不确定仅仅因为某些东西是多余的就应该被禁止。