c/c++:线程同步(互斥锁、死锁、读写锁、条件变量、生产者和消费者模型、信号量)

目录

1. 概念

2. 互斥锁

3. 死锁

4. 读写锁

5. 条件变量

5.1 生产者和消费者模型

6. 信号量


 

1. 概念

  • 线程同步:

 > 当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作。
  > - 在多个线程操作一块共享数据的时候
  >   - 按照先后顺序依次访问
  >   - 有原来的 并行 -> 串行

  • 临界资源:一次只允许一个线程使用的资源。
  • 原子操作:

  > 原子操作,就是说像原子一样不可再细分不可被中途打断。

  > 一个操作是原子操作,意思就是说这个操作是以原子的方式被执行,要一口气执行完,执行过程不能够被OS的其他行为打断,是一个整体的过程,在其执行过程中,OS的其它行为是插不进来的。

 

2. 互斥锁

  • 互斥锁类型:
// pthread_mutex_t 互斥锁的类型
pthread_mutex_t mutex;
  • 互斥锁特点:让多个线程, 串行的处理临界区资源(一个代码块)
  • 互斥锁相关函数:
  #include <pthread.h>

  // 初始化互斥锁
  int pthread_mutex_init(pthread_mutex_t *restrict mutex,
             const pthread_mutexattr_t *restrict attr);
  	参数: 
  		- mutex: 互斥锁的地址
  		- attr: 互相锁的属性, 使用默认属性, 赋值为NULL就可以

  // 释放互斥锁资源
  int pthread_mutex_destroy(pthread_mutex_t *mutex);

  // 将参数指定的互斥锁上锁
  // 比如: 3个线程, 第一个线程抢到了锁, 对互斥锁加锁 -> 加锁成功, 进入了临界区
  //  第二,三个个线程也对这把锁加锁, 因为已经被线程1锁定了, 线程2,3阻塞在了这把锁上 -> 不能进入临界区,
  // 	当这把锁被打开, 线程2,3解除阻塞, 线程2,3开始抢锁, 谁抢到谁加锁进入临界区, 另一个继续阻塞在锁上
  int pthread_mutex_lock(pthread_mutex_t *mutex);

  // 尝试加锁, 如果这把锁已经被锁定了, 加锁失败, 函数直接返回, 不会阻塞在锁上
  int pthread_mutex_trylock(pthread_mutex_t *mutex);

  // 解锁函数
  int pthread_mutex_unlock(pthread_mutex_t *mutex);

其中:

  restrict: 修饰符, 被修饰过的变量特点: 不能被其他指针引用
  	- mutex变量对应一块内存
  	- 举例: pthread_mutex_t* ptr; ptr = &mutex; // error
  	-  即便做了赋值, 使用ptr指针操作mutex对应的内存也是不允许的

 

3. 死锁

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

死锁几种场景:

  • 忘记释放锁,自己将自己锁住
  • 单线程重复申请锁
  • 多线程多锁申请, 抢占锁资源(线程A有一个锁1,线程B有一个锁2。线程A试图调用lock来获取锁2就得挂起等待线程B释放,线程B也调用lock试图获得锁1。都在等对方释放,然后获得对方的锁。)

 

4. 读写锁

  • 读写锁类型? 是几把锁?

      1. 读写锁是一把锁
      2. 锁定读操作, 锁定写操作
      3. 类型: pthread_rwlock_t

  • 读写锁的特点

/*
      1. 读操作可以并进行, 多个线程
      2. 写的时候独占资源的
      3. 写的优先级高于读的优先级
*/
场景:

  •       // 1. 线程A加读锁成功, 又来了三个线程, 做读操作, 可以加锁成功----读操作是共享的, 三个新来的线程可以加读锁成功
  •       // 2. 线程A加写锁成功, 又来了三个线程, 做读操作, 三个线程阻塞-------加读锁失败, 会阻塞在读锁上, 写完了
  •       // 3. 线程A加读锁成功, 又来了B线程加写锁阻塞, 又来了C线程加读锁阻塞------写的独占的, 写的优先级高
  • 什么时候使用读写锁?

