Go语言设计与实现(上)

Python010

Go语言设计与实现(上),第1张

基本设计思路:

类型转换、类型断言、动态派发。iface,eface。

反射对象具有的方法:

编译优化:

内部实现:

实现 Context 接口有以下几个类型(空实现就忽略了):

互斥锁的控制逻辑:

设计思路:

(以上为写被读阻塞,下面是读被写阻塞)

总结,读写锁的设计还是非常巧妙的:

设计思路:

WaitGroup 有三个暴露的函数:

部件:

设计思路:

结构:

Once 只暴露了一个方法:

实现:

三个关键点:

细节:

让多协程任务的开始执行时间可控(按顺序或归一)。(Context 是控制结束时间)

设计思路: 通过一个锁和内置的 notifyList 队列实现,Wait() 会生成票据,并将等待协程信息加入链表中,等待控制协程中发送信号通知一个(Signal())或所有(Boardcast())等待者(内部实现是通过票据通知的)来控制协程解除阻塞。

暴露四个函数:

实现细节:

部件:

包: golang.org/x/sync/errgroup

作用:开启 func() error 函数签名的协程,在同 Group 下协程并发执行过程并收集首次 err 错误。通过 Context 的传入,还可以控制在首次 err 出现时就终止组内各协程。

设计思路:

结构:

暴露的方法:

实现细节:

注意问题:

包: "golang.org/x/sync/semaphore"

作用:排队借资源(如钱,有借有还)的一种场景。此包相当于对底层信号量的一种暴露。

设计思路:有一定数量的资源 Weight,每一个 waiter 携带一个 channel 和要借的数量 n。通过队列排队执行借贷。

结构:

暴露方法:

细节:

部件:

细节:

包: "golang.org/x/sync/singleflight"

作用:防击穿。瞬时的相同请求只调用一次,response 被所有相同请求共享。

设计思路:按请求的 key 分组(一个 *call 是一个组,用 map 映射存储组),每个组只进行一次访问,组内每个协程会获得对应结果的一个拷贝。

结构:

逻辑:

细节:

部件:

如有错误,请批评指正。

读写锁(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 初始化方法如下:

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

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

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

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

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

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

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

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

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

创建并发队列方法如下:

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

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

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

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

Demo名称:Synchronization

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

参考资料:

欢迎更多指正: https://github.com/pro648/tips

本文地址: https://github.com/pro648/tips/blob/master/sources/线程同步之读写锁.md