-
Notifications
You must be signed in to change notification settings - Fork 196
线程同步之读写锁
这是并发控制方案的系列文章,介绍了各种锁的使用及优缺点。
OSSpinLock、os_unfair_lock、pthread_mutex_t、pthread_cond_t、pthread_rwlock_t 是值类型,不是引用类型。这意味着使用 = 会进行复制,使用复制的可能导致闪退。pthread 函数认为其一直处于初始化的内存地址,将其移动到其他内存地址会产生问题。使用copy的OSSpinLock不会崩溃,但会得到一个全新的锁。
如果你对线程、进程、串行、并发、并行、锁等概念还不了解,建议先查看以下文章:
读写锁(readers-writer)是计算机程序并发控制的一种同步机制,用于解决读写问题。
当多个线程并行访问共享资源时,有些线程执行读操作、有些线程执行写操作,这时会出现读写问题。多个线程同时读共享资源不会出现问题,但有线程写时其他线程必须等待,否则会损坏数据。
读写锁允许并行读、串行写。与互斥锁的一次只有一个线程执行操作相比,性能更高。比如构建缓存系统,将网络资源写入缓存,后期从缓存读取资源。缓存系统必须线程安全,允许并行读取,串行写入。
这篇文章将介绍pthread_rwlock_t
和 dispatch barrier 两种实现读写锁的方案。
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()
}
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()
函数一样添加读锁,但当有写锁持有、等待读写锁时,则会直接失败。
pthread_rwlock_wrlock()
函数将读写锁锁定为写状态。如果当前没有线程(包括读锁、写锁)持有读写锁,写锁加锁成功。否则,会堵塞线程直到成功。如果当前线程(包括读锁、写锁)已经持有读写锁,再次调用写锁会导致无法预期的结果。
// 添加写锁
pthread_rwlock_wrlock(&rwLock)
pthread_rwlock_trywilock()
函数与pthread_rwlock_wrlock()
相似,但当有线程(包括读锁、写锁)持有读写锁时,pthread_rwlock_trywilock()
函数会立即返回错误。
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)
pthread_rwlock_destroy()
函数销毁读写锁,及读写锁使用的资源。销毁后可以使用pthread_rwlock_init()
再次初始化锁,任何其他方式使用已销毁的锁都会产生无法预期的后果。
当读写锁处于加锁状态时,调用pthread_rwlock_destroy()
会产生无法预期的后果。销毁未初始化的锁,会产生无法预期的后果。
pthread_rwlock_destroy(&rwLock)
GCD 提供、管理执行任务的队列。这种抽象隐藏了线程管理,开发者只需专注于构建任务序列。
在保护临界区域时,GCD 提供了 dispatch barrier。当执行 barrier 任务时,队列中所有其他任务都会等待。没有执行 barrier 任务时,其他任务并行执行。
如果你对 GCD 不了解,可以查看我的另一篇文章:Grand Central Dispatch的使用。
Dispatch barrier 必须用在自定义并发队列中。如果用在全局队列中,将起不到屏障作用。串行队列一次执行一个任务,无需使用 barrier。
创建并发队列方法如下:
private var barrierQueue = DispatchQueue.init(label: "BarrierQueue", qos: .utility)
Dispatch barrier 使用dispatch_barrier_async
和dispatch_barrier_sync
函数控制写操作。上述函数和dispatch_sync
和dispatch_async
类似,独特之处在于用在自定义并发队列时,使用 barrier 函数提交的任务串行执行,并且会等待当前执行中的任务执行完毕才开始。当 barrier 任务执行完毕后,恢复并行执行。
串行序列没有必要使用 barrier 函数,因为串行序列本身一次只会执行一个任务。由于系统也在使用全局队列,barrier 会堵塞其他任务执行,因此在全局队列中使用 barrier 将起不到屏障作用。此时,效果完全等价于dispatch_sync
和dispatch_async
。
barrierQueue.async(flags: .barrier) {
sleep(1)
print("write")
}
使用 barrier 后可以确保只有一个线程进入临界区域,确保线程安全。
使用sync
函数直接读锁即可:
barrierQueue.async {
sleep(1)
print("read")
}
点击这里可以查看关于 barrier 更详细介绍。
Demo名称:Synchronization
源码地址:https://github.com/pro648/BasicDemos-iOS/tree/master/Synchronization
上一篇:线程同步之条件锁
参考资料: