Objective-C线程安全计数器

时间:2013-06-18 18:26:31

标签: iphone ios objective-c cocoa-touch

我试图以线程安全的方式控制网络活动指标。

这是我目前正在做的方式,但我认为必须有更好的方法来做到这一点。我正在寻找使用锁,但它似乎是一个昂贵的操作。我一直在关注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完成这样的事情?

5 个答案:

答案 0 :(得分:3)

您无法单独使用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,从而导致{{1}被设置为1。

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

如果第二个线程继续,并且下一行被执行,newValue不会大于零,那么将使用newValue调用setNetworkActivityIndicatorVisible方法。此时,活动指示器无论如何都不可见,所以这没有任何作用,但它也没有造成任何伤害。

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

因此,我们在将YES设置为setVisible时将该函数调用一次,并将YES设置为setVisible再次调用该函数。您可能会认为这会导致活动指示器不可见,但事实并非如此。事实上,如果没有其他电话,它将永远保持可见。这显然是不对的。

最重要的是,您需要将整个事物包裹在NO块或类似的内容中。

答案 1 :(得分:1)

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

+ (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,所有内容都会自动更改。如果某个其他线程在当前线程counterlocalCounter调用之间发生OSAtomicCompareAndSwap32更改,则检查将失败,并且将重试。

即使看起来它可能会永远留下一些线程循环,但这种结构在现实条件下足够安全。

答案 2 :(得分:0)

NSLock(适用于iOS 2.0+和OS X 10.0+)是您正在寻找的。

  

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];
}

答案 3 :(得分:0)

我不确定为什么所有这些原子操作在使问题复杂化时都会被使用,并且在解释UIApplication时我们需要修复线程同步这一事实并不能解决问题。

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

因此:

+ (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];
    }
}

答案 4 :(得分:0)

UIApplication的networkActivityIndi​​catorVisible是一个非原子属性,所以它只能在主线程中使用。因此,不需要同步计数器,因为它永远不应该从线程调用。

是一个简单的静态int和停止时开始和减少的增量。