带你搞定多线程(中) ——通过互斥锁,条件变量,信号量实现线程安全

线程安全概念

线程安全的概念就是多个执行流对临界资源进行争抢访问但是不会出现数据二义性
线程安全的实现主要是通过同步与互斥

  • 同步:通过条件判断保证对临界资源访问的合理性
  • 互斥:通过同一时间对临界资源访问的唯一性实现对临界资源访问的安全性

同步与互斥详细概念

相交进程之间的关系主要有两种,同步与互斥。所谓互斥,是指散步在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它 们之中的任一程序片段 只能等到该进程运行完这个程序片段后才可以运行。所谓同步,是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的 某种先后次序来运行,这种先后次序依赖于要完成的特定的任务

显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。
  
  也就是说互斥是两个线程之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要安照某种次序来运行相应的线程(也是一种互斥)!
  
总结
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

我们可以通过互斥锁,条件变量,信号量来实现同步与互斥,下面对其逐一介绍。

互斥锁

互斥锁原理

互斥锁本质上是一个0/1的计数器,描述了临界资源当前是否可以访问,每当有执行流想要访问临界资源的时候都需要先进行判断,可以访问则加锁(将临界资源置为不可访问状态),然后自己访问,最后解锁。当不能访问的时候,就会进行等待。

互斥锁可以被多个执行流争抢的访问使用,因此互斥锁的读写操作自身必须保证是安全的。
互斥锁通过保证自身计数是原子操作来实现自身的安全性。

每当线程访问互斥锁的时候,寄存器就会将自己先置为1(不可访问状态),然后该寄存器的值和内存中互斥锁的计数器进行交换,这种交换是原子操作,这样就可以不必担心时间片切换导致的一些问题了。
在这里插入图片描述

互斥锁接口说明

1.定义互斥锁变量

pthread_mutex_t mutex

2.初始化互斥锁变量

pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

因为互斥锁是一个结构体,因此方法二是通过直接赋值的方式初始化。

3.访问临界资源前先加锁

pthread_mutex_lock(pthread_mutex_t *mutex)
pthread_mutex_trylock(pthread_mutex_t *mutex)

方法一是阻塞加锁,方法二非阻塞。

4.访问完毕后进行解锁

pthread_mutex_unlock(pthread_mutex_t *mutex)

5.销毁互斥锁

pthread_mutex_destory(pthread_mutex_t *mutex)

死锁

当一个进程中有多个互斥锁,当多个执行流对锁资源进行争抢访问,但是由于推进顺序不当,造成互相等待无法继续推进,这时候就造成了死锁。
死锁实际上就是,程序卡在某个地方,没办法继续执行的概念。

死锁产生的必要条件:有一个不满足就不会造成死锁。

  • 互斥条件:我上了锁,其他人不能上锁
  • 不可剥夺条件:我上了锁,别人不能解,只有我可以解锁
  • 请求与保持条件:我加了A锁,去请求B锁,如果不能对B加锁,也不释放A锁
  • 环路等待条件:我上了A锁,去请求B,另一个人上了B来请求A

死锁的预防:
主要破环3,4条件即可。

死锁的避免:
死锁检测算法/银行家算法

代码实现

以模拟抢票为例,代码一是没有使用互斥锁,执行结果中抢到了-1/-2.着显然不合常理。
代码而使用互斥锁进行对ticket的保护,是正确代码。

代码一:
在这里插入图片描述
执行结果:
在这里插入图片描述
正确代码:
在这里插入图片描述
执行结果:
在这里插入图片描述

条件变量

条件变量搭配互斥锁就可以实现不同执行流之间的同步与互斥。
条件变量的实现方式:当指定执行流不满足执行条件时,就让该执行流陷入睡眠,进入pcb等待队列,等待条件满足之后唤醒执行流。

函数接口

1.定义条件变量

pthread_cond_t cond

2.初始化条件变量

pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t * attr)
cond = PTHREAD_COND_INITIALIZER

3.使线程挂起休眠

pthread_cond_wait(pthread_cond_t *cond, pthread_mutex *mutex)
pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex *mutex, struct timespec)

调用函数一会让线程一直等待,直到被唤醒。
调用函数二,会在我们设置的等待时间到来之后,自动醒来。

4.唤醒执行流

pthread_cond_single(pthread_cond_t *cond)
pthread_cond_broadcast(pthread_cond_t *cond)

函数一唤醒至少一个线程。
函数二唤醒所有等待线程。

5.销毁条件变量释放资源

pthread_cond_destory(pthread_cond_t *cond)

代码测试

