mutex和rwmutex类型基本概念
pthread_mutex_t:互斥锁,同一瞬间只能有一个线程能够获取锁,其他线程在等待获取锁的时候会进入休眠状态。因此pthread_mutex_t消耗的CPU资源很小,但是性能不高,因为会引起线程切换。
pthread_spinlock_t:自旋锁,同一瞬间也只能有一个线程能够获取锁,不同的是,其他线程在等待获取锁的过程中并不进入睡眠状态,而是在 CPU上进入“自旋”等待。自旋锁的性能很高,但是只适合对很小的代码段加锁(或短期持有的锁),自旋锁对CPU的占用相对较高。
pthread_rwlock_t:读写锁,同时可以有多个线程获得读锁,同时只允许有一个线程获得写锁。其他线程在等待锁的时候同样会进入睡眠。读写锁在互斥锁的基础上,允许多个线程“读”,通常在读多写少的场景下能提高性能。
std::mutex:互斥锁,同一瞬间只能有一个线程能够获取锁,等待获取锁的线程将会被阻塞并处于等待(waiting)状态。
std::shared_mutex:共享读写锁,支持多个读线程同时访问,但只允许一个写线程访问。std::shared_mutex实现读写分离,读锁需要跟踪读者数量,锁定/解锁开销更大。写操作仍然是独占的,写锁性能与std::mutex相当。
nsync_mutex_t:google开源并发库nsync的互斥锁。
nsync_rwlock_t:google开源并发库nsync的读写锁,即write mode下使用lock,read mode下使用rlock。
测试机器情况
测试结果
本次测试使用std::unorder_map。
exp.1
os: linux
threads: 3读1写
hashmap length: 36675
read times: 10000 / thread
write times: 10000 / thread
epoch: 4
加锁方式 | cost1 | cost2 | cost3 | cost4 | avg | |
std::mutex | 批 | 9186 us | 6446 us | 6415 us | 6403 us | 7112 us |
pthread_mutex_t | 单 | 24062 us | 19120 us | 16866 us | 17141 us | 19297 us |
批 | 9730 us | 18748 us | 16933 us | 17394 us | 15701 us | |
pthread_rwlock_t | 单 | 21546 us | 19722 us | 18900 us | 20660 us | 20207 us |
批 | 13019 us | 10260 us | 10149 us | 10330 us | 10939 us | |
pthread_spinlock_t | 单 | 24909 us | 24051 us | 20031 us | 20241 us | 22308 us |
批 | 10846 us | 6420 us | 6395 us | 6393 us | 7513 us | |
nsync_mutex_t | 单 | 29628 us | 28371 us | 25245 us | 25253 us | 27124 us |
批 | 9267 us | 6562 us | 6501 us | 6500 us | 7207 us | |
nsync_rwlock_t (写lock, 读rlock mix) | 单 | 25121 us | 24177 us | 21694 us | 21674 us | 23166 us |
批 | 13293 us | 10319 us | 10439 us | 10589 us | 11160 us | |
std::shared_mutex | 批 | 13134 us | 13138 us | 10487 us | 10671 us | 11857 us |
exp.2
os: linux
threads: 3读1写
hashmap length: 36675
read times: 10000 / thread
write times: 1000 / thread
epoch: 4
加锁方式 | cost1 | cost2 | cost3 | cost4 | avg | |
std::mutex | 批 | 4965 us | 4612 us | 4630 us | 4646 us | 4713 us |
pthread_mutex_t | 单 | 12305 us | 9831 us | 10552 us | 10900 us | 10897 us |
批 | 4968 us | 9776 us | 10180 us | 10894 us | 8954 us | |
pthread_rwlock_t | 单 | 13780 us | 13561 us | 13517 us | 13342 us | 13550 us |
批 | 8906 us | 8408 us | 8443 us | 8428 us | 8546 us | |
pthread_spinlock_t | 单 | 13822 us | 12730 us | 14003 us | 13518 us | 13518 us |
批 | 5214 us | 4691 us | 4666 us | 4754 us | 4831 us | |
nsync_mutex_t | 单 | 18121 us | 17780 us | 18220 us | 18829 us | 18237 us |
批 | 4699 us | 4534 us | 4496 us | 4675 us | 4601 us | |
nsync_rwlock_t (写lock, 读rlock mix) | 单 | 16456 us | 16375 us | 15957 us | 16975 us | 16440 us |
批 | 8821 us | 8800 us | 8592 us | 8473 us | 8671 us | |
std::shared_mutex | 批 | 8891 us | 8565 us | 8748 us | 8559 us | 8690 us |
exp.3
os: linux
threads: 4读2写
hashmap length: 36675
read times: 10000 / thread
write times: 1000 / thread
epoch: 4
加锁方式 | cost1 | cost2 | cost3 | cost4 | avg | |
std::mutex | 批 | 7697 us | 6348 us | 6094 us | 6086 us | 6556 us |
pthread_mutex_t | 单 | 21670 us | 18028 us | 18288 us | 18539 us | 19131 us |
批 | 6684 us | 18915 us | 18855 us | 18791 us | 15811 us | |
pthread_rwlock_t | 单 | 20989 us | 20792 us | 20321 us | 20618 us | 20680 us |
批 | 16854 us | 16228 us | 15962 us | 16417 us | 16365 us | |
pthread_spinlock_t | 单 | 21585 us | 22893 us | 21294 us | 22650 us | 22105 us |
批 | 7055 us | 7292 us | 6142 us | 7366 us | 6963 us | |
nsync_mutex_t | 单 | 24586 us | 24548 us | 24948 us | 25607 us | 24922 us |
批 | 6602 us | 6008 us | 5980 us | 5985 us | 6143 us | |
nsync_rwlock_t (写lock, 读rlock mix) | 单 | 22541 us | 23444 us | 22741 us | 22859 us | 22896 us |
批 | 17076 us | 15687 us | 16011 us | 15786 us | 16140 us | |
std::shared_mutex | 批 | 16847 us | 16008 us | 15922 us | 16075 us | 16213 us |
exp.4
os: linux
threads: 6读3写
hashmap length: 36675
read times: 10000 / thread
write times: 3000 / thread
epoch: 4
加锁方式 | cost1 | cost2 | cost3 | cost4 | avg | |
std::mutex | 批 | 13420 us | 10872 us | 10700 us | 10704 us | 11424 us |
pthread_mutex_t | 单 | 43406 us | 41285 us | 40320 us | 40768 us | 41444 us |
批 | 12956 us | 40143 us | 38579 us | 38886 us | 32641 us | |
pthread_rwlock_t | 单 | 42786 us | 38810 us | 39713 us | 38104 us | 39853 us |
批 | 31753 us | 29010 us | 29338 us | 29371 us | 29868 us | |
pthread_spinlock_t | 单 | 49577 us | 44320 us | 42921 us | 39853 us | 44167 us |
批 | 16009 us | 12867 us | 15400 us | 10944 us | 13805 us | |
nsync_mutex_t | 单 | 48348 us | 47371 us | 44357 us | 44867 us | 46235 us |
批 | 13579 us | 10974 us | 10890 us | 10833 us | 11569 us | |
nsync_rwlock_t (写lock, 读rlock mix) | 单 | 46518 us | 46523 us | 45031 us | 44671 us | 45685 us |
批 | 32345 us | 29480 us | 29459 us | 29425 us | 30177 us | |
std::shared_mutex | 批 | 31113 us | 28986 us | 28955 us | 31712 us | 30191 us |
exp.5
os: linux
threads: 8读4写
hashmap length: 36675
read times: 10000 / thread
write times: 3000 / thread
epoch: 4
加锁方式 | cost1 | cost2 | cost3 | cost4 | avg | |
std::mutex | 批 | 18684 us | 15223 us | 15186 us | 15191 us | 16071 us |
pthread_mutex_t | 单 | 61733 us | 55128 us | 52081 us | 52337 us | 55319 us |
批 | 18807 us | 53878 us | 51183 us | 50275 us | 43535 us | |
pthread_rwlock_t | 单 | 58043 us | 55728 us | 54218 us | 55093 us | 55770 us |
批 | 47133 us | 44446 us | 47107 us | 43977 us | 45665 us | |
pthread_spinlock_t | 单 | 73558 us | 65183 us | 58573 us | 61395 us | 64677 us |
批 | 20976 us | 20723 us | 17269 us | 16682 us | 18542 us | |
nsync_mutex_t | 单 | 64249 us | 61961 us | 59575 us | 60961 us | 61686 us |
批 | 19168 us | 15756 us | 15542 us | 15591 us | 16514 us | |
nsync_rwlock_t (写lock, 读rlock mix) | 单 | 66496 us | 63230 us | 59097 us | 60990 us | 62453 us |
批 | 48564 us | 45194 us | 45920 us | 45330 us | 46252 us | |
std::shared_mutex | 批 | 47652 us | 44839 us | 44833 us | 44265 us | 45397 us |
实验结论
1.按照keys的批次加锁的情况来看,pthread_mutex_t是性能最差的(剔除异常的cost1数据),其次由于共享资源的锁竞争较小,pthread_rwlock_t和std::shared_mutex相差不大,表现最好的是nsync_mutex_t。就目前的实验参数来看,std::mutex和nsync_mutex_t还要略优于pthread_spinlock_t。nsync_rwlock_t、std::shared_mutex和pthread_rwlock_t在批量加锁的性能表现上是十分相近的。建议在一般情况下,使用std::mutex即可,尽量不用读写锁。
2.pthread_mutex_t、pthread_rwlock_t、pthread_spinlock_t、nsync_mutex_t、nsync_rwlock_t在对单key加锁时性能普遍较低,不推荐在单key的粒度下使用。
3.当共享资源的锁竞争很小时,rwlock的cpu性能消耗反而比spinlock要高。所以不是所有读多写少的情况下,rwlock都优于spinlock。
3.1分析:从spinlock和rwlock的功能上分析,前者只允许一个线程访问共享资源,其余线程均是忙等待;后者则允许多个读者同时访问,绝对限制只有一个线程可以作为写者。当读者访问资源时,写者必须忙等待,反之亦然。只从功能描述上分析,rwlock从功能上应该比spinlock性能要好。而cache数据结构一般使用hash表维护,当散列函数比较理想时,锁竞争发生的概率可能很小。由此猜测rwlock造成的性能下降,可能是因为rwlock的自身上锁解锁的cpu消耗要比spinlock高导致的。在上锁时,pthread_rwlock_wrlock和pthread_rwlock_rdlock会引入线程阻塞和切换的开销,而pthread_spin_lock会自旋等待锁,不会阻塞线程,减少了线程切换的开销。
4.在pthread系列中,读写锁的性能要优于互斥锁;但在nsync系列中,互斥锁要优于读写锁。
5.std::mutex在linux环境下性能比pthread_mutex_t要好的原因有以下几点:
-
std::mutex采用自旋锁+条件变量实现,在锁持有时间短的情况下可以避免上下文切换。而pthread_mutex_t直接使用操作系统mutex,依赖系统调用获取锁,上下文切换开销较大。
解释:GCC标准库实现的std::mutex,在Linux下默认会使用PTHREAD_MUTEX_ADAPTIVE_NP类型的pthread mutex作为底层实现。这种底层实现可以让std::mutex在不同的并发场景下自动选择最优的实现策略(锁竞争少时采用自旋,多时采用阻塞实现来避免频繁上下文切换)。而pthread_mutex_t是固定使用一种类型(阻塞),因此这是std::mutex在linux环境下比pthread_mutex_t更优的原因。
-
C++标准库mutex实现经过了大量优化,如GCC里采用了特定于CPU的优化指令。
-
std::mutex可以实现锁粒度更细的优化控制,避免不必要的锁竞争。