09_线程同步_互斥锁_读写锁_条件变量_信号量

代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode

代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode

1. 概念

1.1、线程同步

当有一个线程在对内存进行操作时,直到该线程完成操作,其他线程都不可以对这个内存地址进行操作。

  • 在多个线程操作一块共享数据的时候
    • 按照先后顺序依次访问
    • 由原来的 并行 -> 串行

1.2、临界资源

  • 线程之间共享的资源都称之为临界资源
    • 如:堆区、全局区、代码段、动态库加载区
    • 在程序中就是全局变量, 堆变量 …
    • 线程同步之后,临界资源一次只允许一个线程使用

1.3、临界区

一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问,这个区域称之为临界区。当有线程进入临界区段时,其他线程必须等待,以确保这些共用资源是被互斥使用的。

临界区对应的代码块越小越好(但是,该包括的一定要包括进来)

2. 互斥锁(互斥量)

2.1、互斥锁类型?

// 互斥锁的类型:pthread_mutex_t 
pthread_mutex_t mutex;

2.2、互斥锁特点?

让多个线程, 串行的处理临界区资源(一个代码块)

2.3、互斥锁相关函数

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);
// 初始化互斥锁, 初始化完毕之后, 互斥锁是打开的
参数: 
	- mutex: 要初始化的互斥锁的地址
	- attr: 互斥锁的属性, 一般使用默认属性不设置,NULL
        
restrict: 关键字,修饰一个指针(地址)
    - restrict mutex 是说mutex指针指向的地址只能由mutex这个指针进行操作, 其他指针不行
    - 举例:
		pthread_mutex_t *restrict ptr = mutex;	
		// error,通过ptr操作mutex指针指向的内存

int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 销毁互斥锁	

int pthread_mutex_lock(pthread_mutex_t *mutex);
// 对互斥锁上锁
// 1. 如果互斥锁状态是打开的, 上锁成功, 锁被锁定
// 2. 如果互斥锁状态是已经被锁定了, 线程被阻塞在这把锁上, 当这把锁被打开之后, 线程解除阻塞

int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 对互斥锁上锁
// 1. 如果互斥锁状态是打开的, 上锁成功, 锁被锁定
// 2. 如果互斥锁状态是已经被锁定了,线程不会阻塞在这把锁上, 这个函数不阻塞线程
// 3. 通过返回值判断加锁成功了还是失败了
// 		0-> 成功, 非0 -> 失败

int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 打开互斥锁

3. 死锁

两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁 。

3.1、死锁 的几种场景

3.1.1、忘记释放锁,自己将自己锁住
void funcA()
{
    while(1)
    {
    	pthread_mutex_lock(&mutex);
    	......
        .......
      // 忘记解锁
    }
}
3.1.2、单线程重复申请锁
void funcA()
{
    while(1)
    {
    	pthread_mutex_lock(&mutex);
    	......
        .......
        pthread_mutex_unlock(&mutex);
    }
}
int funcB()
{
    ......;
  ......;
    pthread_mutex_lock(&mutex);
    funcA();
    pthread_mutex_unlock(&mutex);
}
3.1.3、多线程多锁申请, 抢占锁资源

线程A有一个锁1,线程B有一个锁2。线程A试图调用lock来获取锁2就得挂起等待线程B释放,线程B也调用lock试图获得锁1。那么这就很尴尬,都在等对方释放,然后获得对方的锁

在这里插入图片描述

3.2、死锁的解决

  • 1.加锁的时候使用trylock

    pthread_mutex_trylock

  • 2.在访问其他资源加锁的时候, 先将自己的锁解开

4. 读写锁

1.读写锁类型? 是几把锁?

类型:pthread_rwlock_t
这是一把锁, 可以做两件事儿
	- 对读操作加锁
	- 对写操作加锁

2.读写锁的特点?

// 场景
// 1. 线程A加读锁成功, 又来了三个线程, 做读操作, 可以加锁成功
	- 读写锁对于读来说是共享的, 可以并行操作
        
// 2. 线程A加写锁成功, 又来了三个线程, 做读操作, 三个线程阻塞
//	  线程A加读锁成功, 又来了三个线程, 做写操作, 三个线程阻塞
    - 既有读又有写操作, 不共享, 先来的加锁成功, 后来的阻塞,

