【问题标题】:Synchronizing read/write access to an instance variable for high performance in iOS?在 iOS 中同步对实例变量的读/写访问以实现高性能?
【发布时间】:2013-12-31 03:37:02
【问题描述】:

在 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 的写入已完成一半。

【问题讨论】:

  • 您是否将其与简单队列读写器模式进行了基准测试(如 WWDC 2012 视频Asynchronous Design Patterns with Blocks, GCD, and XPC 中所述,即相同的并发队列、同步读取、异步屏障写入,就像您在上面所做的那样,但是没有单独的 v[0] 和 v[1] 逻辑)?基于 GCD 的读写器模式非常高效(通常比锁更高效),感觉上面的代码示例效率低于简单的读写器模式。
  • @Rob,我还没有比较基准测试结果。我只用一个并发队列描述了标准的读写器模式。 Instruments 显示,由于等待,阅读和写作所花费的时间平均增加了一倍。我正在尝试找到一种方法来优化它。
  • 好的。正如 CouchDeveloper 建议的那样,您的读者可以使用“尝试”锁定机制(使用自旋锁或读/写互斥锁)。但是,如果对象是不可变的(或者在您的示例中是基本数据类型),您不能只使用 atomic 属性吗?在那些你可以摆脱atomic 属性的简单情况下,它通常比锁和读写 GCD 模式快得多。
  • @Rob,我可能会按照你的建议使用原子属性,因为它非常简单,并且比 concurrent_queue-dispatch_sync-dispatch_barrier_sync 方法快 2000 倍 (LOL)。
  • 您的 2000x 评论促使我进行了一些基准测试,我得到了一些完全不同的结果(原子操作仅比 GCD 读写器模式快 20-40%,而不是快三个数量级)。最重要的是,atomic 比 GCD、自旋锁或使用 try 算法的互斥锁快一点。并且这三种同步方式比 pthread 读写锁快 2-3 倍,而 pthread 本身比简单的 mutex/NSLock/@synchronized 锁快 3-4 倍。所以,虽然我的原子性能没有你描述的 2000 倍增益,但它绝对是最快的。

标签: ios iphone multithreading synchronization


【解决方案1】:

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

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

也就是说,读取器任务使用OSSpinLockTry(),并且只有在可以获得锁的情况下才检索实际值。否则,读取任务将使用之前的值。

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

来自手册页:

名称 OSSpinLockTry、OSSpinLockLock、OSSpinLockUnlock——原子自旋锁同步原语

概要

 #include <libkern/OSAtomic.h>

 bool
 OSSpinLockTry(OSSpinLock *lock);

 void
 OSSpinLockLock(OSSpinLock *lock);

 void
 OSSpinLockUnlock(OSSpinLock *lock);

描述

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

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

OSSpinLockLock() 将在锁已被持有时自旋,但会采用各种策略来回退,使其免受大多数优先级反转活锁的影响。但因为它可以旋转,所以在某些情况下可能效率低下。

OSSpinLockTry() 如果持有锁,则立即返回 false,如果获得锁则返回 true。它不旋转。

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

返回值

OSSpinLockTry() 如果获取了锁则返回 true,如果锁已被持有则返回 false。

【讨论】:

【解决方案2】:

我认为 CouchDeveloper 建议在同步锁中使用try-checks 是一种有趣的可能性。在我的特定实验中,它对自旋锁的影响可以忽略不计,对 pthread 读写锁的影响不大,而对简单互斥锁的影响最大)。我敢打赌,不同的配置也可以通过自旋锁获得一些收益,但一定是我未能获得足够的自旋锁争用,才能观察到使用try 的影响。

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

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

不知道您已经完成了自己的基准测试,我对该文档中讨论的一些技术进行了基准测试(在使用和不使用“try”算法的情况下进行互斥锁和 pthread 读/写锁),以及 GCD 阅读器-作家模式。在我的测试中,我读取了 5m 次,同时写入了 500k 次随机值。这产生了以下基准(以秒为单位,越小越好)。

|表 |模拟器 |设备 | +---------------+------------+--------- -+ |原子| 1.9 | 7.2 | |无需尝试的自旋锁 | 2.8 | 8.0 | |带尝试的 Pthread RW 锁 | 2.9 | 9.1 | |带尝试的互斥锁 | 2.9 | 9.4 | | GCD读写器模式| 3.2 | 9.1 | | Pthread RW lock w/o try | 7.2 | 22.2 | | NSLock | 23.1 | 89.7 | |互斥锁,无需尝试 | 24.2 | 80.2 | | @同步 | 25.2 | 92.0 |

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

【讨论】:

猜你喜欢
  • 2019-02-27
  • 2020-06-25
  • 1970-01-01
  • 2013-07-08
  • 1970-01-01
  • 2018-07-27
  • 1970-01-01
  • 1970-01-01
  • 2018-11-15
相关资源
最近更新 更多