线程同步
文章目录
1.1线程同步概念
假设有 4 个线程 A、B、C、D,当前一个线程 A 对内存中的
共享资源
进行访问的时候,其他线程 B, C, D 都不可以对这块内存进行操作,直到线程 A 对这块内存访问完毕为止,B,C,D 中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。
1.1.1 为什么需要线程同步
设计两个线程交替数数(每个线程数10个数,交替数到20):
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/types.h>
#define MAX 10
//全局变量
int number;
//线程处理函数
void* funcA_num(void* arg)
{
for (int i =0;i<MAX;i++)
{
int cur = number;
cur++;
usleep(10);
number =cur;
printf("Thread A , ID =%ld ,number =%d\n",pthread_self(),cur);
}
return NULL;
}
void* funcB_num(void* arg)
{
for (int i =0;i<MAX;i++)
{
int cur = number;
cur++;
number =cur;
printf("Thread B , ID =%ld ,number =%d\n",pthread_self(),cur);
}
return NULL;
}
int main()
{
pthread_t p1,p2;
pthread_create(&p1,NULL,funcA_num,NULL);
pthread_create(&p2,NULL,funcB_num,NULL);
//阻塞,资源回收
pthread_join(p1,NULL);
pthread_join(p2,NULL);
return 0;
}
liu@liu-Ubuntu:~/vscode$ gcc tong.c -lpthread -o tong
liu@liu-Ubuntu:~/vscode$ ./tong
Thread A , ID =140377753675520 ,number =1
Thread A , ID =140377753675520 ,number =3
Thread A , ID =140377753675520 ,number =4
Thread A , ID =140377753675520 ,number =5
Thread A , ID =140377753675520 ,number =6
Thread A , ID =140377753675520 ,number =7
Thread A , ID =140377753675520 ,number =8
Thread A , ID =140377753675520 ,number =9
Thread A , ID =140377753675520 ,number =10
Thread A , ID =140377753675520 ,number =11
Thread B , ID =140377745282816 ,number =2
Thread B , ID =140377745282816 ,number =12
Thread B , ID =140377745282816 ,number =13
Thread B , ID =140377745282816 ,number =14
Thread B , ID =140377745282816 ,number =15
Thread B , ID =140377745282816 ,number =16
Thread B , ID =140377745282816 ,number =16
Thread B , ID =140377745282816 ,number =17
Thread B , ID =140377745282816 ,number =18
Thread B , ID =140377745282816 ,number =19
Thread B , ID =140377745282816 ,number =20
可以看出虽然每个线程内部循环了 10 次每次数一个数,,通过输出的结果可以看到,有些数字被重复数了多次,其原因就是没有对线程进行同步处理,造成了数据的混乱。
两个线程在数数的时候分时复用时间片,且其中一个线程调用了sleep()函数导致CPU时间片没用完就被迫挂起了 ,这样就能让CPU的上下文切换 (保存当前状态,下次继续运行的时候需要加载的状态)更加频繁的出现,更容易出现数据混乱的现象;
CPU对应的寄存器、一级缓存、二级缓存、三级缓存是独占的,用于存储处理的数据和线程的状态信息,数据被 CPU处理完成需要再次写入物理内存中,物理内存数据也可以通过文件IO操作写入磁盘中;
在测试程序中两个线程共用全局变量 number 当线程变成运行态之后开始数数,从物理内存加载数据,让后将数据放到 CPU 进行运算,最后将结果更新到物理内存中。如果数数的两个线程都可以顺利完成这个流程,那么得到的结果肯定是正确的。
如果线程 A 执行这个过程期间就失去了 CPU 时间片,线程 A 被挂起了最新的数据没能更新到物理内存。线程 B 变成运行态之后从物理内存读数据,很显然它没有拿到最新数据,只能基于旧的数据往后数,然后失去 CPU 时间片挂起。线程 A 得到 CPU 时间片变成运行态,第一件事儿就是将上次没更新到内存的数据更新到内存,但是这样会导致线程 B 已经更新到内存的数据被覆盖,活儿白干了,最终导致有些数据会被重复数很多次。
1.1.2 同步方式
为了放置多线程访问共享资源出现数据混乱的问题,需要进行线程同步。共享资源通常为全局区资源或堆区变量 ,这些变量对应的共享资源被称为临界资源;
常用的线程同步的四种方式: 互斥锁、读写锁、条件变量、信号量
找到临界资源后,再找临界资源相关的上下文代码,得到一个代码块该代码块称为临界区;
临界区越小越好 ,在确定号临界区之后,开始进行线程同步;
- 在临界区代码的上面,添加加锁函数,对临界区加锁。
- 无论那个线程调用这句代码,就会把锁锁上,其他线程只能阻塞在锁上;
- 在临界区代码的下边,添加解锁函数,对临界区解锁。
- 出临界区的线程会将锁定的那把锁打开,其他抢到锁的线程就可以进入临界区了;
通过锁机制能保证临界区代码最多只能同时又一个线程访问,于是并行访问便成了串行访问;
1.2 互斥锁
1.2.1 互斥锁函数
互斥锁是线程同步最常用的一种方式, 通过互斥锁可以锁定一个代码块,
被锁定的代码块所有线程只能顺序执行(不能并行处理)
,但是执行效率较低,因为默认临界区多个线程可以并行处理,现在只能串行处理;
在Linux中互斥锁的类型为 pthread_mutex_t
,创建一个该类型对象就得到一把互斥锁:
pthread_mutex_t mutex;
创建的锁对象保存了当前这把锁的状态信息: 锁是否锁定,如果是锁定状态,还会记录下给这把锁加锁的线程信息(线程ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程在对互斥锁变量加锁会被阻塞,直到互斥锁被解锁 ,被阻塞的线程才被解除阻塞;一般情况下,每一个共享资源对应一把互斥锁,锁的个数和线程的个数无关。
Linux提供互斥锁操作函数,如果函数调用成功返回 0 , 调用失败会返回错误号;
//初始化互斥锁
//restrict: 用于修饰指针的关键字,有了该关键字修饰的指针可以访问指向的内存地址;
int pthread_mutex_init(pthread_mutex_t *restrict mutex ,const pthread_mutexattr_t *restrict attr);
//释放互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
- mutex: 互斥锁变量的地址;
- attr: 互斥锁的属性 , 一般默认 NULL
//修改互斥锁的状态,将其**设定为锁定状态** ,该参数被写入到 参数mutex中
int pthread_mutex_lock(pthread_mutex_t *mutex);
该函数被调用,会先判断参数mutex互斥锁中状态是否为锁定状态;
- 没有被锁定,该线程会被加锁成功,且这个
锁对象会记录那个线程加锁成功;
- 如果被锁定了,其他线程加锁失败,
这些线程会阻塞在这把锁上;
- 当把这把锁解开之后,这些阻塞在锁上的线程就解除阻塞了,并通过竞争对这把锁加锁,没抢到锁的线程继续阻塞;
//尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
该函数被调用对互斥锁有两种情况:
- 如果这把锁没有被锁定 , 线程加锁成功;
- 如果锁变量被锁定,调用该函数加锁的线程不会被阻塞,加锁失败直接返回错误号;
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
不是所有的线程都可以对互斥锁解锁,哪个线程加的锁,哪个线程才能解锁成功。
1.2.2 互斥锁使用
将上面多线程交替计数的例子修改一下,使用互斥锁进行线程同步,两个线程共同操作同一个全局变量:
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/types.h>
#define MAX 10
//全局变量
int number;
//全局互斥锁
pthread_mutex_t mutex;
//线程处理函数
void* funcA_num(void* arg)
{
for (int i =0;i<MAX;++i)
{
//如果线程A加锁成功 ,不阻塞
//如果线程B加锁成功 ,不阻塞
pthread_mutex_trylock(&mutex);
int cur = number;
cur++;
number =cur;
//解锁
pthread_mutex_unlock(&mutex);
printf("Thread A , ID =%ld ,number =%d\n",pthread_self(),number);
}
return NULL;
}
void* funcB_num(void* arg)
{
for (int i =0;i<MAX;++i)
{
//如果线程A加锁成功 ,不阻塞
//如果线程B加锁成功 ,不阻塞
pthread_mutex_trylock(&mutex);
int cur = number;
cur++;
number =cur;
pthread_mutex_unlock(&mutex);
printf("Thread B , ID =%ld ,number =%d\n",pthread_self(),number);
}
return NULL;
}
int main()
{
pthread_t p1,p2;
//初始化互斥锁
pthread_mutex_init(&mutex, NULL);
pthread_create(&p1,NULL,funcA_num,NULL);
pthread_create(&p2,NULL,funcB_num,NULL);
//阻塞,资源回收
pthread_join(p1,NULL);
pthread_join(p2,NULL);
// 销毁互斥锁
// 线程销毁之后, 再去释放互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
1.3 死锁
当多个线程访问共享资源,需要加锁,如果使用锁不当会造成死锁现象 。
死锁的后果:
所有线程都被阻塞,且线程的阻塞无法解开(可以解锁的线程也被阻塞了)。
造成死锁的场景:
- 加锁后忘记解锁:
// 场景1
void func()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功, 当前循环完毕没有解锁, 在下一轮循环的时候自己被阻塞了
// 其余的线程也被阻塞
pthread_mutex_lock(&mutex);
....
.....
// 忘记解锁
}
}
// 场景2
void func()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功
// 其余的线程被阻塞
pthread_mutex_lock(&mutex);
....
.....
if(xxx)
{
// 函数退出, 没有解锁(解锁函数无法被执行了)
return ;
}
pthread_mutex_lock(&mutex);
}
}
- 反复加锁,造成死锁
void func()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功
// 其余的线程阻塞
pthread_mutex_lock(&mutex);
// 锁被锁住了, A线程阻塞
pthread_mutex_lock(&mutex);
....
.....
pthread_mutex_unlock(&mutex);
}
}
// 隐藏的比较深的情况
void funcA()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功
// 其余的线程阻塞
pthread_mutex_lock(&mutex);
....
.....
pthread_mutex_unlock(&mutex);
}
}
void funcB()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功
// 其余的线程阻塞
pthread_mutex_lock(&mutex);
funcA(); // 重复加锁
....
.....
pthread_mutex_unlock(&mutex);
}
}
- 在程序中有多个共享资源,会存在多把锁,如果随意加锁,导致互相被阻塞:
场景描述:
1. 有两个共享资源:X ,Y 其中X对应锁A ,Y对应锁B
--- 线程A访问资源X ,加锁A
--- 线程B访问资源Y ,加锁B
2. 线程A要访问资源Y,线程B要访问资源X,因为资源X和Y已经被对应的锁锁住了,因此这两个线程被阻塞
--- 线程A被锁B阻塞 ,无法打开A锁
--- 线程B被锁A阻塞 ,无法打开B锁
使用多线程编程的时候,如何避免死锁:
- 避免多次锁定,多检测;
- 对共享资源访问完毕之后,一定要解锁 ,或在加锁使用的时候使用 trylock;
- 如果程序中有多把锁,可以控制对锁的访问顺序(顺序访问有时无法实现),或可以在对其他互斥锁加锁之钱,先释放当前线程拥有的互斥锁;
- 项目中引入专门用于检测死锁的模块
1.4 读写锁
1.4.1 读写锁函数
读写锁事互斥锁的升级版,
在读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作,那么读是并行的,但是使用互斥锁,读操作是串行的。
读写锁类型为 :pthread_rwlock_t
:
pthread_rwlock_t rwlock;
该锁既可锁住读操作,也能锁定写操作。在该锁中记录了以下信息:
- 锁的状态 :锁定 / 打开
- 锁定的是什么操作:读操作 / 写操作 ,
使用读写锁锁定了读操作,需要先解锁才能去锁定写操作,反之同理;
- 那个线程讲锁锁上了;
读写锁的特点:
- 使用读锁 锁定临界区,线程对临界区的访问是并行的 ,
读锁时共享的;
- 使用写锁 锁定临界区,线程对临界区的访问是串行的 ,
写锁是独占的;
- 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,
因为写锁比读锁的优先级高;
如果说程序中所有的线程都对共享资源做写操作,使用读写锁没有优势,和互斥锁是一样的,如果说程序中所有的线程都对共享资源有写也有读操作,并且对共享资源读的操作越多,读写锁更有优势。
Linux系统下读写锁操作函数原型如下,函数调用成功返回 0, 失败返回错误号;
#include <pthread.h>
pthread_rwlock_t rwlock;
//初始化读写锁
pthread_rwlock_init(pthread_rwlock_t *restrict rwlock ,const pthread_rwlockattr_t *restrict attr);
//释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数:
- rwlock: 读写锁的地址,传出参数;
- attr: 读写锁属性,默认使用 NULL;
//对读写锁加读锁 ,锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
调用该函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用该函数依然可以加锁成功(因为读锁是共享的);如果读写锁锁定了写操作,调用这个函数的线程会被阻塞。
//此函数可以避免死锁
//如果读写锁失败,不会阻塞当前线程,直接返回错误号
int pthread_rwlock_tryrdlock(pthread_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);
1.4.2 读写锁使用
示例 6个线程操作同一个全局变量,3个线程不定时写同一全局资源 ,3个线程不定时读同一个全局资源
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
//全局变量
int number =0;
//定义读写锁
pthread_rwlock_t rwlock;
//线程处理函数
void* writeNum(void* arg)
{
while (1)
{
//加写锁
pthread_rwlock_trywrlock(&rwlock);
int cur =number;
cur++;
number =cur;
printf("写操作完毕 ,number: %d ,tid =%ld \n",number,pthread_self());
pthread_rwlock_unlock(&rwlock);
//添加sleep() ,为了看到线程交替工作
usleep(rand()%50);
}
return NULL;
}
//多线程如果处理动作相同 ,可以使用相同的处理函数
//每个线程中的栈资源是独享的
void* readNUm(void* arg)
{
while (1)
{
//加读锁
pthread_rwlock_rdlock(&rwlock);
printf("全局变量number =%d ,tid =%ld \n",number ,pthread_self());
//解读锁
pthread_rwlock_unlock(&rwlock);
usleep(rand()%50);
}
return NULL;
}
int main()
{
//初始化读写锁
pthread_rwlock_init(&rwlock,NULL);
// 3个写线程, 3个读的线程
pthread_t wtid[3];
pthread_t rtid[3];
for(int i=0; i<3; ++i)
{
pthread_create(&wtid[i], NULL, writeNum, NULL);
}
for(int i=0; i<3; ++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<3; ++i)
{
pthread_join(rtid[i], NULL);
}
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
1.5 条件变量
1.5.1 条件变量函数
条件变量的主要作用不是处理线程同步,而是进行线程的阻塞
。在多线程中使用条件变量无法实现线程同步,必须要配合互斥锁使用 。
条件变量和互斥锁都能阻塞线程 ,但是二者的区别:
- 假设有A-Z 26个线程,这26个线程共同访问同一把互斥锁,如果线程A加锁成功,那么其余25个线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区 ;
条件变量只有在满足指定条件下才会阻塞线程
,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,此情况下可能会出现数据混乱;
一般情况下条件变量用于处理生产消费者模型 ,并配合互斥锁使用 。 条件变量类型对应的类型为:pthread_cond_t
;
pthread_cond_t cond;
被条件变量阻塞的线程的线程信息会被记录到这个变量中 ,以便在解除阻塞的时候使用。
#include <pthread.h>
pthread_cond_t cond;
//初始化
pthread_cond_init(pthread_cond_t *restrict cond ,const pthread_condattr *restrict attr);
//释放资源
pthread_cond_destroy(pthread_cond_t *cond);
参数:
-
cond: 条件变量的地址;
-
attr: 条件变量属性 ,默认 NULL;
//线程阻塞函数 ,那个线程调用这个函数,该线程会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond , pthread_mutex_t *restrict mutex);
该函数调用需要互斥锁,互斥锁进行线程同步,让线程顺序进入临界区避免出现共享资源的数据混乱;
该函数会对互斥锁做以下事:
- 在阻塞线程时,如果线程已经对
互斥锁mutex
上锁,那就会将锁打开避免死锁; - 当线程解除阻塞的时候,函数内部会帮助线程再次将
mutex互斥锁锁上
,继续访问临界区;
// 表示的时间是从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);
该函数前两个参数和 pthread_cond_wait
函数是一样的,第三个函数表示线程阻塞的时长 ;注意:struct_timespec 这个结构体
中记录的时间是从1971.1.1到某个时间点的时间,总长度使用时间秒表示 。赋值相对麻烦一点。
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
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);
调用以上两个函数的任意一个,唤醒 被 pthread_cond_wait
或 pthread_cond_timedwait
阻塞的线程,区别在与 pthread_cond_sognal
唤醒一个被阻塞的线程 ,pthread_cond_broadcast
唤醒所有被阻塞的线程;
1.5.2 生产者和消费者模型
生产者和消费者模型的组成:
-
若干个生产者线程:
— 生产商品(任务)放到任务队列中;
— 任务队列满了就会阻塞 ,不满的时候就继续工作;
— 通过一个生产者的条件变量控制生产者线程阻塞和非阻塞;
-
若干个消费者线程:
— 读任务队列 ,将任务取出;
— 任务队列中有数据就消费,没有数据就阻塞;
— 通过一个消费者条件变量控制消费者线程阻塞和非阻塞;
-
任务队列(存储任务/数据),为了读写访问可以通过一个数据结构维护这块内存:
— 数组、链表,或者使用STL容器: queue/stack/list/vector;
场景描述: 使用条件变量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节
1.6 信号量
在使用信号量进行多线程同步时,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,例如:又有A 、B两个线程,B线程要等A线程完成某一任务之后再进行自己的步骤,这个任务不一定是锁定某一个资源,还要进行一些计算或数据处理;
信号量(信号灯)
与互斥锁和条件变量的主要不同在于**“灯”的概念** ,灯亮意味资源可用,灯灭则意味着不可用。信号量只要用来阻塞线程,不能保证线程安全,如需要保证线程安全需要信号量和互斥锁一起使用。
1.6.1 信号量函数
信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程和消费者线程的运行。信号的类型为
sem_t
对应的头文件<semaphore.h>;
#include <semaphore>
sem_t sem;
Linux系统提供的信号量操作函数原型:
#include <semaphore>
//初始化信号量
int sem_init(sem_t *sem ,int pshared ,unsigned int value);
//资源释放 ,线程销毁后再调用此函数
int sem_destroy(sem_t *sem);
参数:
sem: 信号量变量地址;
pshared: 0 :线程同步 ; 非0 :进程同步;
value: 初始化当前信号量拥有的资源数(>=0),当资源数为0 ,线程被阻塞;
当线程调用此函数,且
sem
中的资源数>0 ,线程不会阻塞,线程会占用sem
中的一个资源,因此资源-1,直到sem
中的资源数减为0时 ,线程被阻塞;
//函数被调用sem中的资源就会消耗1 ,资源数-1
int sem_wait(sem_t* sem);
当线程调用此函数,且
sem
中的资源数>0 ,线程不会阻塞,线程会占用sem
中的一个资源,因此资源-1,直到sem
中的资源数减为0时 ,资源被耗尽但是线程不会被阻塞,直接返回错误号 ,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况;
//函数被调用sem中的资源数被消耗1 ,资源数-1
int sem_trywait(sem_t* sem);
// 表示的时间是从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);
该函数的参数 abs_timeout
和 pthread_cond_timedwait
的最后一个参数是一样的,使用方法不再过多赘述。当线程调用这个函数,并且 sem
中的资源数 >0
,线程不会阻塞 ,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem
中的资源数减为 0
时,资源被耗尽,线程被阻塞 ,当阻塞指定的时长之后,线程解除阻塞。
调用此函数将sem中的资源数+1 ,如果有线程在调用
sem_wait 、sem_trywait、sem_timedwait
时因为sem
中的资源数为0
被阻塞了,此时这些线程会解除阻塞 ,获取资源之后继续向下运行
//调用此函数给sem中资源数+1
int sem_post(sem_t* sem);
此函数可以查看
sem
中现有拥有的资源数 ,通过第二个参数sval
将数据传出 ;第二参数和返回值一样。
//查看信号量 sem中的整形数的当前值,这个值会被写入sval指针对应的内存中
int sem_getvalue(sem_t *sem ,int *sval);
1.6.2 生产者和消费者
由于生产者和消费者是两类线程,且在还没有产生任务之前不能消费。在使用信号量处理此类问题的时候可以定义两个信号量,分别用于记录生产者和消费者线程拥有的总资源数;
// 生产者线程
sem_t psem;
// 消费者线程
sem_t csem;
// 信号量初始化
sem_init(&psem, 0, 5); // 5个生产者可以同时生产
sem_init(&csem, 0, 0); // 消费者线程没有资源, 因此不能消费
// 生产者线程
// 在生产之前, 从信号量中取出一个资源
sem_wait(&psem);
// 生产者商品代码, 有商品了, 放到任务队列
......
......
......
// 通知消费者消费,给消费者信号量添加资源,让消费者解除阻塞
sem_post(&csem);
// 消费者线程
// 消费者需要等待生产, 默认启动之后应该阻塞
sem_wait(&csem);
// 开始消费
......
......
......
// 消费完成, 通过生产者生产,给生产者信号量添加资源
sem_post(&psem);
始化信号量的时候没有消费者分配资源,消费者线程启动之后由于没有资源自然就被阻塞了,等生产者生产出产品之后,再给消费者分配资源,这样二者就可以配合着完成生产和消费流程了。
1.6.3信号量使用
场景描述:使用信号量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。
- 总资源数为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;
}
注意:如果生产者和消费者使用的信号量总资源数为 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);
// 加锁, 这句代码放到 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;
}