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. 条件变量
条件变量不是锁
条件变量两个动作:
条件变量能引起某个线程的阻塞具体来说就是:
- 某个条件满足之后, 阻塞线程
- 某个条件满足, 线程解除阻塞
如果使用了条件变量进行线程同步, 多个线程操作了共享数据, 不能解决数据混乱问题,解决该问题, 需要配合使用互斥锁
条件变量类型
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);
————————————————
版权声明:本文为CSDN博主「陈宸-研究僧」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_35883464/article/details/103547949