【问题标题】:Objective-C Thread Safe CounterObjective-C 线程安全计数器
【发布时间】:2013-06-18 18:26:30
【问题描述】:

我正在尝试以线程安全的方式控制网络活动指示器。

这是我目前正在做的方式,但我认为必须有更好的方式来做到这一点。我一直在寻找使用锁,但这似乎是一项昂贵的操作。我一直在研究 OSAtomicAdd,但不知道在这种情况下如何使用它。

+ (void)start
{
    [self counterChange:1];
}

+ (void)stop
{
    [self counterChange:-1];
}

+ (void)counterChange:(NSUInteger)change
{
    static NSUInteger counter = 0;
    static dispatch_queue_t queue;
    if (!queue) {
        queue = dispatch_queue_create("NetworkActivityIndicator Queue", NULL);
    }
    dispatch_sync(queue, ^{
        if (counter + change <= 0) {
            counter = 0;
            [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
        } else {
            counter += change;
            [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
        }
    });
}

如何使用 OSAtomicAdd 完成这样的事情?

【问题讨论】:

  • 也许你可以使用stackoverflow.com/questions/16420340/…的一些代码。
  • @MartinR 谢谢,您的回答实际上解决了我的问题。我想确保 NumberOfCallsToSetVisible 永远不会变成-1。 NumberOfCallsToSetVisible = 0 线程是安全的还是存在 osatomic 集?
  • 我也喜欢 MartinR 的答案,但如果你想在一般情况下进行序列化,请在类级设置器 +(void)setCounter: 上使用 @synchronize (或等效项)并确保使用增加时设置器。
  • 无论如何,这是一种间接的欺骗。这个问题有两个答案,既解决了线程安全计数器问题,又演示了如何限制并发网络访问,这很关键。

标签: iphone ios objective-c cocoa-touch


【解决方案1】:

您不能仅依靠OSAtomicAdd 之类的东西来同步这种操作。整个操作需要被锁定以确保它成功运行。

考虑this answer 中建议的解决方案,基本上可以归结为:

static volatile int32_t NumberOfCallsToSetVisible = 0;
int32_t newValue = OSAtomicAdd32((setVisible ? +1 : -1), &NumberOfCallsToSetVisible);
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:(newValue > 0)];

如果从一个线程调用此代码,并且将setVisible 设置为YES,则对OSAtomicAdd32 的调用会将NumberOfCallsToSetVisible 加1,从而导致newValue 设置为1。

现在考虑如果该线程在执行下一行之前被抢占,并且另一个线程调用该函数并将setVisible 设置为NO,会发生什么情况。这次对OSAtomicAdd32 的调用将从NumberOfCallsToSetVisible 中减去1,从而将newValue 设置为0。

如果第二个线程继续执行,并执行下一行,newValue 不大于零,因此将使用NO 调用setNetworkActivityIndicatorVisible 方法。此时活动指示器仍然不可见,因此这无济于事,但也不会造成任何伤害。

然而,最终我们将切换回newValue设置为1的第一个线程。所以当该线程执行下一行时,newValue显然大于零,而setNetworkActivityIndicatorVisible方法将使用YES 调用,使活动指示器可见。

所以我们调用了一次函数,将setVisible 设置为YES,并再次将setVisible 设置为NO。您会期望这会导致活动指示器不可见,但事实并非如此。事实上,如果没有其他调用,它将永远保持可见。这显然是不对的。

最重要的是,您需要将整个内容包装在 @synchronize 块或类似内容中。

【讨论】:

  • 你说得对,我的回答并不能可靠地解决问题。
  • 您错过了值不低于零的要求,不是吗? (我不确定您是否需要将您的计数器标记为volatile)。
  • @yonosoytu:函数声明为int32_t OSAtomicAdd32(int32_t __theAmount, volatile int32_t *__theValue);
  • 是的,这意味着在函数内部,给定的指针应被视为volatile int,但您实际上并不需要外部函数中的类型类才能正常工作(IMO)
  • @yonosoytu 看来您完全误解了我的回答。我并没有提议将该代码作为解决方案——我是在指出它为什么行不通。
【解决方案2】:

我建议使用同一系列函数中的OSAtomicCompareAndSwap32,而不是OSAtomicAdd32

+ (void)counterChange:(NSUInteger)change
{
  static int32_t counter = 0;
  int32_t localCounter, newCounter;
  do
  {
    localCounter = counter;
    newCounter = localCounter + change;
    newCounter = newCounter <= 0 ? 0 : newCounter;
  } while (!OSAtomicCompareAndSwap32(localCounter, newCounter, &counter));
  [UIApplication sharedApplication].networkActivityIndicatorVisible = counter > 0;
}

该函数会将localCountercounter 的当前值进行比较,只有当它们匹配时,它才会将counter 更改为newCounter,所有这些都是原子的。如果其他线程在当前线程接受localCounter 和调用OSAtomicCompareAndSwap32 之间更改了counter,则检查将失败,它将重试。

即使看起来它可能会使某些线程永远循环,但这种构造在现实世界条件下是足够安全的。

【讨论】:

  • 想象你有一个对 counterChange:1 的调用,在 localCounter 设置为 0 之后被另一个调用 counterChange:1 的线程抢占。在第二个线程中,counter 变为 1,所以回到第一个线程newCounter 最终将设置为2。如果第一个线程在OSAtomiccompareAndSwap32 调用之前被另一个调用counterChange:-1 的线程再次抢占,counter 将返回0 允许调用成功,但是结果将是 counter 在应该为 1 时更新为 2。这只是我可以想象此代码失败的几种方式之一。
  • 是的,我修复了代码,该行应该是localCounter。关于这可能失败的几种方式之一......好吧,据我了解OSAtomicAdd32 是使用您在代码中看到的东西实现的,除非处理器中有更好的原语。
  • 这是另一个缺陷。假设您调用了counterChange:1,它一直到最后一行,计算counter &gt; 0,但在将networkActivityIndicatorVisible 设置为YES 之前被第二个线程抢占。第二个线程调用counterChange:-1 并完成执行将counter 减回零的函数,因此networkActivityIndicatorVisible 设置为NO。当我们切换回第一个线程时,它将从上次停止的地方继续将networkActivityIndicatorVisible 设置为YES,但这显然不是它应该的样子。
【解决方案3】:

NSLock(适用于 iOS 2.0+ 和 OS X 10.0+)正是您要找的。​​p>

NSLock 对象用于协调同一应用程序中多个执行线程的操作。 NSLock 对象可用于调解对应用程序全局数据的访问或保护代码的关键部分,使其能够以原子方式运行。

您可以在您的应用委托中初始化锁并在计数器代码周围调用-lock-unlock

// Assuming the application delegate implements -counterChangeLock
// to return a momoized instance of NSLock

+ (NSLock *)counterChangeLock
{
    return [(AppDelegate *)([UIApplication sharedApplication].delegate) counterChangeLock];
}

+ (void)start
{
    [[self counterChangeLock] lock]; // blocks if counter is locked already

    // safely increment counter

    [[self counterChangeLock] unlock];
}

+ (void)stop
{

    [[self counterChangeLock] lock];

    // safely decrement counter

    [[self counterChangeLock] unlock];
}

【讨论】:

    【解决方案4】:

    我不确定为什么要使用所有这些原子操作,因为它使问题复杂化并且没有解决在告诉 UIApplication 我们需要的数字时我们需要修复线程同步的事实。

    使用@synchronized 的建议是正确的解决方案,因为它为您提供了一个围绕递增和调用 UIApplication 的互斥锁。如果您对 @synchronized 进行基准测试,您会发现它的速度非常快,而且这种事情非常罕见,以至于原子变量 & 比较和交换容易出错且不必要。不这样做的唯一原因是,如果 (self) 在许多其他部分同步,在这种情况下,您可以为此目的保留一个 NSObject 或使用 NSLock 和等效项。

    因此:

    + (void) incrementActivityCounter {
        [self changeActivityCounter:1];
    }
    
    + (void) decrementActivityCounter {
        [self changeActivityCounter:-1];
    }
    
    + (void) changeActivityCounter:(int)change {
        static int counter = 0;
        @synchronized(self) {
          counter += change;
          [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:counter > 0];
        }
    }
    

    【讨论】:

      【解决方案5】:

      UIApplication 的 networkActivityIndi​​catorVisible 是一个非原子属性,因此只能在主线程中使用。因此不需要同步计数器,因为它不应该从线程中调用。只需要一个简单的静态 int 并在开始时递增和在停止时递减。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-06-06
        • 1970-01-01
        • 1970-01-01
        • 2015-07-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多