Skip to content

线程同步之读写锁

pro648 edited this page May 18, 2020 · 1 revision

mind

这是并发控制方案的系列文章,介绍了各种锁的使用及优缺点。

  1. 自旋锁
  2. os_unfair_lock
  3. 互斥锁
  4. 递归锁
  5. 条件锁
  6. 读写锁
  7. @synchronized

OSSpinLock、os_unfair_lock、pthread_mutex_t、pthread_cond_t、pthread_rwlock_t 是值类型,不是引用类型。这意味着使用 = 会进行复制,使用复制的可能导致闪退。pthread 函数认为其一直处于初始化的内存地址,将其移动到其他内存地址会产生问题。使用copy的OSSpinLock不会崩溃,但会得到一个全新的锁。

如果你对线程、进程、串行、并发、并行、锁等概念还不了解,建议先查看以下文章:

读写锁(readers-writer)是计算机程序并发控制的一种同步机制,用于解决读写问题。

当多个线程并行访问共享资源时,有些线程执行读操作、有些线程执行写操作,这时会出现读写问题。多个线程同时读共享资源不会出现问题,但有线程写时其他线程必须等待,否则会损坏数据。

读写锁允许并行读、串行写。与互斥锁的一次只有一个线程执行操作相比,性能更高。比如构建缓存系统,将网络资源写入缓存,后期从缓存读取资源。缓存系统必须线程安全,允许并行读取,串行写入。

这篇文章将介绍pthread_rwlock_t和 dispatch barrier 两种实现读写锁的方案。

1. pthread_rwlock_t

1.1 初始化 pthread_rwlock_init()

pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr)函数使用 attr 属性初始化 rwlock 读写锁。如果 attr 为 nil,则使用默认属性初始化读写锁。

初始化后 rwlock 可以使用任意次数。初始化已经初始化的 rwlock,会产生无法预期的后果。使用未初始化的 rwlock,会产生无法预期的后果。

pthread_rwlock_t初始化方法如下:

    private var rwLock = pthread_rwlock_t()
    
    override init() {
        pthread_rwlock_init(&rwLock, nil)
        
        super.init()
    }

1.2 读锁 pthread_rwlock_rdlock()

pthread_rwlock_rdlock()函数为读写锁添加读锁。如果没有写锁已经加锁、队列中也没有写锁,该函数为读写锁添加读锁。如果读写锁未被写锁加锁,且队列中有写锁时,无法确定pthread_rwlock_rdlock()是否会加锁。如果写锁已经加锁,则pthread_rwlock_rdlock()函数不会捕获读写锁。如果读锁未能捕获读写锁,则线程会堵塞在pthread_rwlock_rdlock(),直到读锁加锁成功。如果当前线程已经添加了写锁,再次调用pthread_rwlock_rdlock()函数结果将无法定义。

一个线程可以同时多次持有读锁,解锁时需调用对应次数的pthread_rwlock_unlock()

        // 添加读锁
        pthread_rwlock_rdlock(&rwLock)

pthread_rwlock_tryrdlock()函数像pthread_rwlock_rdlock()函数一样添加读锁,但当有写锁持有、等待读写锁时,则会直接失败。

1.3 写锁 pthread_rwlock_wrlock()

pthread_rwlock_wrlock()函数将读写锁锁定为写状态。如果当前没有线程(包括读锁、写锁)持有读写锁,写锁加锁成功。否则,会堵塞线程直到成功。如果当前线程(包括读锁、写锁)已经持有读写锁,再次调用写锁会导致无法预期的结果。

        // 添加写锁
        pthread_rwlock_wrlock(&rwLock)

pthread_rwlock_trywilock()函数与pthread_rwlock_wrlock()相似,但当有线程(包括读锁、写锁)持有读写锁时,pthread_rwlock_trywilock()函数会立即返回错误。

1.4 解锁 pthread_rwlock_unlock()

pthread_rwlock_unlock()函数用于解锁读写锁。如果线程没有持有读写锁,调用pthread_rwlock_unlock()会产生无法预期的错误。

调用pthread_rwlock_unlock()函数解锁读锁后,如果还有其他读锁持有读写锁,则读写锁仍处于读状态。如果pthread_rwlock_unlock()函数释放了当前线程的最后一个读锁,则当前线程不再是该读写锁的持有者。如果pthread_rwlock_unlock()释放了最后一个读锁,则读写锁进入未加锁状态。

调用pthread_rwlock_unlock()函数解锁写锁后,读写锁进入未加锁状态。

如果调用pthread_rwlock_unlock()函数后锁进入未加锁状态,同时有多个线程等待添加写锁,调度策略决定哪个线程获取写锁。如果有多个线程等待添加读锁,调度策略决定线程进入顺序。如果多个线程等待添加读锁、写锁,则无法确定是读锁还是写锁先加锁。

        pthread_rwlock_unlock(&rwLock)

1.5 销毁 pthread_rwlock_destroy()

pthread_rwlock_destroy()函数销毁读写锁,及读写锁使用的资源。销毁后可以使用pthread_rwlock_init()再次初始化锁,任何其他方式使用已销毁的锁都会产生无法预期的后果。

当读写锁处于加锁状态时,调用pthread_rwlock_destroy()会产生无法预期的后果。销毁未初始化的锁,会产生无法预期的后果。

        pthread_rwlock_destroy(&rwLock)

2. Dispatch Barrier

GCD 提供、管理执行任务的队列。这种抽象隐藏了线程管理,开发者只需专注于构建任务序列。

在保护临界区域时,GCD 提供了 dispatch barrier。当执行 barrier 任务时,队列中所有其他任务都会等待。没有执行 barrier 任务时,其他任务并行执行。

如果你对 GCD 不了解,可以查看我的另一篇文章:Grand Central Dispatch的使用

2.1 并发队列

Dispatch barrier 必须用在自定义并发队列中。如果用在全局队列中,将起不到屏障作用。串行队列一次执行一个任务,无需使用 barrier。

创建并发队列方法如下:

    private var barrierQueue = DispatchQueue.init(label: "BarrierQueue", qos: .utility)

2.2 写锁

Dispatch barrier 使用dispatch_barrier_asyncdispatch_barrier_sync函数控制写操作。上述函数和dispatch_syncdispatch_async类似,独特之处在于用在自定义并发队列时,使用 barrier 函数提交的任务串行执行,并且会等待当前执行中的任务执行完毕才开始。当 barrier 任务执行完毕后,恢复并行执行。

串行序列没有必要使用 barrier 函数,因为串行序列本身一次只会执行一个任务。由于系统也在使用全局队列,barrier 会堵塞其他任务执行,因此在全局队列中使用 barrier 将起不到屏障作用。此时,效果完全等价于dispatch_syncdispatch_async

        barrierQueue.async(flags: .barrier) {
            sleep(1)
            print("write")
        }

使用 barrier 后可以确保只有一个线程进入临界区域,确保线程安全。

2.3 读锁

使用sync函数直接读锁即可:

        barrierQueue.async {
            sleep(1)
            print("read")
        }

点击这里可以查看关于 barrier 更详细介绍。

Demo名称:Synchronization
源码地址:https://github.com/pro648/BasicDemos-iOS/tree/master/Synchronization

上一篇:线程同步之条件锁

下一篇:线程同步之@synchronized

参考资料:

  1. pthread_rwlock_init, pthread_rwlock_destroy
  2. pthread_rwlock_rdlock, pthread_rwlock_tryrdlock
  3. pthread_rwlock_wrlock, pthread_rwlock_trywrlock
  4. 读写线程安全
  5. What's New in GCD
Clone this wiki locally