多线程编程
1. 线程概念
线程的理解:
- Linux内核当中没有线程的概念,只有轻量级进程(LWP),线程是C库中的概念
- 一个程序中只有一个main线程,可以有多个工作线程
- 一个线程 是执行代码的 一个执行流
- 多个线程 可以同步执行
线程的优点:
- 创建进程的耗费 高于 创建线程的耗费
进程与线程的对比:
-
进程 是资源分配的基本单位
-
线程 是调度的基本单位
-
一个进程 至少拥有 一条线程
-
线程共享进程的数据,但也拥有自己的一部分数据
-
共享数据:
文件描述符表 + 信号处理方式 + 当前工作目录 + 用户id + 组id
独有数据:
线程ID + 一组寄存器 + 调用栈 + errno + 信号屏蔽字(哪些信号该进程需要阻塞) + 调度优先级
进程与线程的现实比喻:
- 进程可以比作一个工厂
- 线程可以比作一个工厂内生产产品的生产线
- 一个工厂中生产产品的生产线可以有多条
线程与进程的组合图解:
2. 线程控制
2.1 POSIX线程库
- 线程相关的的函数构成的函数库,绝大多数函数是以
pthread_
开头的 - 要使用这些函数需要引入头文件
pthread.h
- 编译含有多线程函数的源文件时,要加上编译器命令
-lpthread
选项
2.2 线程创建
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
功能:创建一个新的线程
参数:
- pthread_t: 线程标识符,是一个输出型参数,从函数返回的
- pthread_attr_t: 设置线程的属性,attr为NULL表示使用默认属性,一般不设置线程属性,传入NULL
- start_routine : 线程启动函数指针,表示线程创建后需要执行什么函数
- arg : 传给线程启动函数start_routine的参数
返回值:成功
线程创建程序演示
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* work_thread(void* arg)
{
while(1)
{
printf("I'm a work thread!\n");
sleep(1);
}
return NULL;
}
int main()
{
pthread_t pid;
pthread_create(&pid,NULL,work_thread,NULL);
while(1)
{
printf("I'm a main thread!\n");
sleep(1);
}
return 0;
}
[gongruiyang@localhost createMulThread]$ gcc create1.c -o test.c -lpthread
[gongruiyang@localhost createMulThread]$ ./test.c
I'm a main thread!
I'm a work thread!
I'm a main thread!
I'm a work thread!
I'm a main thread!
I'm a work thread!
I'm a work thread!
I'm a main thread!
I'm a work thread!
I'm a main thread!
^C
2.3 线程终止
线程终止的方法:
- 从
线程入口函数
中return返回pthread_exit(void*)
函数,void*参数是线程退出的信息,是返回给等待线程的,可以传递也可以不传递,谁调用谁退出pthread_cancel(pthread_t)
参数是一个线程标识符,想要取消哪个线程,就传递哪个线程的标识符
测试pthread_exit程序
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* work_thread(void* arg)
{
printf("1\n");
pthread_exit(NULL);
printf("2\n");
//pthread_cancel(pthread_self());
printf("3\n");
return NULL;
}
int main()
{
pthread_t pid;
int ret_create = pthread_create(&pid,NULL,work_thread,NULL);
if(ret_create < 0)
{
perror("pthread_create");
return -1;
}
while(1)
{
printf("main_thread:%d\n",pthread_self());
sleep(1);
}
return 0;
}
[gongruiyang@localhost createMulThread]$ ./test
main_thread:1703049024
1
main_thread:1703049024
main_thread:1703049024
main_thread:1703049024
main_thread:1703049024
main_thread:1703049024
^C
- pthread_exit函数会立即退出该进程,后续代码不再执行
测试pthread_cancel函数
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* work_thread(void* arg)
{
printf("1\n");
//pthread_exit(NULL);
printf("2\n");
pthread_cancel(pthread_self());
printf("3\n");
return NULL;
}
int main()
{
pthread_t pid;
int ret_create = pthread_create(&pid,NULL,work_thread,NULL);
if(ret_create < 0)
{
perror("pthread_create");
return -1;
}
while(1)
{
printf("main_thread:%d\n",pthread_self());
sleep(1);
}
return 0;
}
[gongruiyang@localhost createMulThread]$ ./test
main_thread:2103330624
1
2
3
main_thread:2103330624
main_thread:2103330624
main_thread:2103330624
^C
- pthread_cancel函数不会立即退出指定进程,而是需要一段时间才能退出
2.4.1 退出不当僵尸进程的产生
- 主线程创建完工作线程后立即终止,导致主线程变成僵尸进程,其资源未被回收
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* work_thread_task(void* arg)
{
while(1)
{
printf("I'm a work thread : %d\n",pthread_self());
sleep(1);
}
return NULL;
}
int main()
{
pthread_t pid;
int ret_create = pthread_create(&pid,NULL,work_thread_task,NULL);
if(ret_create < 0)
{
perror("pthread_create");
return -1;
}
pthread_exit(NULL);
return 0;
}
[gongruiyang@localhost ~]$ ps -aux | grep test
gongrui+ 8516 0.0 0.0 0 0 pts/0 Zl+ 17:23 0:00 [test] <defunct>
gongrui+ 8530 0.0 0.0 112828 980 pts/1 R+ 17:24 0:00 grep --color=auto test
[gongruiyang@localhost ~]$ top -H -p 8516
top - 17:26:32 up 4:31, 3 users, load average: 0.04, 0.04, 0.05
Threads: 2 total, 0 running, 1 sleeping, 0 stopped, 1 zombie
%Cpu(s): 0.0 us, 0.7 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 3861272 total, 2015232 free, 916552 used, 929488 buff/cache
KiB Swap: 4063228 total, 4063228 free, 0 used. 2675052 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
8516 gongrui+ 20 0 0 0 0 Z 0.0 0.0 0:00.00 test
8517 gongrui+ 20 0 0 0 0 S 0.0 0.0 0:00.00 test
主进程pid为8561,进程状态为Z,即僵尸进程
工作进程pid未8517,进程状态为S,即正常运行
2.4 线程等待
- 线程在创建出来的时候,属性默认是joinable属性,意味着线程在退出的时候需要其他执行流(线程)来回收线程的资源
线程等待函数接口
int pthread_join(pthread_t thread, void **retval);
功能:若线程A调用了该函数等待B线程,A线程会阻塞,直到B线程退出后,A线程才会解除阻塞状态
参数:
- pthread_t : 线程标识符,要等待哪一个线程,就传递哪个线程的标识符
- retval : 保存的是一个常数,
线程退出方式 *retval保存的定西 return 入口函数返回值 pthread_exit函数 pthread_exit函数参数 pthread_cancel函数 PTHREAD_CANCEL宏定义
( #define PTHREAD_CANCEL (void*) -1 )返回值:成功返回0,失败返回错误码
程序演示
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define THREAD_COUNT 4
void* work_thread_task(void* arg)
{
sleep(1);
printf("work thread:%d\n",pthread_self());
//线程退出
pthread_exit(NULL);
}
int main()
{
pthread_t pid[THREAD_COUNT];
//创建线程
for(int i = 0; i < THREAD_COUNT; i++)
{
int ret_create = pthread_create(&pid[i],NULL,work_thread_task,NULL);
if(ret_create < 0)
{
perror("pthread_create");
return -1;
}
}
//阻塞在join中等待线程退出
for(int i = 0 ; i < THREAD_COUNT; i++)
pthread_join(pid[i],NULL);
while(1)
{
printf("main thread : %d\n",pthread_self());
sleep(1);
}
return 0;
}
[gongruiyang@localhost wait]$ gcc wait.c -o test -lpthread -std=gnu99
[gongruiyang@localhost wait]$ ./test
work thread:-1124186368
work thread:-1149364480
work thread:-1140971776
work thread:-1132579072
main thread : -1115846848
main thread : -1115846848
main thread : -1115846848
main thread : -1115846848
main thread : -1115846848
^C
2.5 分离线程
- 分离线程是将线程标记成已分离,其属性从joinable变成detach,对于detach属性的线程终止后,系统会自动回收其资源
分离线程函数接口
int pthread_detach(pthread_t thread);
功能:将线程标记为已分离,目的是当分离的线程终止时,其资源会自动释放,防止产生僵尸进程,防止内存泄漏
参数:
- pthread_t :需要标记分离的线程标识符
- 调用
pthread_detach函数
的位置可以是:- 在主线程中调用分离创建出来的线程,即主线程标记分离工作线程;
- 在工作线程的线程入口函数中调用,即自己标记分离自己;
- 线程分离的实质就是将
线程的属性
设置为detach
程序演示
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define THREAD_COUNT 4
void* work_thread_task(void* arg)
{
//线程分离自身
pthread_detach(pthread_self());
printf("work thread : %d\n",pthread_self());
pthread_exit(NULL);
}
int main()
{
pthread_t ptid[THREAD_COUNT];
for(int i = 0; i < THREAD_COUNT; i++)
{
int ret_create = pthread_create(&ptid[i],NULL,work_thread_task,NULL);
if(ret_create < 0)
{
perror("pthread_create");
return -1;
}
}
while(1)
{
printf("main thread : %d\n",pthread_self());
sleep(1);
}
return 0;
}
[gongruiyang@localhost detach]$ ./test
main thread : 1528891200
work thread : 1520551680
work thread : 1512158976
work thread : 1503766272
work thread : 1495373568
main thread : 1528891200
main thread : 1528891200
main thread : 1528891200
^C
2.6 pthread_t源码和pthread_attr_t源码
pthread_t源码
typedef unsigned long int pthread_t;
pthread_attr_t源码
union pthread_attr_t
{
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;
};
#ifndef __have_pthread_attr_t
typedef union pthread_attr_t pthread_attr_t;
# define __have_pthread_attr_t 1
#endif
3. 线程安全
使用一个 抢票程序 演示线程安全的概念及重要性
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
// 2个线程共同抢票
#define THREAD_COUNT 2
// 100张票
int ticket = 100;
void* work_thread_task(void* arg)
{
// 创建出来的线程只要 有票 ,就一直抢票
while(1)
{
if(ticket > 0)
{
ticket--;
printf(" %p get one! rest : %d\n",pthread_self(),ticket);
}
else
break;
}
//票没了,线程退出
return NULL;
}
int main()
{
//线程标识符
pthread_t ptid[THREAD_COUNT];
//线程创建
for(int i = 0; i < THREAD_COUNT; i++)
{
int ret_create = pthread_create(&ptid[i],NULL,work_thread_task,NULL);
if(ret_create < 0)
{
perror("pthread_create");
return -1;
}
}
//主线程 等待子线程的退出,并回收他们的资源
for(int i = 0; i < THREAD_COUNT; i++)
pthread_join(ptid[i],NULL);
return 0;
}
线程不安全的原因:同一时刻两个线程对同一个数据进行修改,导致程序结果出现二义性
详细解释:
- 假设同一个程序中有
两个线程
,分别为线程A和线程B,并且有一个int类型的全局变量
,其值为10,线程A和线程B在各自的入口函数当中都对这样的一个全局变量进行++操作
- 当线程A拥有CPU之后,对全局变量进行++操作是
非原子性
的操作,也就意味着,这个操作随时可能会被打断,假设线程A刚刚将全局变量的数值10读取到CPU的寄存器中时就被切换出去了;(程序计数器中保存着线程A下一步要执行的指令,上下文信息中保存寄存器的值,这两个东西是为了当线程A再次拥有CPU使用权的时候,还原当时线程A切换出去的线程现场使用的) - 当线程A被切换出去后,这会儿,可能线程B获得CPU时间片,对全局变量进行了++操作,此时全局变量从10变成了11,并且回写到了内存中去
- 当线程A再次拥有CPU时间片之后,恢复当时切换出去的现场,继续往下执行,由于上下文信息中保存的全局变量值仍然是10,执行完++操作后变成11,然后再写回内存中,全局变量的值仍然还是11
- 虽然,两个线程都对这个全局变量进行了++操作,但是从值上面来看,全局变量仅仅进行了一次++操作
- 这就是线程的不安全
4. 线程同步与互斥
互斥
:保证各个线程对共享资源
的独占式访问
同步
:保证各个线程对于共享资源
的访问具有合理性
- 为了控制线程对
共享资源
的独占式访问
,并且访问次序
具有合理性
,有以下三种方式实现线程同步
POSIX信号量
互斥量
条件变量
什么是对共享资源的独占式访问呢?
举一个栗子:同学们在上厕所的时候,坑位
就是一个共享资源
,同学们在上厕所时,将厕所门
关上,就是对共享资源的独占式访问
,在你上厕所的时候,别人进不来(无法访问),引申到代码中,就是相当:将对于一个变量的操作变成原子性操作
,要么操作成功,要么没操作,不存在操作一半被打断的情况
4.1 POSIX 信号量
4.1.1 信号量概念
- 信号量本质是 计数器 + PCB等待队列 + 函数接口
- 计数器:对共享资源的计数
- 当执行流获取信号量成功后,信号量当中的计数器会进行减一操作,当获取失败后,该执行流就会被放入PCB等待队列当中
- 当执行流释放信号量成功之后,信号量当中的计数器会进行加一操作
- PCB等待队列:用于存放等待信号量的线程
- 函数接口:用于操作信号量的一组函数
4.1.2 信号量保证同步互斥的原理探究
- 信号量不仅仅可以完成
线程
之间的同步与互斥,也可以完成进程
之间的同步与互斥
互斥原理
初始化信号量
后,信号量中的计数器保存的数值为1
,表示说只有一个资源可以被使用- 当执行流A想要访问共享资源时,首先获取信号量,由于计数器中的值为1,表示可以访问,执行流A获取到信号量后,计数器的值从1变成0,从而执行流A去访问共享资源
- 此时,执行流B想要去访问共享资源,执行流B首先得去获取信号量,但是信号量中的计数器中的值为0,表示无法获取该信号量,进而无法访问共享资源,因此,执行流B的PCB被放进了
PCB等待队列
当中,等待目标信号量的释放,同时信号量当中的计数器的值进行减一操作
,计数器中的值变成了-1,这里的-1表示当前还有1个执行流在等待访问共享资源
同步原理
- 当执行流想要访问
共享资源
的时候,首先
需要获取信号量 - 如果信号量中的计数器
大于0
,则表示能
够获取信号量,并且访问共享资源 - 如果信号量中的计数器
小于或等于0
,则表示不能
获取信号量,并且无法访问共享资源,该执行流被放入PCB等待队列中,同时计数器进行减一操作 - 当
释放
信号量的时候,会对信号量中的计数器进行加一操作
- 如果信号量中的计数器
大于0
,则唤醒
PCB等待队列中的线程 - 如果信号量中的计数器
小于或等于0
,则不唤醒
PCB等待队列中的线程
4.1.3 信号量相关函数
- POSIX信号量的函数的名字都是以
sem_
开头,常用的POSIX信号量函数有以下这些
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, struct timespec*abs_timeout);
sem_t
是一个信号量共用体
sem_t源码
typedef union
{
char __size[__SIZEOF_SEM_T];
long int __align;
} sem_t;
4.1.3.1 初始化信号量函数
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化一个信号量
参数:
sem
:指向被操作的信号量,传入信号量的地址pshared
:表示该信号量是用于进程间的还是用于线程间的,填入以下的值
数值 | 含义 |
---|---|
0 | 用于线程间 |
非0 | 用于进程间 |
value
:资源的个数,本质上就是计数器的值
4.1.3.2 等待信号量函数
int sem_wait(sem_t *sem);
功能:执行流调用该函数后,将会对计数器进行减一操作
-
如果说:减一操作后,计数器的值大于0,表示其他执行流仍然可以访问共享资源
-
如果说:减一操作后,计数器的值等于0,表示该执行流可以访问共享资源,其他执行流若想访问需要进入PCB等待队列
-
如果说:减一操作后,计数器的值小于0,表示当前执行流需要进入PCB等待队列,其他执行流若想访问也需要进入PCB等待队列
参数:
sem
:指向被操作的信号量,传入信号量的地址
4.1.3.3 释放信号量函数
int sem_post(sem_t *sem);
功能:执行流调用该函数后,将会对计数器进行加一操作
参数:
sem
:指向被操作的信号量,传入信号量的地址
4.1.3.4 销毁信号量函数
int sem_destroy(sem_t *sem);
功能:销毁目标信号量
参数:
sem
:指向被操作的信号量,传入信号量的地址
4.2 互斥量 -> 保证互斥性
4.2.1 互斥量概念
互斥量
是互斥锁
内部的一个计数器,这个计数器的取值只能为0或1,可以用于保护关键代码
,以确保其被独占式的访问
互斥量为1
的时候,代表线程可以获得该互斥锁
,也就意味着可以访问
到共享资源互斥量为0
的时候,代表该互斥锁已经被其他线程占用
,也就意味着除了获得互斥锁的线程之外的线程都无法访问
到共享资源- 当进入关键代码段之前,我们需要
获得互斥锁
并将关键代码段加锁
,由于该关键代码段已被互斥锁
锁上,其他线程如果没有该互斥锁,是无法访问该代码段的,只能等待锁被归还
后,再拿到该代码段的互斥锁,执行该代码段 - 当
关键代码段执行完毕
之后,需要对关键代码段进行解锁
并归还互斥锁
,以唤醒
其他等待该互斥锁的线程
4.2.2 互斥量的实现原理探究
- 前面我们说了,单纯的++操作并不是原子性操作,那么互斥量从
0变成1
和从1变成0
是如何保证原子性的呢?
为了保证互斥锁操作,大多数体系结构都提供了swap和exchange指令,该指令作用是把寄存器和内存单元的数据相交换,由于只有一条指令,只会出现执行成功和未执行两种情况,所以是原子性的
图解原理:想要获得互斥锁并加锁
- 寄存器中的值赋为0
- 将
寄存器
中的值和内存
中的值进行交换
- 若寄存器中的值为
1
,表示该锁未被
别的线程拿走,表示可以
占用该锁 - 若寄存器中的值为
0
,表示该锁已被
别的线程拿走,表示不可以
占用该锁
4.2.2 POSIX互斥锁基础API
- POSIX互斥锁的相关函数主要有以下5个
#include <pyhread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 这些函数的第一个参数mutex都是一个
pthread_mutex_t结构体
指针变量,指向要操作的目标互斥锁结构体
- 参数attr是一个
pthread_mutexattr_t结构
指针变量,指向互斥锁属性结构体
,如果填入NULL,表示使用默认属性
4.2.2.1 初始化互斥锁变量函数
4.2.2.1.1 动态初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
功能:初始化一个互斥锁结构体的属性
参数:
- pthread_mutex_t* : 一个指向
pthread_mutex_t结构体
的指针变量 - pthread_mutexattr_t* : 一个指向
pthread_mutexattr_t结构
的指针变量
返回值:成功返回0,失败返回errno
4.2.2.1.2 静态初始化
- 使用
宏PTHREAD_MUTEX_INITIALIZER
来初始化一个互斥锁结构体,实际上该宏是将互斥锁的各个字段初始化为0
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
宏定义源码:
#define PTHREAD_MUTEX_INITIALIZER \
{ { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }
4.2.2.2 加锁函数
4.2.2.2.1 阻塞加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:给 关键代码
阻塞等待加锁
参数:
- pthread_mutex_t* : 一个指向
pthread_mutex_t结构体
的指针变量
返回值:返回0表示成功;加锁失败返回errno
阻塞加锁解释:
如果mutex中互斥量的值为1,则pthread_mutex_lock函数就返回0表示加锁成功
如果mutex中互斥量的值为0,则线程
阻塞
在pthread_mutex_lock函数中,直到加锁成功
4.2.2.2.1 非阻塞加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:给 关键代码
非阻塞加锁
参数:
- pthread_mutex_t* : 一个指向
pthread_mutex_t结构体
的指针变量
返回值:返回0表示成功;加锁失败返回errno
非阻塞加锁解释:
如果mutex中互斥量的值为1,则pthread_mutex_trylock函数就
返回0
表示加锁成功如果mutex中互斥量的值为0,则pthread_mutex_trylock函数就
返回错误码EBUSY
4.2.2.2.3 带有超时时间的加锁
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);
功能:如果不能立即获得目标互斥锁,则等待abs_timeout时间,如果在等待时间内加锁成功则直接返回,如果超过等待时间则直接返回表示加锁失败
4.2.2.3 解锁函数
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:以原子操作的方式给一个互斥锁解锁,如果此时有其他线程正在等待这个互斥锁,则其他线程会获得该互斥锁
参数:
- pthread_mutex_t* : 一个指向
pthread_mutex_t结构体
的指针变量
返回值:返回0表示解锁成功;解锁失败返回errno
将互斥锁中的互斥量从0变成1,表示其他线程可以获取该互斥锁了
4.2.2.4 销毁互斥锁函数
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:将目标互斥锁销毁
参数:
- pthread_mutex_t* : 一个指向
pthread_mutex_t结构体
的指针变量
返回值:销毁成功返回0;销毁失败返回errno
4.2.3 带互斥锁的抢票程序
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define THREAD_COUNT 4 //线程数量
//定义一个互斥锁
pthread_mutex_t lock_ticket;
int ticket = 100; //票总量
void* work_thread_task(void* arg)
{
while(ticket > 0)
{
//锁上互斥锁:在开始访问临界资源的地方加锁
pthread_mutex_lock(&lock_ticket);
if(ticket > 0)
{
printf("I'm : %p! rest : %d\n",pthread_self(),ticket);
ticket--;
}
else
{
//解除互斥锁
pthread_mutex_unlock(&lock_ticket);
break;
}
//解除互斥锁
pthread_mutex_unlock(&lock_ticket);
}
//线程退出
pthread_exit(NULL);
}
int main()
{
//初始化互斥锁
pthread_mutex_init(&lock_ticket,NULL);
pthread_t ptid[THREAD_COUNT];
//创建线程
for(int i = 0; i < THREAD_COUNT; i++)
{
int ret_create = pthread_create(&ptid[i],NULL,work_thread_task,NULL);
if(ret_create < 0)
{
perror("pthread_create");
return -1;
}
}
//等待工作线程退出并回收资源
for(int i = 0 ; i < THREAD_COUNT; i++)
pthread_join(ptid[i],NULL);
//销毁互斥锁
pthread_mutex_destroy(&lock_ticket);
return 0;
}
4.3 条件变量 -> 保证同步性
- 条件变量提供了一种线程间的通知机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程
- 条件变量本质是一个PCB等待队列
4.3.1 条件变量基础API
- 条件变量的相关函数主要以下几个
#include <pthread.h>
int pthread_cond_init(pthread_cond_t * cond,pthread_condattr_t * attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_timedwait(pthread_cond_t * cond, pthread_mutex_t * mutex, struct timespec * abstime);
int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_t
是条件变量结构体pthread_condattr_t
是条件变量属性结构体PTHREAD_COND_INITIALIZER
是一个宏,用来初始化条件变量结构体的,本质是将条件变量各个字段设置为0
4.3.1.1 初始化条件变量函数
4.3.1.1.1 动态初始化
int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* attr);
功能:初始化
条件变量结构体
参数:
- pthread_cond_t : 条件变量结构体指针
- pthread_condattr_t : 条件变量属性结构体指针,常传入NULL,使用默认的属性
pthread_cond_t cond;
pthread_cond_init(&cond,NULL);
4.3.1.1.2 静态初始化
- 使用宏
PTHREAD_COND_INITIALIZER
初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
4.3.1.2 销毁条件变量函数
int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁
一个条件变量
参数:
- pthread_cond_t : 条件变量结构体指针
pthread_cond_destroy(&cond)
4.3.1.3 等待条件变量函数
int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex);
功能:将调用该函数的线程放入PCB等待队列
中
参数:
-
pthread_cond_t : 条件变量结构体指针
-
mutex : 该线程等待的互斥锁
4.3.1.4 唤醒条件变量函数
4.3.1.4.1 单个唤醒
int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒一个
等待目标条件变量的线程,至于唤醒的是哪个线程,取决于线程的优先级
和调度策略
参数:
- pthread_cond_t : 条件变量结构体指针
4.3.1.4.2 广播唤醒
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:以广播的方式
唤醒所有等待目标条件变量的线程
参数:
- pthread_cond_t : 条件变量结构体指针
4.3.2 为什么条件变量中用到了互斥锁?
- 条件变量只能保证线程的同步性,但是无法保证线程的互斥性
- 互斥锁只能保证线程的互斥性,但是无法保证线程的同步性
- 将两个一起使用,既可以保证线程的同步性又可以保证线程的互斥性
4.4 具有线程同步性与互斥性的 消费者与生产者程序
- 生产着生产一个,提醒消费者消费
- 消费者消费完毕后提醒生产者生产
- 进而,消费与生产达到平衡
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_COUNT 3
int resource = 1;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t consumer_cond = PTHREAD_COND_INITIALIZER;
pthread_cond_t producer_cond = PTHREAD_COND_INITIALIZER;
void* consumer_task(void* arg)
{
while(1)
{
pthread_mutex_lock(&lock);
while(resource <= 0)
{
pthread_cond_wait(&consumer_cond,&lock);
}
printf("consumer thread : [%p], rest resource : [%d]\n",pthread_self(),resource);
resource--;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&producer_cond);
}
return NULL;
}
void* producer_task(void* arg)
{
while(1)
{
pthread_mutex_lock(&lock);
while(resource > 0)
{
pthread_cond_wait(&producer_cond,&lock);
}
printf("producer thread : [%p], rest resource : [%d]\n",pthread_self(),resource);
resource++;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&consumer_cond);
}
return NULL;
}
int main()
{
pthread_t consumer[THREAD_COUNT],producer[THREAD_COUNT];
for(int i = 0 ; i < THREAD_COUNT; i++)
{
//创建消费者线程
int ret_cons = pthread_create(&consumer[i],NULL,consumer_task,NULL);
if(ret_cons < 0)
{
perror("pthread_create");
return -1;
}
//创建生产者线程
int ret_prod = pthread_create(&producer[i],NULL,producer_task,NULL);
if(ret_prod < 0)
{
perror("pthread_create");
return -1;
}
}
for(int i = 0 ; i < THREAD_COUNT; i++)
{
pthread_join(consumer[i],NULL);
pthread_join(producer[i],NULL);
}
pthread_cond_destroy(&consumer_cond);
pthread_cond_destroy(&producer_cond);
pthread_mutex_destroy(&lock);
return 0;
}
Q1:为什么先将线程放入PCB等待队列中后才释放锁?
A:如果先释放锁再进入PCB等待队列可能会出现以下情况:
生产者将锁释放后还没来得及进入PCB等待队列,此时,消费者已经拿到锁并消耗资源并唤醒PCB等待队列,但是由于生产者还未来得及入队,导致没有生产者被唤醒,在消费者唤醒之后,生产者再入队,此时出现,没有生产者生产,消费者无法消费 的局面
Q2:线程被唤醒后,需要做啥事?
A:从PCB等待队列当中移除出来,抢占互斥锁
情况1:拿到互斥锁,从pthread_cond_wait函数返回出来
情况2:没有抢占到互斥锁,阻塞在pthread_cond_wait函数内部