互斥锁: 数据所有的读写都是串行的
读写锁:
       - 读: 并行
       - 写: 串行
  读的频率 > 写的频率

  • 操作函数:
  #include <pthread.h>
  // 初始化读写锁
  int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
             const pthread_rwlockattr_t *restrict attr);
  	参数:
  		- rwlock: 读写锁地址
  		- attr: 读写锁属性, 使用默认属性, 设置为: NULL

  // 释放读写锁资源
  int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

  // 加读锁
  // rwlock被加了写锁, 这时候阻塞
  // rwlock被加了读锁, 不阻塞, 可以加锁成功 -> 读共享
  int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

  // 尝试加读锁
  int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

  // 加写锁
  // 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);

 

练习例子:  8个线程操作同一个全局变量,其中3个线程不定时写同一全局资源,其中5个线程不定时读同一全局资源

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

int number = 1;
pthread_rwlock_t rwlock;

void* writeNum(void* arg)
{
	while(1)
	{
		pthread_rwlock_wrlock(&rwlock);
		number++;
		usleep(100);
		printf("+++ write, tid: %ld, number: %d\n", pthread_self(), number);
		pthread_rwlock_unlock(&rwlock);
		usleep(100);
	}
	return NULL;
}

void* readNum(void* arg)
{
	while(1)
	{
		pthread_rwlock_rdlock(&rwlock);
		printf("=== read, tid: %ld, number: %d\n", pthread_self(), number);
		pthread_rwlock_unlock(&rwlock);
		usleep(100);
	}
	return NULL;
}

int main(int argc, char *argv[])
{
	pthread_t wtid[3], rtid[5];
	//初始化锁
	pthread_rwlock_init(&rwlock, NULL);
	//创建写进程
	for (int i=0; i<3; ++i)
	{
		pthread_create(&wtid[i],NULL, writeNum, NULL);
	}
	//创建读进程
	for (int i=0; i<5; ++i)
	{
		pthread_create(&rtid[i], NULL, readNum, NULL);
	}

	//回收进程
	for (int i=0; i<3; ++i)
	{
		pthread_join(wtid[i], NULL);
	}
	for (int i=0; i<5; ++i)
	{
		pthread_join(rtid[i], NULL);
	}
	//销毁锁
	pthread_rwlock_destroy(&rwlock);
	return 0;
}

 

5. 条件变量

  • 条件变量不是锁
  • 条件变量两个动作:

条件变量能引起某个线程的阻塞具体来说就是:

  1.    - 某个条件满足之后, 阻塞线程
  2.    - 某个条件满足, 线程解除阻塞

如果使用了条件变量进行线程同步, 多个线程操作了共享数据, 不能解决数据混乱问题,解决该问题, 需要配合使用互斥锁

  • 条件变量类型
pthread_cond_t
  • 条件变量操作函数