// 3. 线程A加读锁成功, 又来了B线程加写锁阻塞, 又来了C线程加读锁阻塞
    - 写的优先级比读的优先级高, 
	  当线程A解锁之后, 线程B拿到了锁, 加锁成功, C继续阻塞
    - 写的不共享的, 是独占的

3.什么时候使用读写锁?

// 假设共享数据大部分时候线程对其进行读操作 -> 概率: 90%读-> 使用读写锁,效率高
大量的读操作,少量的写操作,加读写锁-->能提高程序的效率

4.操作函数

#include <pthread.h>
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           const pthread_rwlockattr_t *restrict attr);
参数:
	参数一:读写锁的地址
	参数二:读写锁的属性,默认为NULL
	
// 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

// 加读锁, 阻塞
// 有其他线程对这把锁加了写锁, 这时候阻塞
// 有其他线程对这把锁加了读锁, 不阻塞, 读锁是共享的
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

// 尝试加读锁, 不阻塞
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

// 加写锁, 阻塞
// 不管这把锁现在加的是读锁还是写锁, 只要是锁定状态,再加写锁 就阻塞
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

// 尝试加写锁, 不阻塞
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

// 解锁  
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

练习/code

# 8个线程操作同一个全局变量
	3个线程不定时写同一全局资源,
	5个线程不定时读同一全局资源
 		1.查看不加锁结果
		2.查看加锁之后的结果

5. 条件变量 -> condition

5.1.1条件变量是锁吗?

不是锁, 不能同步线程, 
但是可以阻塞线程, 需要配合互斥锁一起使用

5.1.2条件变量的两个动作

使用者可以设置一些条件
 1.如果条件不满足, 可以让线程阻塞(调用条件变量中对应的函数)
 2.当条件满足, 唤醒阻塞的线程(调用条件变量中对应的函数)

5.1.3条件变量类型

pthread_cond_t

5.1.4条件变量/信号量的使用场景

在处理生产者和消费者模型的时候, 使用条件变量/信号量

5.3、条件变量操作函数

#include <pthread.h>

// 初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,
      const pthread_condattr_t *restrict attr);
参数:
	参数一:条件变量的地址
	参数二:条件变量的属性,一般写NULL

// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);






// 阻塞线程的函数
// 线程走到这个函数的位置:线程阻塞;这个线程已经成功对互斥锁加锁
// 这个wait函数干两件事:
// 1.(阻塞线程之前)
// 假设这个线程阻塞在条件变量这儿,整个程序有可能死锁
// 因此这个wait函数在阻塞线程之前,会自动将线程对应的互斥锁解锁
// 2.(解除阻塞线程之后)
// 当线程解除阻塞,wait函数会将线程对应的互斥锁再加锁  
int pthread_cond_wait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex);
参数:
	- cond: 条件变量的地址
	- mutex: 互斥锁变量 -> 使用这个互斥锁进行线程同步
		
// 将线程阻塞一定的时长
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
参数:
	- cond: 使用的条件变量的地址
	- mutex: 互斥锁变量 -> 使用这个互斥锁进行线程同步
	- abstime: 阻塞的时长, 这个时间是从1970.1.1开始计算
		需要: time(NULL) + 需要阻塞的时长
	struct timespec 
	{
		time_t tv_sec;      /* Seconds */
		long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
	};
	即:abstime.tv_sec=time(NULL)+阻塞的秒数

        
        
        
        
        
        
// 唤醒阻塞的线程, 至少唤醒一个阻塞的线程
int pthread_cond_signal(pthread_cond_t *cond);

// 唤醒阻塞的线程, 唤醒所有阻塞的线程
int pthread_cond_broadcast(pthread_cond_t *cond);

5.2、生产者和消费者模型

// 几个角色: 生产者, 消费者, 还需要一个容器(存储数据的, 为生产者和消费者服务)
// 容器需要对应一个数据结构: 队列
/*
生产者:
	- 生成数据放到消息队列中, 队列满了之后, 生产者阻塞
	- 当队列从满 -> 不满, 生产者解除阻塞, 继续工作
消费者:  
	- 处理任务队列中的消息
		- 当前任务队列为空的时候, 消费者阻塞
		- 当前任务队列不为空的时候, 消费者解除阻塞
*/

