Linux多线程学习(4) --读写锁和其他类型的锁以及线程安全

版权声明:本文为博主原创文章,欢迎转载,转载请声明出处! https://blog.csdn.net/hansionz/article/details/84842790

多线程学习总结(1):https://blog.csdn.net/hansionz/article/details/84665815
多线程学习总结(2):https://blog.csdn.net/hansionz/article/details/84675536
多线程学习总结(3):https://blog.csdn.net/hansionz/article/details/84766601

一.读写锁

1.什么是读写锁

学习多线程的时候,有一种情况是十分常见的。那就是有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,对临界资源的修改,一定要加上互斥量去保护临界资源,但是在读的过程中,往往伴随着查找的操作,中间耗时很长。如果给这种代码段加互斥锁,会极大地降低程序的效率。读写锁就是解决多读少写问题。

读写锁支持当没有线程去写入的时候,可以存在多个读者线程同时去共享的访问临界资源,而当临界区没有读者线程去访问或者没有写者线程去写的时候才允许该线程去写。这种用于共享访问给定资源的读写锁,也叫共享-独占锁,获取一个读写锁用于读称为共享锁,获取一个读写锁用于写称为独占锁

2.读者和写者的关系

  • 读者和读者:共享关系。可以允许多个线程同时读
  • 写者和写者:互斥关系。当有一个线程在写,其他线程不能写入
  • 读者和写者:步与互斥。当有读者在读或者写者在写的时候,不能存在其他线程写;当当有线程在读的时候,不能有其他线程写入。

读写锁分配规则:

  • 只要没有线程拿着读写锁用于写任意数目的线程可以拿到读写锁用来读
  • 如果没有线程拿着读写锁用来读或写的时候,才可以存在线程用来

读写锁的行为:
在这里插入图片描述

总结:写独占、读共享、写锁优先级高

3.初始化读写锁

读写锁的数据类型为pthread_rwlock_t。它存在两种初始化方式:

  • 静态方法:
//直接给读写锁变量赋值PTHREAD_RWLOCK_INITALIZER
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITALIZER
  • 动态分配
//调用函数动态初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
参数:
	  rwlock为读写锁变量的地址
	  attr为读写锁的属性,一般不使用可以设置为NULL
返回值:成功返回0,失败返回错误码

4.销毁读写锁

当所有的线程不在持有也不在去申请读写锁的时候,该读写锁应该被销毁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 
参数:
	  rwlock为读写锁变量的地址
返回值:成功返回0,失败返回错误码

5.获取和释放读写锁

pthread_rwlock_rlock函数用来获取一个读锁,如果对于的读写锁被某个写者线程拥有,则该函数会阻塞调用线程pthread_rwlock_wrlock用来获取一个写锁,如果对应的读写锁由另一个写入者或者一个或多个读者所有,那么阻塞调用线程。pthread_rwlock_unlock函数用来释放一个读锁或写锁

//获取读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 
//获取写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); 
//释放读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); 
返回值:成功返回0,出错返回错误码

下面两个函数用来尝试获取读写锁,但是如果锁不能立即获得,就返回EBUSY错误,而不是调用阻塞线程。

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);
返回值:成功返回0,出错返回错误码

注:以上的函数均位于头文件<pthread.h>

6.读写锁的实例

#include <iostream>
#include <unistd.h>

using namespace std;

int book = 0;
pthread_rwlock_t rwlock;

//可以存在任意多个线程同时读
void *read_routine(void *arg)
{
  while(1)
  {
    pthread_rwlock_rdlock(&rwlock);
    
    cout << "my tid is:" << pthread_self() << ".read book data is:" << book << endl;

    pthread_rwlock_unlock(&rwlock);
    sleep(1);
  }
}
//任意时刻只能有一个写者线程在写(在写的时候不能有读者读)
void *write_routine(void *arg)
{
  while(1)
  {
    pthread_rwlock_wrlock(&rwlock);

    ++book;
    cout << "my tid is:"<< pthread_self() << ".write book data is:" << book << endl;
    
    pthread_rwlock_unlock(&rwlock);
    sleep(2);
  }
}
int main()
{
  //初始化读写锁变量
  pthread_rwlock_init(&rwlock, NULL);

  pthread_t r1,r2,w1,w2;//创建两个读者和写者线程
  pthread_create(&r1, NULL, read_routine, NULL);
  pthread_create(&r2, NULL, read_routine, NULL);
  pthread_create(&w1, NULL, write_routine, NULL);
  pthread_create(&w2, NULL, write_routine, NULL);

  pthread_join(r1, NULL);
  pthread_join(r2, NULL);
  pthread_join(w1, NULL);
  pthread_join(w2, NULL);

  //销毁读写锁
  pthread_rwlock_destroy(&rwlock);
  return 0;
}

以上程序的运行结果应该是任意一个时刻,只存在一个写者在修改book变量,但是当没有写者在写的时候,可以允许两个读者同时在读book变量的值。以下为运行结果:
在这里插入图片描述

二.其他常见的各种锁

1.乐观锁和悲观锁

  • 悲观锁::在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁, 行锁等),当其他线程想要访问数据时,被阻塞挂起。悲观锁适用于多写的场景,因为多写的情况会经常产生冲突。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他线程在更新前有没有对数据进行修改。乐观锁适用于多读的应用类型,这样可以提高吞吐量主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的是否相等。如果相等则用新值更新。若不等则失败,失败则重试。 即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数:需要读写的内存值 V进行比较的值 A拟写入的新值 B当且仅当 V 的值等于A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
  • 版本号机制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

推荐阅读:https://blog.csdn.net/qq_34337272/article/details/81072874

2.自旋锁

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断是否能够被成功获取,直到获取到锁才会退出循环

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁互斥锁比较类似,它们都是为了解决对某项资源互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。

推荐阅读:https://blog.csdn.net/qq_34337272/article/details/81252853

3.公平锁和非公平锁

  • 公平锁:按照线程加锁的顺序来分配,即先来先得FIFO
  • 非公平锁:一种获取锁的抢占机制,是随机的获得锁的,这样可能会有些线程一直会拿不到锁,结果也就是不公平

三.线程安全

1.什么是线程安全

线程安全是多线程编程中的一个概念。在拥有共享数据多个线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等错误情况。

2.线程安全版本的单例模式

对于线程安全版本的单例模式单独总结于我的另外一边博客:https://blog.csdn.net/hansionz/article/details/83752531

主要问题:

  • 加锁解锁的位置
  • 双重if判定, 避免不必要的锁竞争
  • volatile关键字防止过度优化

3.STL中的容器是否是线程安全的

答:不是。原因是STL的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响。且对于不同的容器加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。 因此 STL默认不是线程安全, 如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。

4.智能指针是否是线程安全的

  • 对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题。
  • 对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题.。但是标准库实现的时候考虑到了这个问题 ,基于原子操作(CAS)的方式保证 shared_ptr能够高效,原子的操作引用计数。

猜你喜欢

转载自blog.csdn.net/hansionz/article/details/84842790
今日推荐