怎样建设学校网站,济南seo关键词排名工具,最出名的网站建设公司,ppt简洁模板整套免费Linux读写锁的容易犯的问题
读写锁是互斥锁之外的另一种用于多线程之间同步的一种方式。
多线程对于一个共享变量的读操作是安全的#xff0c; 而写操作是不安全的。如果在一个读很多而写很少的场景之下#xff0c;那么使用互斥锁将会阻碍大量的线程安全的读操作的进行。在…Linux读写锁的容易犯的问题
读写锁是互斥锁之外的另一种用于多线程之间同步的一种方式。
多线程对于一个共享变量的读操作是安全的 而写操作是不安全的。如果在一个读很多而写很少的场景之下那么使用互斥锁将会阻碍大量的线程安全的读操作的进行。在这种场景下读写锁这样一种设计便诞生了。
读写锁的特性如下表所示 总结起来就是读读不互斥 读写互斥 写写互斥。
读写读不互斥互斥写互斥互斥
看似这样好的一个设计在实际的使用中确存在诸多的使用误区陈硕大神在他的Linux多线程服务端编程一书中曾给出他的建议不要使用读写锁。 为什么如此呢 下面一一道来。
读写锁使用的正确性
读写锁第一个容易出错的地方就是可能在持有读锁的地方修改了共享数据。对于一些比较简单的方法可能是不容易出错的但是对于嵌套调用的场景下也是容易犯错的。例如下面的例子read方法持有了读锁但是operator4会修改共享变量。由于operator4的调用深度较深因此可能容易犯错。
//operator4会修改共享变量
void operation4();
{//...
}void operation3()
{operation4();
}void operation2()
{operation3();
}void read() {std::shared_lockstd::shared_mutex lock(mtx);operation1();
}读写锁性能上的开销
读写锁从设计上看是比互斥锁要复杂一些因此其内部加锁和解锁的逻辑也要比互斥锁要复杂。
下面是glibc读写锁的数据结构可以推测在加锁解锁过程中要更新reader和writers的数目而互斥锁是无需这样的操作的。
struct __pthread_rwlock_arch_t
{unsigned int __readers;unsigned int __writers;unsigned int __wrphase_futex;unsigned int __writers_futex;unsigned int __pad3;unsigned int __pad4;int __cur_writer;int __shared;unsigned long int __pad1;unsigned long int __pad2;/* FLAGS must stay at this position in the structure to maintainbinary compatibility. */unsigned int __flags;
};下面的一个例子使用互斥锁和读写锁分别对一个临界区进行反复的加锁和解锁。因为临界区没有内容因此开销基本都在锁的加锁和解锁上。
//g test1.cpp -o test1
#include pthread.h
#include iostream
#include unistd.hpthread_mutex_t mutex;
int i 0;void *thread_func(void* args) {int j;for(j0; j10000000; j) {pthread_mutex_lock(mutex);// testpthread_mutex_unlock(mutex);}pthread_exit((void *)0);
}int main(void) {pthread_t id1;pthread_t id2;pthread_t id3;pthread_t id4;pthread_mutex_init(mutex, NULL);pthread_create(id1, NULL, thread_func, (void *)0);pthread_create(id2, NULL, thread_func, (void *)0);pthread_create(id3, NULL, thread_func, (void *)0);pthread_create(id4, NULL, thread_func, (void *)0);pthread_join(id1, NULL);pthread_join(id2, NULL);pthread_join(id3, NULL);pthread_join(id4, NULL);pthread_mutex_destroy(mutex);
}//g test2.cpp -o test2
#include pthread.h
#include iostream
#include unistd.hpthread_rwlock_t rwlock;
int i 0;void *thread_func(void* args) {int j;for(j0; j10000000; j) {pthread_rwlock_rdlock(rwlock);//test2pthread_rwlock_unlock(rwlock);}pthread_exit((void *)0);
}int main(void) {pthread_t id1;pthread_t id2;pthread_t id3;pthread_t id4;pthread_rwlock_init(rwlock, NULL);pthread_create(id1, NULL, thread_func, (void *)0);pthread_create(id2, NULL, thread_func, (void *)0);pthread_create(id3, NULL, thread_func, (void *)0);pthread_create(id4, NULL, thread_func, (void *)0);pthread_join(id1, NULL);pthread_join(id2, NULL);pthread_join(id3, NULL);pthread_join(id4, NULL);pthread_rwlock_destroy(rwlock);
}[rootlocalhost test1]# time ./test1real 0m2.531s
user 0m5.175s
sys 0m4.200s
[rootlocalhost test1]# time ./test2real 0m4.490s
user 0m17.626s
sys 0m0.004s可以看出单纯从加锁和解锁的角度看互斥锁的性能要好于读写锁。
当然这里测试时临界区的内容时空的如果临界区较大那么读写锁的性能可能会优于互斥锁。
不过在多线程编程中我们总是会尽可能的减少临界区的大小因此很多时候读写锁并没有想象中的那么高效。
读写锁容易造成死锁
前面提到过读写锁这样的设计就是在读多写少的场景下产生的然而这样的场景下很容易造成写操作的饥饿。因为读操作过多写操作不能拿到锁造成写操作的阻塞。
因此写操作获取锁通常拥有高优先级。
这样的设定对于下面的场景将会造成死锁。假设有线程A、B和锁按如下时序执行
1、线程A申请读锁2、线程B申请写锁3、线程A再次申请读锁
第2步中线程B在申请写锁的时候线程A还没有释放读锁于是需要等待。第3步中因此线程B正在申请写锁于是线程A申请读锁将会被阻塞于是陷入了死锁的状态。
下面使用c17的shared_mutex来模拟这样的场景。
#include iostream
#include thread
#include mutex
#include condition_variable
#include shared_mutexvoid print() {std::cout \n;
}
templatetypename T, typename... Args
void print(T first, Args ...args) {std::cout first ;print(std::forwardArgs(args)...);
}std::shared_mutex mtx;
int step 0;
std::mutex cond_mtx;
std::condition_variable cond;void read() {//step0: 读锁std::shared_lockstd::shared_mutex lock(mtx);std::unique_lockstd::mutex uniqueLock(cond_mtx);print(read lock 1);//通知step0结束step;cond.notify_all();//等待step1: 写锁 结束cond.wait(uniqueLock, []{return step 2;});uniqueLock.unlock();//step2: 再次读锁std::shared_lockstd::shared_mutex lock1(mtx);print(read lock 2);
}void write() {//等待step0: 读锁 结束std::unique_lockstd::mutex uniqueLock(cond_mtx);cond.wait(uniqueLock, []{return step 1;});uniqueLock.unlock();//step1: 写锁std::lock_guardstd::shared_mutex lock(mtx);uniqueLock.lock();print(write lock);//通知step1结束step;cond.notify_all();uniqueLock.unlock();
}int main() {std::thread t_read{read};std::thread t_write{write};t_read.join();t_write.join();return 0;
}可以使用下面的在线版本进行测试。
have a try
在线版本的输出是下面这样的程序由于死锁执行超时被杀掉了。
Killed - processing time exceeded
Program terminated with signal: SIGKILL死锁的原因就是线程1与线程2相互等待导致。 对于glibc的读写锁其提供了读优先和写优先的属性。
使用pthread_rwlockattr_setkind_np方法即可设置读写锁的属性。其拥有下面的属性
PTHREAD_RWLOCK_PREFER_READER_NP, //读者优先即同时请求读锁和写锁时请求读锁的线程优先获得锁PTHREAD_RWLOCK_PREFER_WRITER_NP, //不要被名字所迷惑也是读者优先PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP, //写者优先即同时请求读锁和写锁时请求写锁的线程优先获得锁PTHREAD_RWLOCK_DEFAULT_NP PTHREAD_RWLOCK_PREFER_READER_NP // 默认读者优先
glibc的读写锁模式是读优先的。下面分别使用读优先和写优先来进行测试。
写优先
#include iostream
#include pthread.h
#include unistd.hpthread_rwlock_t m_lock;
pthread_rwlockattr_t attr;int A 0, B 0;// thread1
void* threadFunc1(void* p)
{printf(thread 1 running..\n);pthread_rwlock_rdlock(m_lock);printf(thread 1 read source A%d\n, A);usleep(3000000); // 等待3s此时线程2大概率会被唤醒并申请写锁pthread_rwlock_rdlock(m_lock);printf(thread 1 read source B%d\n, B);//释放读锁pthread_rwlock_unlock(m_lock);pthread_rwlock_unlock(m_lock);return NULL;
}//thread2
void* threadFunc2(void* p)
{printf(thread 2 running..\n);pthread_rwlock_wrlock(m_lock);A 1;B 1;printf(thread 2 write source A and B\n);//释放写锁pthread_rwlock_unlock(m_lock);return NULL;
}int main()
{pthread_rwlockattr_init(attr);pthread_rwlockattr_setkind_np(attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);//设置写锁优先级高//初始化读写锁if (pthread_rwlock_init(m_lock, attr) ! 0){printf(init rwlock failed\n);return -1;}//初始化线程pthread_t hThread1;pthread_t hThread2;if (pthread_create(hThread1, NULL, threadFunc1, NULL) ! 0){printf(create thread 1 failed\n);return -1;}usleep(1000000);if (pthread_create(hThread2, NULL, threadFunc2, NULL) ! 0){printf(create thread 2 failed\n);return -1;}pthread_join(hThread1, NULL);pthread_join(hThread2, NULL);pthread_rwlock_destroy(m_lock);return 0;
}设置写优先会导致死锁。
读优先
#include iostream
#include pthread.h
#include unistd.hpthread_rwlock_t m_lock;
pthread_rwlockattr_t attr;int A 0, B 0;// thread1
void* threadFunc1(void* p)
{printf(thread 1 running..\n);pthread_rwlock_rdlock(m_lock);printf(thread 1 read source A%d\n, A);usleep(3000000); // 等待3s此时线程2大概率会被唤醒并申请写锁pthread_rwlock_rdlock(m_lock);printf(thread 1 read source B%d\n, B);//释放读锁pthread_rwlock_unlock(m_lock);pthread_rwlock_unlock(m_lock);return NULL;
}//thread2
void* threadFunc2(void* p)
{printf(thread 2 running..\n);pthread_rwlock_wrlock(m_lock);A 1;B 1;printf(thread 2 write source A and B\n);//释放写锁pthread_rwlock_unlock(m_lock);return NULL;
}int main()
{pthread_rwlockattr_init(attr);pthread_rwlockattr_setkind_np(attr, PTHREAD_RWLOCK_PREFER_READER_NP);//初始化读写锁if (pthread_rwlock_init(m_lock, attr) ! 0){printf(init rwlock failed\n);return -1;}//初始化线程pthread_t hThread1;pthread_t hThread2;if (pthread_create(hThread1, NULL, threadFunc1, NULL) ! 0){printf(create thread 1 failed\n);return -1;}usleep(1000000);if (pthread_create(hThread2, NULL, threadFunc2, NULL) ! 0){printf(create thread 2 failed\n);return -1;}pthread_join(hThread1, NULL);pthread_join(hThread2, NULL);pthread_rwlock_destroy(m_lock);return 0;
}读优先则没有死锁的问题可以正常的执行下去。
thread 1 running..
thread 1 read source A0
thread 2 running..
thread 1 read source B0
thread 2 write source A and B通过上面的实验当reader lock需要重入时需要很谨慎一旦读写锁的属性是写优先那么很有可能会产生死锁。
总结
读写锁适用于读多写少的场景在这种场景下可能会有一些性能收益读写锁的使用上存在着一些陷阱平常尽量用互斥锁(mutex)代替读写锁。