在这里插入图片描述

5.4、练习

使用条件量实现:生产线和消费者模型
	生产者往链表中添加节点
	消费者删除链表节点
规则如下:
	生产者:新节点挂在头部
	消费者:删除头部节点
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <pthread.h>

//6.1.1、定义条件变量
pthread_cond_t cond;

//4.1、创建互斥锁
pthread_mutex_t mutex;

//2.1、定义链表节点的结构体 
struct Node 
{
    int number;//存储的数据
    struct Node* next;//指向下一个节点的指针
};
//2.2、定义头结点指针
//生产者和消费者通过它来操作容器
struct Node* head = NULL;

//3.1、线程处理函数
void* producer(void* arg)
{
    while(1)
    {
        //5.1、互斥锁:加锁
        pthread_mutex_lock(&mutex);

        //3.1.1、生成节点
        struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
        //3.1.2、初始化
        pnew->number = rand()%1000;
        pnew->next = NULL;
        //3.1.3、添加到表头
        pnew->next = head;
        head = pnew;
        
        //5.1、互斥锁:解锁
        pthread_mutex_unlock(&mutex);

        sleep(rand()%3);

        //6.2、唤醒阻塞在条件变量上的线程
        pthread_cond_signal(&cond);
    }
    return NULL;
}
//3.1、线程处理函数
void* customer(void* arg)
{
    while(1)
    {
        //5.1、互斥锁:加锁
        pthread_mutex_lock(&mutex);

        //3.2.1、判断链表是否为空
        // 多线程情况下:多个消费者线程同时阻塞在wait函数这儿
        // 当生产者线程同时唤醒好几个阻塞在此处的消费者线程后
        // 当一个解除阻塞的消费者线程去执行下面的代码(删除节点)后
        // 另外一个解除阻塞的消费者线程应该先判断容器是否为空,
        // 因为可能消费了一次后,容器又为空了,不能消费,需继续阻塞线程
        while(head == NULL)
        {
            //链表为空,消费者就不能消费,消费者线程阻塞
            //6.1、阻塞消费者线程
            // 线程走到这个函数的位置:线程阻塞;这个线程已经成功对互斥锁加锁 
            // 这个wait函数干两件事:
            // 1.(阻塞线程之前)
            // 假设这个线程阻塞在条件变量这儿,整个程序有可能死锁
            // 因此这个wait函数在阻塞线程之前,会自动将线程对应的互斥锁解锁
            // 2.(解除阻塞线程之后)
            // 当线程解除阻塞,wait函数会将线程对应的互斥锁再加锁 
            pthread_cond_wait(&cond,&mutex);         
        }
        //3.2.2、链表不为空,开始消费
        struct Node* pNode = head;
        head = head->next;
        printf("删除节点的number:%d,%ld\n",pNode->number,pthread_self());
        free(pNode);//删除节点
        
        //5.1、互斥锁:解锁
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
int main()
{

    //4.2、初始化互斥锁
    pthread_mutex_init(&mutex,NULL);

    //6.1.2、初始化条件变量
    pthread_cond_init(&cond,NULL);

    //1.1、创建线程--5个读,5个写线程
    pthread_t ptid[5];//生产者
    pthread_t ctid[5];//消费者
    for(int i=0;i<5;++i)
    {
        pthread_create(&ptid[i],NULL,producer,NULL);
        pthread_create(&ctid[i],NULL,customer,NULL);
    }
    
    //1.2、回收子线程资源
    for(int i=0;i<5;++i)
    {
        pthread_join(ptid[i],NULL);
        pthread_join(ctid[i],NULL);
    }
    
    //4.3、销毁互斥锁
    pthread_mutex_destroy(&mutex);

    //6.1.3、销毁条件变量
    pthread_cond_destroy(&cond);
    
    return 0;
}

6. 信号量

概念

信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。

信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务 并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。

信号量(信号灯)与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。

信号量主要阻塞线程, 不能完全保证线程安全.

如果要保证线程安全, 需要信号量和互斥锁一起使用.

信号量类型

sem_t:理解为在这个变量中有一个整形数 >=0
     == 0: 线程阻塞
     => 0: 不阻塞

信号量操作函数

#include <semaphore.h>

// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
    - 参数一: 信号量变量的地址
    - pshared:
        0: 线程操作
        1: 进程操作
    - value: 给信号量初始化一个整形值 >=0, 设置给了第一个参数
      value值代表了初试时有几个线程工作

// 销毁信号量
int sem_destroy(sem_t *sem);

// 在某些情况下阻塞线程
int sem_wait(sem_t *sem);
 调用sem_init的时候,第三个参数value初始化了一个整形数
  1.sem_wait每调用一次,这个整形数 -1
  2.当这个整形数为0的时候, 再调用这个函数->线程阻塞


int sem_trywait(sem_t *sem);
 1.sem_trywait每调用一次,这个整形数 -1
 2.当这个整形数为0的时候, 调用这个函数->不阻塞线程, 函数返回


int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
 1.sem_timedwait每调用一次,这个整形数 -1
 2.当这个整形数为0的时候, 调用这个函数->阻塞线程, 
   阻塞第二个参数指定的时间长度
 3.阻塞时间完成, 解除阻塞

// 让信号量变量sem中的整数 +1
int sem_post(sem_t *sem);

// 获取当前参数sem中的整数值
int sem_getvalue(sem_t *sem, int *sval);
 1.获取当前参数sem中的整数现在的值为多少
 2.通过第二个参数将得到的数值传出

练习

使用信号量实现:生产线和消费者模型: 
    生产者往链表中添加节点
    消费者删除链表节点
/*
思路:
- 分析有几类线程, 需要定义几个信号量: 两类线程、对应两类信号量
    - 生产者线程: sem_t psem;
    - 消费者线程: sem_t csem;
信号量初始化:
    生产者:sem_init(&psem, 0, 5); // 生产者可以生产5次
    消费者:sem_init(&psem, 0, 0); // 消费者默认不能消费 -> 阻塞
    sem_init(&psem, 0, 5); // 生产者线程5个
    sem_init(&psem, 0, 0); // 消费者线程0个,默认不能消费 -> 阻塞
*/
伪代码:

// 生产者线程-回调函数
void* producer(void* arg)
{
    // 通过信号量去尝试阻塞线程
    sem_wait(&psem);	// psem--
    // 创建链表节点, 添加到表头
    // 创建完成, 通知消费者消费
    sem_post(&csem);	// csem++
}
// 消费者线程-回调函数
void* customer(void* arg)
{
    // 根据初始化数据, 刚开始所有的消费者线程都会阻塞在这
    sem_wait(&csem);
    
    // 监测是否可以消费
    // 消费:删除链表节点
    
    // 通知生产者生产
    sem_post(&psem);//psem++
}

int main()
{
	// 创建生产者线程 -> 5
    // 创建消费者线程 -> 5    
   sem_init(&psem, 0, 5); // 生产者线程5个
   sem_init(&psem, 0, 0); // 消费者线程0个,默认不能消费 -> 阻塞
}

sem, 0, 0); // 消费者线程0个,默认不能消费 -> 阻塞
*/


```c
伪代码:

// 生产者线程-回调函数
void* producer(void* arg)
{
    // 通过信号量去尝试阻塞线程
    sem_wait(&psem);	// psem--
    // 创建链表节点, 添加到表头
    // 创建完成, 通知消费者消费
    sem_post(&csem);	// csem++
}
// 消费者线程-回调函数
void* customer(void* arg)
{
    // 根据初始化数据, 刚开始所有的消费者线程都会阻塞在这
    sem_wait(&csem);
    
    // 监测是否可以消费
    // 消费:删除链表节点
    
    // 通知生产者生产
    sem_post(&psem);//psem++
}

int main()
{
	// 创建生产者线程 -> 5
    // 创建消费者线程 -> 5    
   sem_init(&psem, 0, 5); // 生产者线程5个
   sem_init(&psem, 0, 0); // 消费者线程0个,默认不能消费 -> 阻塞
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值