#include <pthread.h>
  // 初始化条件变量
  int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
  	参数: 
  		- cond: 条件变量的地址
  		- attr: 使用默认属性, 这个值设置为NULL

  // 释放资源
  int pthread_cond_destroy(pthread_cond_t *cond);

  // 线程调用该函数之后, 阻塞
  int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
  	参数:
  		- cond: 条件变量
  		- mutex: 互斥锁
  		
  struct timespec {
  	time_t tv_sec;      /* Seconds */
  	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
   };
  // 在指定的时间之后解除阻塞
  int pthread_cond_timedwait(pthread_cond_t *restrict cond,
             pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
  	参数:
  		- cond: 条件变量
  		- mutex: 互斥锁
  		- abstime: 阻塞的时间
  			- 当前时间 + 要阻塞的时长
  				struct timeval val;
  			可以使用函数:gettimeofday(&val, NULL);

  // 唤醒一个或多个阻塞在 pthread_cond_wait / pthread_cond_timedwait 函数上的线程
  int pthread_cond_signal(pthread_cond_t *cond);

  // 唤醒所有的阻塞在 pthread_cond_wait / pthread_cond_timedwait 函数上的线程
  int pthread_cond_broadcast(pthread_cond_t *cond);

 

5.1 生产者和消费者模型

角色分析:
      - 生产者
      - 消费者
      - 容器

栗子:使用条件量实现 生产线和消费者模型: 生产者往链表中添加节点, 消费者删除链表节点

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

pthread_cond_t cond;         //条件变量
pthread_mutex_t mutex;       //互斥锁

//连表节点
struct Node
{
	int number;
	struct Node* next;
};

//指向链表第一个节点的指针
struct Node* head = NULL;

// 生产者函数、
void* producer(void* arg)
{
	while(1)
	{
		//创建新的链表节点
		pthread_mutex_lock(&mutex);
		struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
		pnew->next = head;
		head = pnew;
		pnew->number = rand() % 1000;
		printf("add+++ node, number: %d, tid = %ld\n", pnew->number, pthread_self());
		pthread_mutex_unlock(&mutex);

		//生产者生产了东西,通知消费者消费
		pthread_cond_signal(&cond);
	}
	return NULL;
}

//消费者函数
void* customer(void* arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);
		while (head == NULL)
		{
			//链表为空,阻塞
			pthread_cond_wait(&cond, &mutex);
		}

		struct Node* pnode = head;
		head = head->next;
		printf("del--- node, number: %d, tid = %ld\n", pnode->number, pthread_self());
		free(pnode);
		pthread_mutex_unlock(&mutex);
	}

	return NULL;
}

int main(int argc, char *argv[])
{
	pthread_t ptid[5], ctid[5];
	pthread_cond_init(&cond,NULL);
	pthread_mutex_init(&mutex,NULL);

	for (int i=0; i<5; ++i)
	{
		pthread_create(&ptid[i], NULL, producer, NULL);
		pthread_create(&ctid[i], NULL, customer, NULL);
	}

	for (int i=0; i<5; ++i)
	{
		pthread_join(ptid[i], NULL);
		pthread_join(ctid[i], NULL);
	}
	pthread_cond_destroy(&cond);
	pthread_mutex_destroy(&mutex);
	return 0;
}

 

6. 信号量

  • 信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。
  • 信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务 并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
  • 信号量(信号灯)与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用
  • 信号量主要阻塞线程, 不能完全保证线程安全.
  •  如果要保证线程安全, 需要信号量和互斥锁一起使用.

 

- 信号量类型:

sem_t
  在这个变量中记录了一个整形数, 如果这个数据 是5, 允许有五个线程访问数据
          o o o o o
          如果有一线程访问了共享资源, 这个整形数 -1, 后边又有4个线程访问了共享数据 0, 
          这时候, 再有线程访问共享数据, 这些线程阻塞

- 信号量操作函数:

#include <semaphore.h>
  // 初始化信号量
  int sem_init(sem_t *sem, int pshared, unsigned int value);
  	参数: 
  		- sem: 信号量的地址
  		- pshared: 0-> 处理线程, 1-> 处理进程
  		- value: sem_t中整形数初始化

  // 释放资源
  int sem_destroy(sem_t *sem);

  // 有可能引起阻塞
  // 调用一次这个函数 sem 中整形数 --
  // 当 sem_wait 并且 sem中的整形数为0 , 阻塞了
  int sem_wait(sem_t *sem);

  // 当 sem_trywait 并且 sem中的整形数为0 , 返回, 不阻塞
  int sem_trywait(sem_t *sem);

  // 当 sem_timedwait 并且 sem中的整形数为0 , 阻塞一定的时长, 时间到达, 返回
  int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

  // 当 sem_post sem 中的整形数 ++
  int sem_post(sem_t *sem);

  // 查看 sem中的整形数的值, 通过第二个参数返回
  int sem_getvalue(sem_t *sem, int *sval);

 

 

  • 16
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值