同步对实例变量的读/写访问以在iOS中实现高性能?

时间:2013-12-31 03:37:02

标签: ios iphone multithreading synchronization

在iOS的objective-c中同步对实例变量的读/写访问的最佳方式/最小等待方式是什么?

变量经常被读取和写入(假设每秒读取和写入1000次)。更改立即生效并不重要。读取与另一个读取数据一致并不重要,但写入必须迟早反映在读取获取的数据中。是否有一些数据结构允许这个?

我想到了这个:

  • 创建两个变量而不是一个变量;我们称他们为v[0]v[1]
  • 对于每个v[i],创建一个并发调度队列,用于构建围绕它的readers-writer-locking机制。我们称他们为q[i]
  • 现在进行书写操作时,只有v[0]被写入,并使用q[0]加入锁定机制。
  • 在读取操作中,首先读取v[1]并且仅在某个机会,例如1%,读取操作会查看v[0]并在必要时更新v[1]

以下伪代码说明了这一点:

typedef int VType; // the type of the variable

VType* v; // array of first and second variable
dispatch_queue_t* q; // queues for synchronizing access to v[i]

- (void) setV:(VType)newV {
    [self setV:newV at:0];
}

- (void) setV:(VType)newV at:(int)i {
    dispatch_barrier_async(q[i], ^{
        v[i] = newV;
    });
}

- (VType) getV:(int)i {
    __block VType result;

    dispatch_sync(q[i], ^{
        result = v[i];
    });

    return result;
}

- (VType) getV {
    VType result = [self getV:1];

    if ([self random] < 0.01) {
        VType v0_result = [self getV:0];

        if (v0_result != result) {
            [self setV:v0_result at:1];
            result = v0_result;
        }
    }

    return result;
}

- (float) random {
    // some random number generator - fast, but not necessarily good
}

这有以下好处:

  • v[0]通常不会被读取操作占用。因此,写操作通常不会阻止。

  • 在大多数情况下,v[1]都没有写入,因此对此操作的读取操作通常不会阻止。

  • 但是,如果发生许多读取操作,最终写入的值会从v[0]传播到v[1]。可能会遗漏一些值,但这对我的申请无关紧要。

你们怎么想?这有用吗?有更好的解决方案吗?

更新

一些性能基准测试(一次读取和写入一个基准测试,尽可能快地完成1秒,一个读取队列,一个写入队列):

在带有iOS 7的iPhone 4S上:

runMissingSyncBenchmark: 484759 w/s
runMissingSyncBenchmark: 489558 r/s
runConcurrentQueueRWSyncBenchmark: 2303 w/s
runConcurrentQueueRWSyncBenchmark: 2303 r/s
runAtomicPropertyBenchmark: 460479 w/s
runAtomicPropertyBenchmark: 462145 r/s

在iOS 7的模拟器中:

runMissingSyncBenchmark: 16303208 w/s
runMissingSyncBenchmark: 12239070 r/s
runConcurrentQueueRWSyncBenchmark: 2616 w/s
runConcurrentQueueRWSyncBenchmark: 2615 r/s
runAtomicPropertyBenchmark: 4212703 w/s
runAtomicPropertyBenchmark: 4300656 r/s

到目前为止,原子财产获胜。极大。这是使用SInt64测试的。

我预计并发队列的方法在性能上与原子属性相似,因为它是r / w-sync机制的标准方法。

当然,runMissingSyncBenchmark有时会产生一些读数,表明SInt64的写入已经完成了一半。

2 个答案:

答案 0 :(得分:2)

也许,spinlock将是最优的(参见man 3 spinlock)。

由于可以测试旋转锁当前是否锁定(这是一个快速操作),如果编写器任务保持旋转锁定,则读取器任务可以返回先前的值。

也就是说,读者任务使用OSSpinLockTry()并仅在可以获得锁定时检索实际值。否则,读取任务将使用先前的值。

编写器任务将分别使用OSSpinLockLock()OSSpinLockUnlock()以自动更新值。

从手册页:

  

NAME        OSSpinLockTry,OSSpinLockLock,OSSpinLockUnlock - 原子自旋锁同步原语

     

概要

 #include <libkern/OSAtomic.h>

 bool
 OSSpinLockTry(OSSpinLock *lock);

 void
 OSSpinLockLock(OSSpinLock *lock);

 void
 OSSpinLockUnlock(OSSpinLock *lock);
     

<强>描述

     

自旋锁是一种简单,快速,线程安全的同步原语,适用于预期争用率较低的情况。自旋锁操作使用内存屏障来同步对由锁保护的共享内存的访问。在锁定时可以进行抢占。

     

OSSpinLock是整数类型。惯例是解锁为零,锁定为非零。锁必须自然对齐,不能在缓存禁止的内存中。

     如果已经锁定,

OSSpinLockLock()将会旋转,但是采用各种策略来退出,使其免受大多数优先级倒置活锁的影响。但由于它可以旋转,在某些情况下可能效率低下。

     如果持有锁,

OSSpinLockTry()会立即返回false,如果锁定,则返回true。它不旋转。

     

OSSpinLockUnlock()通过归零来无条件解锁。

     

返回值

     

OSSpinLockTry()如果锁定则返回true,如果已经锁定则返回false。

答案 1 :(得分:1)

我认为CouchDeveloper建议使用try - 检查同步锁是一种有趣的可能性。在我的特定实验中,它对自旋锁的影响可以忽略不计,pthread读写锁的适度增益,以及简单的互斥锁的最大影响)。我打赌,差异配置也可以通过自旋锁获得一些好处,但是必须让我无法通过自旋锁获得足够的争用,以便使try可以被观察到。

如果您正在使用不可变或基本数据类型,您还可以使用线程编程指南中的Synchronization Tools部分中所述的atomic属性: < / p>

  

原子操作是一种简单的同步形式,适用于简单的数据类型。原子操作的优点是它们不会阻止竞争线程。对于简单的操作,例如递增计数器变量,这可以带来比锁定更好的性能。

没有意识到你已经完成了自己的基准测试,我对该文档中讨论的几种技术进行了基准测试(使用和不使用“try”算法进行互斥锁定和pthread读/写锁定)以及GCD读取器 - 写作模式。在我的测试中,我做了500万次读取,同时进行了500k的随机值写入。这产生了以下基准(以秒为单位测量,越小越好)。

| Tables                    | Simulator | Device   | 
+---------------------------+-----------+----------+
| Atomic                    |       1.9 |      7.2 |
| Spinlock w/o try          |       2.8 |      8.0 |
| Pthread RW lock w/ try    |       2.9 |      9.1 |
| Mutex lock w/ try         |       2.9 |      9.4 |
| GCD reader-writer pattern |       3.2 |      9.1 |
| Pthread RW lock w/o try   |       7.2 |     22.2 |
| NSLock                    |      23.1 |     89.7 |
| Mutex lock w/o try        |      24.2 |     80.2 |
| @synchronized             |      25.2 |     92.0 |

底线,在此特定测试中,原子属性表现最佳。显然,原子属性有很大的局限性,但在你的场景中,听起来这是可以接受的。这些结果显然取决于您的场景的具体情况,听起来您的测试已经确认原子属性为您带来了最佳性能。