两组线程,一组代表厨师,一组代表顾客,全局变量bowl代表有没有饭,厨师有饭时等待,没饭时被唤醒做饭,顾客没饭时等待,有饭时被唤醒吃饭。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

信号量

在之前进程间通信的章节简单介绍信号量,信号量实现了同步互斥,使得共享内存可以安全合理进行。信号量主要是用来实现同步的,

信号量本质上是一个计数器加上一个pcb等待队列。

同步的实现:通过计数器的计数判断当前执行流是否可以访问临界资源。
互斥的实现:通过计数器不大于1,实现对临界资源访问的唯一性。

函数接口

1.定义信号量

sem_t sem

2.初始化信号量

int sem_init(sem_t *sem, int pshared, unsigned int value)

sem:定义的信号量变量
pshared:0用于线程/ 非0用于进程
value:初始化信号量的初值,就是计数器的初值

3.访问临界资源前,先访问信号量,判断是否可以访问临界资源

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

wait:阻塞等待
try_wait:非阻塞等待
timedwait:可设置等待时间

4.促使访问条件满足(+1),唤醒线程/进程

int sem_post(sem_t *sem);

5.销毁信号量,释放资源

sem_t destory(sem_t *sem);

信号量实现生产者消费者模型

代码实现:

    1#include <cstdio>
    2 #include <iostream>
    3 #include <vector>                                                                                                                                                      
    4 #include <pthread.h>
    5 #include <semaphore.h>
    6 
    7 #define QUEUE_MAX 5
    8 
    9 class SemQueue{
   10   public:
   11     SemQueue(int maxq = QUEUE_MAX):_queue(maxq), _capacity(maxq),_step_read(0),_step_write(0){
   12       sem_init(&_lock, 0, 1);
   13       sem_init(&_sem_data, 0, 0);
   14       sem_init(&_sem_idle, 0, maxq);//传入空闲节点maxq
   15     }
   16     ~SemQueue(){
   17       sem_destroy(&_lock);//释放资源
   18       sem_destroy(&_sem_data);
   19       sem_destroy(&_sem_idle);
   20     }
   21     bool Push(int data){
   22       sem_wait(&_sem_idle);//插入数据先判断
   23       sem_wait(&_lock);
   24       _queue[_step_write] = data;
   25       _step_write = (_step_write + 1) % _capacity;
   26       sem_post(&_lock);
   27       sem_post(&_sem_data);
   28       return true;
   29     }
   30     bool Pop(int *data){
   31       sem_wait(&_sem_data);//删除数据先判断
   32       sem_wait(&_lock);
   33      *data =  _queue[_step_read];
   34      _step_read = (_step_read + 1) % _capacity;
   35      sem_post(&_lock);
   36      sem_post(&_sem_idle);
   37      return true;
   38     }
   39   private:
   40     std::vector<int> _queue;
   41     int _capacity;
   42     int _step_read;//写入位置下标
   43     int _step_write;//读取位置下标
   44     sem_t _lock;//实现互斥
   45     sem_t _sem_idle;//计算空闲节点
sem_t _sem_data;//计算存储数据节点
   47 
   48 };
   49 void *thr_productor(void *arg){
   50   SemQueue *queue = (SemQueue*)arg;
   51   int i = 0;
   52   while(1) {
   53     queue->Push(i);
   54     printf("product push data: %d\n", i++);
   55   }
   56   return NULL;
   57 }
   58 void *thr_customer(void *arg){
   59   SemQueue *queue = (SemQueue*)arg;
   60   while(1) {
   61     int data;
   62     queue->Pop(&data);
W> 63     printf("tid: %p customer pop data: %d\n", pthread_self(), data);
   64   }
   65   return NULL;
   66 }
   67 
   68 int main () {
   69   int ret;
   70   int i;
   71   pthread_t product[4], custom[4];
   72   SemQueue queue;
   73   for (i = 0; i < 4; i++) {//创建线程
   74     ret = pthread_create(&product[i], NULL, thr_productor, (void*)&queue);
   75     if (ret != 0) {
   76       printf("creat prodect error\n");
   77       return -1;
   78     }
   79     ret = pthread_create(&custom[i], NULL, thr_customer, (void*)&queue);
   80     if (ret != 0) {
   81       printf("creat custom error\n");
   82       return -1;                                                                                                                                                       
   83     }
   84   }
   85   for (i = 0; i < 4; i++) {//等待线程
   86     pthread_join(product[i], NULL);
   87     pthread_join(custom[i], NULL);
   88   }
   89   return 0;
   90 }      

执行结果:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/ifwecande/article/details/107571743