在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
的写入已经完成了一半。
答案 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 |
底线,在此特定测试中,原子属性表现最佳。显然,原子属性有很大的局限性,但在你的场景中,听起来这是可以接受的。这些结果显然取决于您的场景的具体情况,听起来您的测试已经确认原子属性为您带来了最佳性能。