Linux下线程同步
一. 线程同步简介
1. 定义
所谓线程同步,其实是为了避免多个线程同时访问一个共享资源,最终导致共享资源出现偏差的情况(目前计算机大都是多核CPU,因此会出现多个进程、线程同时运行的情况)。因此,同步其实是让多个线程依次访问某一共享资源,不要出现多个线程同时访问的情况。
2. 实例分析
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 全局变量,是共享资源
int number;
void *func_a(void *arg){
for (int i = 0; i < 10; i++){
int var = number;
var++;
// 线程先阻塞10微秒,然后再将修改的数写回去
usleep(10);
number = var;
printf("线程A的ID = %ld, number = %d\n", pthread_self(), number);
}
return NULL;
}
void *func_b(void *arg){
for (int i = 0; i < 10; i++){
int var = number;
var++;
number = var;
printf("线程B的ID = %ld, number = %d\n", pthread_self(), number);
// 线程先修改完,再休眠
usleep(5);
}
return NULL;
}
int main(int argc, const char * argv[]){
// 创建两个修改共享资源的子线程
pthread_t p1, p2;
pthread_create(&p1, NULL, func_a, NULL);
pthread_create(&p2, NULL, func_b, NULL);
// 主线程回收子线程资源
pthread_join(p1, NULL);
pthread_join(p2, NULL);
return 0;
}
编译运行:
输出结果如下:
线程B的ID = 140334390478592, number = 1
线程A的ID = 140334398871296, number = 1
线程B的ID = 140334390478592, number = 2
线程A的ID = 140334398871296, number = 2
线程B的ID = 140334390478592, number = 3
线程A的ID = 140334398871296, number = 3
线程B的ID = 140334390478592, number = 4
线程B的ID = 140334390478592, number = 5
线程A的ID = 140334398871296, number = 5
线程B的ID = 140334390478592, number = 6
线程A的ID = 140334398871296, number = 6
线程B的ID = 140334390478592, number = 7
线程A的ID = 140334398871296, number = 7
线程B的ID = 140334390478592, number = 8
线程A的ID = 140334398871296, number = 8
线程B的ID = 140334390478592, number = 9
线程B的ID = 140334390478592, number = 10
线程A的ID = 140334398871296, number = 9
线程A的ID = 140334398871296, number = 10
线程A的ID = 140334398871296, number = 11
可以看出:两个线程对共享资源不论先后的写导致结果出现异常。
原理分析:
在本程序中,如果线程A先抢到的CPU时间片,获取了number(假设为0)的值并传递給var变量,随后A对var进行了++操作,下一步A休眠了,并没有将立即将var的新值传给number,也就是说做A对number的修改还没有写回去,就被阻塞了。(此时number仍为0)
此时B如果抢到了时间片,就会去读取内存中number的值,结果获得的number是0,然后修改了number,令number=1,随后B线程休眠。(此时number=1)
此时A线程如果抢到时间片,由于CPU保留了A线程之前的状态,所以A线程会从休眠指令后一句开始执行,也就是将新的number写回到CPU,于是我们发现,number还是1。
此时两个线程均进行了number++操作,结果却是number只增加了一个1。这就是多线程同时操作临界资源(就是共享资源)带来的隐患。
二. 互斥锁
1. 简介
互斥锁是一种线程同步的常用手法。它通过为操作临界资源的代码块(也就是临界区,一般包含所有涉及临界资源的代码)加锁,从而保证同一时间只有一个线程能够操纵临界资源,避免了多个线程同时操作临界资源的带来的隐患。
注意:
- 一个临界资源最好对应一个锁。
- 加锁后记得释放,否则线程会阻塞。
- 注意
死锁
现象。
2. 函数
数据类型:
linux中互斥锁的类型为pthread_mutex_t
#include <pthread.h>
pthread_mutex_t mutex;
这个锁对象中包含了当前这把锁的状态信息:锁定还是打开,如果锁上了,上锁的线程的ID是多少。一个互斥锁变量只能被一个线程加锁,如果一个线程企图对一个已经加锁的锁进行加锁,那么该线程会阻塞,直到这个锁被其上锁的线程释放,本线程才能将这个锁锁定,随后线程继续运行。
初始化:
#include <pthread.h>
// restrict:一个用于修饰指针的关键字,限定只有当前指针能够访问其指向的内存。
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
- 参数:
- mutex:互斥锁变量的地址。
- attr:互斥锁的属性,一般默认即可,也就是NULL。
- 返回值:初始化成功返回0,失败返回错误码。
释放互斥锁资源:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 参数:
- mutex:互斥锁变量的地址。
- 返回值:释放资源成功返回0,失败返回错误码。
锁定互斥锁:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 参数:
- mutex:互斥锁变量的地址。
- 返回值:锁定互斥锁成功返回0;失败返回错误码,且当前线程进入阻塞状态。
- 特点:如果上锁成功,会继续向下运行。如果失败,线程会阻塞在此处,直到这个锁被释放,当前线程抢到这个锁,会再次将其锁定,然后继续向下运行。
尝试锁定互斥锁:
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 参数:
- mutex:互斥锁变量的地址。
- 返回值:锁定互斥锁成功返回0;失败返回错误码,但当前线程不会阻塞。
为互斥锁解锁
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 参数:
- mutex:互斥锁变量的地址。
- 返回值:锁定互斥锁成功返回0;失败返回错误码。
- 注意:只有为这个锁加锁的线程能够将其解锁,否则解锁会失败。
3. 实例
这里对1.2
中的代码尽心修改。
代码如下:
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 全局变量,是共享资源
int number;
// 互斥锁
pthread_mutex_t mutex;
void *func_a(void *arg){
for (int i = 0; i < 10; i++){
// 为临界区加锁
pthread_mutex_lock(&mutex);
int var = number;
var++;
usleep(10);
number = var;
printf("线程A的ID = %ld, number = %d\n", pthread_self(), number);
// 解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *func_b(void *arg){
for (int i = 0; i < 10; i++){
// 为临界区加锁
pthread_mutex_lock(&mutex);
int var = number;
var++;
number = var;
printf("线程B的ID = %ld, number = %d\n", pthread_self(), number);
// 解锁
pthread_mutex_unlock(&mutex);
usleep(5);
}
return NULL;
}
int main(int argc, const char * argv[]){
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建两个修改共享资源的子线程
pthread_t p1, p2;
pthread_create(&p1, NULL, func_a, NULL);
pthread_create(&p2, NULL, func_b, NULL);
// 主线程阻塞回收子线程资源
pthread_join(p1, NULL);
pthread_join(p2, NULL);
// 回收互斥锁资源
pthread_mutex_destroy(&mutex);
return 0;
}
编译运行
# 编译:
gcc synchronous.c -o app -l pthread
./app
# 运行结果:
线程B的ID = 140005370177280, number = 1
线程A的ID = 140005378569984, number = 2
线程A的ID = 140005378569984, number = 3
线程A的ID = 140005378569984, number = 4
线程A的ID = 140005378569984, number = 5
线程A的ID = 140005378569984, number = 6
线程A的ID = 140005378569984, number = 7
线程A的ID = 140005378569984, number = 8
线程A的ID = 140005378569984, number = 9
线程A的ID = 140005378569984, number = 10
线程A的ID = 140005378569984, number = 11
线程B的ID = 140005370177280, number = 12
线程B的ID = 140005370177280, number = 13
线程B的ID = 140005370177280, number = 14
线程B的ID = 140005370177280, number = 15
线程B的ID = 140005370177280, number = 16
线程B的ID = 140005370177280, number = 17
线程B的ID = 140005370177280, number = 18
线程B的ID = 140005370177280, number = 19
线程B的ID = 140005370177280, number = 20
可以看到,运行结果不再出错。
三. 死锁
所谓死锁,就是由于锁的使用不当,导致所有线程搜被阻塞,并且线程的阻塞无法解开,程序就此宕机。
1. 死锁场景
-
加锁后忘记解锁。
-
重复加锁。
-
程序中有多个共享资源,因此有多把锁,线程对齐任意加锁,可能导致死锁。例如:
有两个共享资源:x、y。 线程A对x加锁,想要申请资源y。同时线程B对y加锁,想要申请资源x。此时死锁发生。
2. 解决方法
- 避免多次锁定。
- 对共享资源加锁后,一定要记得解锁。
- 如果程序中有多把锁,要对加锁顺序进行规定,
但有些情况下无法找到绝对不产生死锁的顺序
。 - 如果程序中有多把锁,也可以在对其它互斥锁进行加锁之前,先释放本线程所拥有的锁。
- 项目程序引入专门用于死锁检测的模块。
四. 读写锁
1. 引入
在多个线程对临界资源进行操作时,有三种情况:
- 全部都在读临界资源。
- 一部分读临界资源,一部分修改临界资源。
- 全部都在修改临界资源。
这其中,2、3两种情况会产生同步问题,可能导致临界资源异常,但1这种情况是不会产生异常的。那么,我们可不可以将锁的类型进行划分,如果当前临界资源上的是读锁,那么其他线程想读该临界资源时也可以读,但不能写;如果当前临界资源上的是写锁,那么其他线程对该临界资源即不可以读,也不可以写。这样不就能将多线程的效率进一步提高了吗?读写锁应运而生。
2. 函数
linux中读写锁类型为pthread_rwlock_t
pthread_rwlock_t
这把锁记录信息如下:
- 锁的状态:锁定、开启
- 锁定的操作:读操作、写操作
- 上锁的线程ID。
使用逻辑如下:
- 当这把锁被锁定且锁定的操作为读操作时,那么其余线程仍可以读其锁定的临界资源,但不能修改。也就是说:
读锁是共享的
- 当这把锁被锁定且锁定的操作为写操作时,其余线程不能读也不能写其锁定的临界资源。也就是说:
写锁是独占的,且写的优先级大于读的优先级
使用场景:
当所有线程对临界资源的操作都是读时,读写锁和互斥锁的效率没有区别。而当有写的操作时,读写锁就有了优势,且写的操作越多,读写锁的优势越大,效率越高。
2.1 初始化
#include <pthread.h>
pthread_rwlock_t rwlock;
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
- 参数:
- rwlock:读写锁变量的地址。
- attr:读写锁的属性,一般使用默认属性,即NULL。
2.2 销毁读写锁
#include <pthread.h>
pthread_rwlock_t rwlock;
// 释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
- 参数:
- rwlock:读写锁变量的地址。
2.3 加锁
阻塞式加读锁:
// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
- 参数:
- rwlock:读写锁变量的地址。
非阻塞式加读锁
// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
- 参数:
- rwlock:读写锁变量的地址。
阻塞式加写锁:
// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
- 参数:
- rwlock:读写锁变量的地址。
非阻塞式加写锁:
// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
- 参数:
- rwlock:读写锁变量的地址。
2.4 释放锁
// 解锁,不论是读锁还是写锁,都使用该函数释放
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
- 参数:
- rwlock:读写锁变量的地址。
3. 实例
题目:使用八个线程操作一个变量,其中五个读,一个写。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 全局变量,是共享资源
int number = 0;
// 读写锁
pthread_rwlock_t rwlock;
// 子线程读操作
void *rd_num(void *arg){
while (1){
pthread_rwlock_rdlock(&rwlock);
printf("read operation, thread id = %ld, number = %d\n", pthread_self(), number);
pthread_rwlock_unlock(&rwlock);
sleep(rand()%5); // 5m内随机取一个整数
}
return NULL;
}
// 子线程写操作
void *wr_num(void *arg){
while(1) {
pthread_rwlock_wrlock(&rwlock);
int var = number;
var++;
number = var;
printf("write operation, thread id = %ld, number = %d\n", pthread_self(), number);
pthread_rwlock_unlock(&rwlock);
sleep(rand()%5);
}
return NULL;
}
int main(int argc, const char * argv[]){
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 子线程,其中3个写,5个读
pthread_t rtid[5];
pthread_t wtid[3];
// 子线程初始化
for (int i = 0; i < 5; ++i){
pthread_create(&rtid[i], NULL, rd_num, NULL);
}
for (int i = 0; i < 3; ++i){
pthread_create(&wtid[i], NULL, wr_num, NULL);
}
// 释放子线程
for (int i = 0; i < 5; ++i){
pthread_join(rtid[i], NULL);
}
for (int i = 0; i < 3; ++i){
pthread_join(wtid[i], NULL);
}
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
五. 条件变量
1. 概述
作用:
条件变量的作用在于根据一定条件来批量阻塞线程
,他无法实现线程同步,因此在多线程中往往与互斥锁配合使用。
与互斥锁的区别:
- 互斥锁:一次只通过一个线程,阻塞其余所有线程。
- 条件变量:只要不满足条件,一次会阻塞N个线程。如果满足条件,N个线程都可以通过。
使用场景:
一般用于生产者——消费者
模型,并且和互斥锁配合使用。
2. 函数
2.1 变量
pthread_cond_t
#include <pthread.h>
pthread_cond_t cond;
被条件变量阻塞的线程的线程信息会记录在这个变量中,以便于在解除阻塞时使用。
2.2 初始化
#include <pthread.h>
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
- 参数:
- cond:条件变量的地址
- attr:条件变量的属性,一般默认为NULL。
2.3 销毁
// 销毁释放资源
int pthread_cond_destroy(pthread_cond_t *cond);
- 参数:
- cond:条件变量的地址
2.4 阻塞函数
// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
-
参数:
- cond:条件变量的地址。
- mutex:互斥锁变量的地址。
-
功能:
- 在对线程进行阻塞时,如果线程已经对
mutex
上锁,那么会将互斥锁打开,从而避免死锁。 - 当线程解除阻塞时,会尝试对
mutex
上锁,如果上锁成功,则可以访问临界区,否则继续阻塞。
- 在对线程进行阻塞时,如果线程已经对
2.5 阻塞指定时间
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
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:一个结构体,用于表示到什么时候解除阻塞。
-
注意:在使用时,如果
timespec
中只是用一项,那么一定要将另一项设为0,否则会影响结果。time_t mytim = time(NULL); // 1970.1.1 0:0:0 到当前的总秒数 struct timespec tmsp; tmsp.tv_nsec = 0; tmsp.tv_sec = time(NULL) + 100; // 线程阻塞100s
2.6 解除阻塞
至少解除一个被阻塞的线程:
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
解除所有被阻塞的线程:
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);
3. 实例
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include "list.h"
// 互斥锁
pthread_mutex_t mutex;
// 条件变量
pthread_cond_t empty;
pthread_cond_t full;
// 链表最长
int MAX = 50;
// 链表头节点
Node *head = NULL;
// 消费者线程工作
void *consume(void *arg){
while (1) {
pthread_mutex_lock(&mutex);
while (list_length(head) == 0){
list_destroy(&head);
pthread_cond_wait(&empty, &mutex);
}
int data = list_get(head);
printf("consumer get data: %d\n", data);
// 消费了产品,因此要提醒生产者生产
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&full);
sleep(rand()%5);
}
return NULL;
}
// 生产者线程工作
void *produce(void *arg){
while (1) {
pthread_mutex_lock(&mutex);
// 判断是否已经满了
// 这里必须使用while,不能用if,if会报错。
while (list_length(head) == MAX){
pthread_cond_wait(&full, &mutex);
}
if(head == NULL){
list_init(&head);
}
int data = rand()%100;
printf("producer make data %d\n", data);
list_insert(head, data);
// 生产了产品,因此要提醒消费者消费
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&empty);
sleep(rand()%3);
}
return NULL;
}
int main(int argc, const char * argv[]){
// 初始化互斥锁、条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&empty, NULL);
pthread_cond_init(&full, NULL);
// 子线程,其中5个消费,5个生产
pthread_t p_tid[5];
pthread_t c_tid[5];
// 子线程初始化
for (int i = 0; i < 5; ++i){
pthread_create(&p_tid[i], NULL, produce, NULL);
pthread_create(&c_tid[i], NULL, consume, NULL);
}
// 释放子线程
for (int i = 0; i < 5; ++i){
pthread_join(p_tid[i], NULL);
pthread_join(c_tid[i], NULL);
}
// 销毁互斥锁、条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&empty);
pthread_cond_destroy(&full);
return 0;
}
注意:
关于条件变量的判断,必须使用while
而不是if
,这是由于如果使用if的话,我们以生产者为例,
- 假设当前生产者线程A获取了互斥锁,但此时商品已经满了,条件变量会对其进行阻塞(阻塞在
pthread_cond_wait()
处),锁也随之释放。 - 假设此时生产者线程B再抢到了互斥锁,但此时商品仍是满的,条件变量也会对其进行阻塞(阻塞在
pthread_cond_wait()
处),锁也随之释放。 - 假设此时消费者线程X抢到了锁,消费了产品,并释放了锁,同时通过
pthread_cong_signal()
释放了至少一个被条件变量阻塞的生产者线程。 - 假设此时A和B都被唤醒,A、B开始抢锁,B没抢到,继续阻塞(阻塞在
pthread_cond_wait()
处)。A抢到了,由于是if判断,无需再次进行判断,因此直接向下执行,生产一个商品,商品又满了。此时A释放锁。 - A、B继续抢锁(A通过
pthread_mutex_lock()
抢锁,B通过pthread_cong_wati()
抢锁),结果B抢到了,此时商品虽然已经满了,但由于对于条件变量的判断是if,此时已经判断过了,因此无需判断,直接执行,B又生产了一份。商品溢出了。如果是while循环,那么B抢到锁后会再次判断,结果发现当前商品仍旧是满的,于是继续阻塞在pthread_cong_wati()
处,同时释放锁,自然不会再生产了。
六. 信号量
1. 概述
信号量类似于观察者模式
,一个线程完成某个任务后,可以通过信号量通知另一个线程,让它继续运行或是阻塞,从而实现对线程状态的控制。
此外,信号量所关注的并不仅仅是临界资源,也包括一些其他操作,比如说输入输出等。信号量主要用于阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要配合互斥锁,一起使用。
信号量和条件变量一样用于“生产者——消费者”模型。
2. 信号量函数
2.1 头文件和信号类型
#include <semaphore.h> // 头文件
sem_t sem; // 信号类型
2.2 操作函数
2.2.1 初始化
#include <semaphore.h>
// 信号量初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 参数:
- sem:信号量变量地址。
- pshared:
- 0:线程同步
- 非0:进程同步。
- value:初始化当前信号量拥有的资源数(资源数>=0)。如果资源数=0,线程会被阻塞。
2.2.2 销毁
#include <semaphore.h>
// 信号量资源释放
int sem_destroy(sem_t *sem);
- 参数:
- sem:信号量变量地址。
2.2.3 调用信号量
#include <semaphore.h>
int sem_wait(sem_t *sem);
- 参数:
- sem:信号量变量地址。
- 效果:当sem资源数>0,时本线程抢占一个资源,sem资源--,本线程正常运行。如果sem资源数==0,本线程阻塞。
注意:和条件变量不同,调用信号量阻塞线程不会释放锁
。因此和互斥锁搭配使用时必须先抢占信号量,然后再抢占互斥锁,否则会造成死锁。
2.2.4 尝试调用信号量
#include <semaphore.h>
int sem_trywait(sem_t *sem);
- 参数:
- sem:信号量变量地址。
- 效果:当sem资源数>0,时本线程抢占一个资源,sem资源--,本线程正常运行。如果sem资源数==0,本线程不会阻塞,该函数会返回一个错误号,因此可以在程序中添加判断分支,用于处理获取资源失败后的情况。
2.2.5 指定时长
#include <semaphore.h>
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
// 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。
// abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
- 效果:当sem资源数>0,时本线程抢占一个资源,sem资源--,本线程正常运行。如果sem资源数==0,本线程阻塞指定时长,如果阻塞期间抢到资源了,那么阻塞也会结束。
2.2.6 新增资源
#include <semaphore.h>
int sem_post(sem_t *sem);
- 效果:令sem中的资源数量++,如果有线程因为sem资源不足阻塞,那么它们会开始抢资源,谁抢到谁向下运行,抢不到继续阻塞。
2.2.7 查看资源数
#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);
- 作用:传出参数。通过
sval
,传出信号量sem当前所拥有的资源数量。
3. 生产者、消费者
3.1 分析
生产者和消费者是两类线程,运行逻辑不一样,因此需要定义两个信号量:
- 生产者信号量:信号量当中的资源象征着同时有多少个生产者可以生产,因此根据需要初始化,如初始化为5,意味着有5个生产者可以参与生产。
- 消费者信号量:资源初始化为0,生产者线程生产资源,从而唤醒消费者线程。
3.2 实例
3.2.1 总资源数为1
当生产者信号量资源+消费者信号量资源==1,此时不论有多少线程,同一时间只有一个线程可以访问任务队列,因此无需使用互斥锁维护临界资源。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <semaphore.h>
#include <pthread.h>
// 链表的节点
struct Node
{
int number;
struct Node* next;
};
// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;
// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;
// 生产者的回调函数
void* producer(void* arg)
{
// 一直生产
while(1)
{
// 生产者拿一个信号灯
sem_wait(&psem);
// 创建一个链表的新节点
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
// 节点初始化
pnew->number = rand() % 1000;
// 节点的连接, 添加到链表的头部, 新节点就新的头结点
pnew->next = head;
// head指针前移
head = pnew;
printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());
// 通知消费者消费, 给消费者加信号灯
sem_post(&csem);
// 生产慢一点
sleep(rand() % 3);
}
return NULL;
}
// 消费者的回调函数
void* consumer(void* arg)
{
while(1)
{
sem_wait(&csem);
// 取出链表的头结点, 将其删除
struct Node* pnode = head;
printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
head = pnode->next;
free(pnode);
// 通知生产者生成, 给生产者加信号灯
sem_post(&psem);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
// 初始化信号量
// 生产者和消费者拥有的信号灯的总和为1
sem_init(&psem, 0, 1); // 生成者线程一共有1个信号灯
sem_init(&csem, 0, 0); // 消费者线程一共有0个信号灯
// 创建5个生产者, 5个消费者
pthread_t ptid[5];
pthread_t ctid[5];
for(int i=0; i<5; ++i)
{
pthread_create(&ptid[i], NULL, producer, NULL);
}
for(int i=0; i<5; ++i)
{
pthread_create(&ctid[i], NULL, consumer, NULL);
}
// 释放资源
for(int i=0; i<5; ++i)
{
pthread_join(ptid[i], NULL);
}
for(int i=0; i<5; ++i)
{
pthread_join(ctid[i], NULL);
}
sem_destroy(&psem);
sem_destroy(&csem);
return 0;
}
3.2.2 多个资源
当生产者信号量资源+消费者信号量资源>1,此时说明存在多个线程同时访问临界资源的情况,因此需要配合互斥锁,实现线程同步。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <semaphore.h>
#include <pthread.h>
// 链表的节点
struct Node
{
int number;
struct Node* next;
};
// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;
// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;
// 生产者的回调函数
void* producer(void* arg)
{
// 一直生产
while(1)
{
// 生产者拿一个信号灯
sem_wait(&psem);
// 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁
pthread_mutex_lock(&mutex);
// 创建一个链表的新节点
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
// 节点初始化
pnew->number = rand() % 1000;
// 节点的连接, 添加到链表的头部, 新节点就新的头结点
pnew->next = head;
// head指针前移
head = pnew;
printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());
pthread_mutex_unlock(&mutex);
// 通知消费者消费
sem_post(&csem);
// 生产慢一点
sleep(rand() % 3);
}
return NULL;
}
// 消费者的回调函数
void* consumer(void* arg)
{
while(1)
{
sem_wait(&csem);
pthread_mutex_lock(&mutex);
struct Node* pnode = head;
printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
head = pnode->next;
// 取出链表的头结点, 将其删除
free(pnode);
pthread_mutex_unlock(&mutex);
// 通知生产者生成, 给生产者加信号灯
sem_post(&psem);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
// 初始化信号量
sem_init(&psem, 0, 5); // 生成者线程一共有5个信号灯
sem_init(&csem, 0, 0); // 消费者线程一共有0个信号灯
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建5个生产者, 5个消费者
pthread_t ptid[5];
pthread_t ctid[5];
for(int i=0; i<5; ++i)
{
pthread_create(&ptid[i], NULL, producer, NULL);
}
for(int i=0; i<5; ++i)
{
pthread_create(&ctid[i], NULL, consumer, NULL);
}
// 释放资源
for(int i=0; i<5; ++i)
{
pthread_join(ptid[i], NULL);
}
for(int i=0; i<5; ++i)
{
pthread_join(ctid[i], NULL);
}
sem_destroy(&psem);
sem_destroy(&csem);
pthread_mutex_destroy(&mutex);
return 0;
}
注意:
不论是消费者还是生产者,必须先抢到信号量资源,然后才能去抢锁:
// 消费者
sem_wait(&csem);
pthread_mutex_lock(&mutex);
// 生产者
sem_wait(&csem);
pthread_mutex_lock(&mutex);
如果顺序颠倒,可能会造成死锁,这是因为信号量阻塞线程时不会释放该线程抢占的锁资源,从而导致死锁。例如:
- 程序启动时,消费者先抢到了任务队列的锁。
- 此时消费者资源数为0,因此信号量阻塞了消费者线程,但互斥锁没有释放。
- 此时生产者无法抢到互斥锁,线程阻塞,也就无法生产新商品。
- 没有新商品,消费者持续阻塞。死锁出现了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理