2013-06-18 73 views
2

我试图以线程安全的方式控制网络活动指示器。Objective-C线程安全计数器

以下是我现在正在做的方式,但我认为必须有更好的方法来做到这一点。我正在寻找使用锁,但它看起来像一个昂贵的操作。我一直在看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?

+1

也许你可以使用http://stackoverflow.com/questions/16420340/fixing-my-network-activity-indicator/16420875#16420875一些代码。 –

+0

@MartinR谢谢,你的回答实际上解决了我的问题。我想确保NumberOfCallsToSetVisible永远不会变成-1。 NumberOfCallsToSetVisible = 0线程安全还是有一个osatomic集? –

+0

我也喜欢MartinR的答案,但是如果您想要序列化,请在类级setter +(void)setCounter上使用@synchronize(或等价物):并确保在增量时使用setter。 – danh

回答

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是要加1 NumberOfCallsToSetVisible导致newValue被设置为1

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

如果此第二线程继续,并且执行下一行,newValue是不大于零,所以setNetworkActivityIndicatorVisible方法将请拨打NO。在这一点上,活动指标无论如何都是不可见的,所以这没有任何作用,但它也没有任何伤害。

然而,最终我们要切换回哪里newValue设置为1的第一个线程所以当线程执行的下一行,newValue明显大于零,且setNetworkActivityIndicatorVisible方法将YES被称为,使活动指示符可见。

因此,我们已经调用的函数一旦与setVisible设置为YES,最后再用setVisible设置为NO。你会期望这会导致活动指标不可见,但这并非如此。事实上,如果没有其他呼叫,它将永远保持可见。这显然是不正确的。

底线是你需要将整个东西包装在@synchronize块或类似的东西。

+0

你说得对,我的回答并不能可靠地解决问题。 –

+0

您错过了值不低于零的要求,是吗? (我不确定你需要把你的计数器标记为'volatile')。 – yonosoytu

+0

@yonosoytu:该函数被声明为int32_t OSAtomicAdd32(int32_t __theAmount,volatile int32_t * __ theValue);' –

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

功能将比较localCounter反对counter当前值,且仅当它们匹配它将改变counternewCounter,它的所有原子。如果其他线程更改counter在当前线程需要localCounter和拨打OSAtomicCompareAndSwap32之间,检查将失败,并且它将重试。

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

+0

想象一下,当'localCounter'被设置为0后,调用'counterChange:1'被另一个调用'counterChange:1'的线程抢占。在第二个线程中'counter'变为1,所以回到第一个线程'newCounter'最终会被设置为2.如果第一个线程在'OSAtomiccompareAndSwap32'调用之前被另一个调用'counterChange:-1'的线程再次抢占,'counter'将返回到0以允许调用成功,但是结果会是'counter'在它应该是1时更新为2.这只是我可以想象这种代码失败的几种方法之一。 –

+0

是的,我修正了代码,该行应该读取'localCounter'。关于几种方法之一,这可能会失败......就我所知,'OSAtomicAdd32'是用你在代码中看到的东西来实现的,除非在处理器中有更好的基元。 – yonosoytu

+0

然后,这是另一个缺陷。假设你有一个调用'counterChange:1'的函数,它最后一行计算'counter> 0',但是在将'networkActivityIndi​​catorVisible'设置为'YES'之前被第二个线程抢占。第二个线程调用'counterChange:-1'并结束执行减少了'counter'的函数返回到零,所以'networkActivityIndi​​catorVisible'设置为'NO'。当我们切换回第一个线程时,它将继续停止将“networkActivityIndi​​catorVisible”设置为“YES”,但这显然不是它应该的。 –

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

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

使用@synchronized的建议是正确的解决方案,因为它为您提供了一个增量和调用UIApplication的互斥体。如果基准@synchronized,你会看到它的速度惊人的快,这种事情是罕见的,原子变量&比较和交换是容易出错和不必要的。不这样做的唯一原因是如果(自我)在许多其他部分同步,在这种情况下,您可以为此保留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]; 
    } 
} 
0

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