线程相关知识
线程的概念
线程的概念
- 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程
- 一个进程中是可以有多个线程
- 多个线程共享同一个进程的所有资源,每个线程参与操作系统的统一调度
- 可以简单理解成 进程 = 内存资源 + 主线程 + 子线 + .......
1.2 线程与进程
- 线程与进程区别
- 内存空间
- 一个进程中多个线程共享同一个内存空间多个进程拥有独立的内存空间
- 内存空间
- 进程/线程间通讯
- 线程间通讯方式简单
- 进程间通讯方式复杂
- 联系比较紧密选择多线程,不紧密选择多进程
1.3 线程资源
- 共享进程的资源
- 同一块地址空间
- 文件描述符表
- 每种信号的处理方式(如: SIG_DFL,SIG IGN或者自定义的信号优先级)
- 当前工作目录
- 用户id和组id
- 独立的资源
- 线程栈
- 每个线程都有私有的上下文信息
- 线程ID
- 寄存的值
- errno变量
- 信号屏蔽字以及调度优先级
线程相关命令
线程命令介绍
在Linux 系统有很多命令可以查看进程,包括 pidstat 、top 、ps 可以查看进程,也可以查看一个进程下的线程
pidstat命令
- -t : 显示指定进程所关联的线程
- -p: 指定进程 pid
top命令
- top 命令查看某一个进程下的线程,需要用到-H 选项在结合-p 指定 pid
- -H :Threads-mode operation0
ps命令
- ps 命令结合 -T 选项就可以查看某个线程
线程创建
并发方案
- 在并发方案中,一般会有两种选择
- 多进程方案
- 优点:
- 进程地址空间独立,一旦某一个进程出现异常,不会影响到其他进程
- 缺点:
- 每个进程都需要分配独立的空间,需要占用更多的内存
- 进程间协同时,进程间通讯比较复杂
- 适用场景
- 多个任务联系不是非常紧密,比如类似远程升级
- 多个任务联系不是非常紧密,比如类似远程升级
- 优点:
- 多线程方案
- 优点:
- 同一个进程的多个线程可以共享资源解决内存空间
- 线程间通讯相对比较简单
- 缺点
- 线程没有独立的进程地址空间,一个线程出现问题会影响到其他线程
- 使用场景
- 多个任务联系比较紧密时,建议使用多线程,如处理网络请求等
- 多个任务联系比较紧密时,建议使用多线程,如处理网络请求等
- 优点:
- 多进程方案
线程创建
创建线程调用 pthread_create 函数
- 函数头文件
- #include <pthread.h>
- 函数原型
- int pthread_create(pthread t *thread, const pthread_attr_t *attr,void (start_routine)(void *), void *arg);
- 函数功能
- 创建一个子线程
- 函数参数
- thread: 线程 ID 变量指针
- attr : 线程属性,默认属性可设置为 NULL
- start_routine: 线程执行函数指针
- arg: 线程执行函数的参数
函数返回值
- 成功 : 返回 0
- 失败 : 返回 错误码
注意
- 一旦子线程创建成功,则会被独立调度执行,并且与其他线程 并发执行
- 在编译时需要链接-lpthread
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
void *do_thread(void *args){
printf("Thread start.\n");
}
int main(void){
int err;
pthread_t tid = 0;
err = pthread_create(&tid,NULL,do_thread,NULL);
if(err != 0){
fprintf(stderr,"[ERROR] pthread_create <%s> \n",strerror(err));
exit(EXIT_FAILURE);
}
fprintf(stdout,"tid = %ld\n",tid);
sleep(2);
return 0;
}
- 问题
- 上述程序执行的结果只打印了 tid,子线程没有执行
- 原因 :
- 子线程还没有来得及执行,主线程已经执行结束,导致所有其他子线程都必须要结束
- 解决:
- 保证主线程不先于子线程结束
安装 线程帮助文档
- Linux 下关于线程的帮助文档需要单独安装,如果没有安装则需要按照下列命令进行安装
- sudo apt-get install manpages-posix-dev
- sudo apt-get install glibc-doc
创建两个子线程,并打印两个子线程的 tid
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
void *do_thread(void *args){
printf("Thread start.\n");
}
int main(void){
int err;
pthread_t tid = 0;
pthread_t tid2 = 0;
err = pthread_create(&tid,NULL,do_thread,NULL);
if(err != 0){
fprintf(stderr,"[ERROR] pthread_create <%s> \n",strerror(err));
exit(EXIT_FAILURE);
}
err = pthread_create(&tid2,NULL,do_thread,NULL);
if(err != 0){
fprintf(stderr,"[ERROR] pthread_create <%s> \n",strerror(err));
}
fprintf(stdout,"tid = %ld, tid2 = %ld \n",tid,tid2);
sleep(2);
return 0;
}
线程退出
线程退出使用 pthread_exit 函数
- 函数头文件
- #include <pthread.h>
- 函数原型
- void pthread_exit(void *retval):
- 函数功能
- 让线程退出,并返回值
- 函数参数
- retval: 线程返回值,通过指针传递
- 函数返回值
- 成功: 返回0
- 失败 : 返回 -1
注意:
- 1.当主线程调用 pthread exit 函数时,进程不会结束,也不会导致其他子线程退出
- 2.任何线程调用 exit 函数会让进程结束
线程等待
- 主线程需要等待子线程退出,并释放子线程资源
- 线程等待调用 pthread_join 函数
- 函数头文件
- #include <pthread.h>
- 函数原型
- int pthread_join(pthread_t thread, void **retval)
- 函数功能
- 等待子进程退出,并释放子线程资源
- 函数参数
- thread :线程 ID
- retval : 获取线程退出值的指针
- 函数返回值
- 成功 : 返回 0
- 失败 : 返回 错误码
- 示例
- 创建一个线程,主线程等待子线程退出
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
void *do_thread(void *args){
printf("Thread start.\n");
pthread_exit((void*)1);
}
int main(void){
int err;
pthread_t tid = 0;
pthread_t tid2 = 0;
err = pthread_create(&tid,NULL,do_thread,NULL);
if(err != 0){
fprintf(stderr,"[ERROR] pthread_create <%s> \n",strerror(err));
exit(EXIT_FAILURE);
}
err = pthread_create(&tid2,NULL,do_thread,NULL);
if(err != 0){
fprintf(stderr,"[ERROR] pthread_create <%s> \n",strerror(err));
}
fprintf(stdout,"tid = %ld, tid2 = %ld \n",tid,tid2);
pthread_join(tid,NULL);
pthread_join(tid2,NULL);
return 0;
}
线程分离
- 线程分为可结合的与可分离的
- 可结合
- 可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。
- 线程创建的默认状态为 可结合的,可以由其他线程调用 thread_join 函数等待子线程退出并释放相关资源
- 可分离
- 不能被其他线程回收或者杀死的,该线程的资源在它终止时由系统来
- 可结合
- 线程分离调用 pthread_detach 函数
- 函数头文件
- #include <pthread.h>
- 函数原型
- int pthread_detach(pthread_t thread):
- 函数功能
- 设置在线程退出后,由操作系统自动释放该线程的资源
- 函数参数
- thread : 线程 ID
- 函数返回值
- 成功:返回 0
- 失败 : 返回 -1
- 注意
- 线程分离函数不会阻塞线程的执行
- 示例
- 创建一个线程,并设置线程为可分离状态
线程间通信
主线程向子线程传递参数
- 主线程给子线程传参方式如下:
- 通过 pthread_create 函数的第4个参数 arg 进行传递即可
- 主线程传递一个整型变量给子线程
#include<stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
void *do_thread(void *arg){
int num = *(int *)arg;
printf("num = %d\n",num);
printf("thread start\n");
pthread_exit(NULL);
}
int main(void){
pthread_t tid;
int err;
int num = 100;
err = pthread_create(&tid,NULL,do_thread,&num);
if(err != 0){
perror("[ERROR] pthread_create");
exit(EXIT_FAILURE);
}
pthread_join(tid,NULL);
return 0;
}
- 设计一个结构 struct perosn ,包含名字与年龄,在主线程中定义结构体变量,并传递到子线程进行打印
#include<stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
struct person{
char name[50];
int age;
};
void *do_thread(void *arg){
struct person p = *(struct person *)arg;
printf("name = <%s>,age = %d\n",p.name,p.age);
printf("thread start\n");
pthread_exit(NULL);
}
int main(void){
pthread_t tid;
int err;
struct person p;
strcpy(p.name,"xiaoming");
p.age = 100;
err = pthread_create(&tid,NULL,do_thread,&p);
if(err != 0){
perror("[ERROR] pthread_create");
exit(EXIT_FAILURE);
}
pthread_join(tid,NULL);
return 0;
}
子线程给主线程传递参数
- 子线程给主线程传参的方式如下:
- 在子线程将需要返回的值存储在 pthread_exit 函数中的 retval 参数中
- 在主线程中通过 pthread_join 函数的第2个参 retval 得到返回,pthread_join 函数会将程的返回值(指针)保存到 retval 中
#include<stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
void *do_thread(void *arg){
static int num = 0;
num = *(int *)arg;
printf("num = <%d>\n",num);
printf("Child thread start\n");
pthread_exit((void*)&num);
}
int main(void){
int num = 1000;
int err;
pthread_t tid;
void* ret_num;
err = pthread_create(&tid,NULL,do_thread,(void*)&num);
if(err != 0){
perror("[ERROR] pthread_create");
exit(EXIT_FAILURE);
}
pthread_join(tid,&ret_num);
printf("ret = %d\n",*(int *)ret_num);
return 0;
}
线程互斥锁
线程互斥锁
- 线程的主要优势在于,能够通过全局变量来共享信息,不过这种便捷的共享是有代价的:
- 必须确保多个线程不会同时修改同一变量
- 某一线程不会读取正由其他线程修改的变量,实际就是 不能让两个线程同时对临界区进行访问
- 线程互斥锁则可以用于解决多线程资源竞争问题
- 创建两个子线程,定义一个全局变量 global = 0,子线程对这个全局变量进行加1
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
static int global = 0;
void *do_thread(void *args){
int loops = *(int*)args;
int i,tmp=0;
for(i = 0;i < loops;i ++){
global ++;
// printf("%d\n",global);
}
pthread_exit(NULL);
}
int main(int argc, char *argv[]){
int err, i = 0;
pthread_t tid[2] = {0};
int loops = 0;
if(argc != 2){
fprintf(stderr,"Usage: <%s> <count loops> \n",argv[0]);
exit(EXIT_FAILURE);
}
loops = atoi(argv[1]);
for(i = 0; i < 2; i++){
err = pthread_create(&tid[i],NULL,do_thread,&loops);
if(err != 0){
perror("[ERROR] pthread_create");
exit(EXIT_FAILURE);
}
}
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
printf("global = %d\n",global);
return 0;
}
线程互斥锁的初始化
- 线程互斥锁的初始化方式主要分为两种
- 静态初始化
- 定义 pthread_mutex_t 类型的变量,然后对其初始化为 PTHREAD_MUTEX_INITIALIZER
- pthread mutex t mtx = PTHREAD MUTEX INITIALIZER
- 动态初始化
- 动态初始化主要涉及两个函数 pthread_mutex_init 函数 与 pthread_mutex_destroy 函数
- pthread_mutex_init 函数
- 函数头文件
- #include <pthread.h>
- 函数原型
- int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
- 函数功能
- 初始化线程互斥锁
- 函数参数
- mutex: 线程互斥锁对象指针
- atr : 线程互斥锁属性
- 函数返回值
- 成功 :返回 0
- 失败 : 返回 错误编码
- 函数头文件
- pthread_mutex_destroy 函数
- 函数头文件
- #include <pthread.h>
- 函数原型
- int pthread_mutex_destroy(pthread mutex t *mutex)
- 函数功能
- 销毁线程互斥锁
- 函数参数
- mutex: 线程互斥锁指针
- 函数返回值
- 成功:返回0
- 失败 : 返回 错误编码
- 函数头文件
- 静态初始化
线程互斥锁操作
- 线程互斥锁的操作主要分为 获取锁(lock) 与 释放锁(unlock),具体函数描述如下
- pthread_mutex_lock
- 函数头文件
- #include <pthread.h>
- 函数原型
- int pthread_mutex_lock(pthread_mutex_t *mutex):
- 函数功能
- 将互斥锁进行锁定,如果已经锁定,则阻塞线程
- 函数参数
- mutex: 线程互斥锁指针
- 函数返回值
- 成功 :返回0
- 失败 : 返回 错误码
- 函数头文件
- pthread_mutex_lock
- pthread_mutex_unlock
- 函数头文件
- #include <pthread.h>
- 函数原型
- int pthread_mutex_unlock(pthread_mutex_t *mutex):
- 函数功能
- 解除互斥锁锁定状态,解除后,所有线程可以重新竞争锁
- 函数参数
- mutex : 线程互斥锁对象的指针
- 函数返回值
- 成功 :返回 0
- 失败 : 返回 错误码
- 函数头文件
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
static int global = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *do_thread(void *args){
int loops = *(int*)args;
int i,tmp=0;
for(i = 0;i < loops;i ++){
pthread_mutex_lock(&mutex);
global ++;
pthread_mutex_unlock(&mutex);
// printf("%d\n",global);
}
pthread_exit(NULL);
}
int main(int argc, char *argv[]){
int err, i = 0;
pthread_t tid[2] = {0};
int loops = 0;
if(argc != 2){
fprintf(stderr,"Usage: <%s> <count loops> \n",argv[0]);
exit(EXIT_FAILURE);
}
loops = atoi(argv[1]);
for(i = 0; i < 2; i++){
err = pthread_create(&tid[i],NULL,do_thread,&loops);
if(err != 0){
perror("[ERROR] pthread_create");
exit(EXIT_FAILURE);
}
}
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
printf("global = %d\n",global);
return 0;
}
线程同步
线程同步概念
- 线程同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对 资源的有序访问
- 条件变量 : 线程库提供的专门针对线程同步的机制 类似于信号量
- 线程同步比较典型的应用场合就是 生产者与消费者
生产者与消费者模型原理
- 在这个模型中,分为 生产者线程 与 消费者线程,通过这个线程来模拟多个线程同步的过程
- 在这个模型中需要以下组件:
- 仓库:用于存储产品,一般作为共享资源生产者线程:用于生产产品
- 消费者线程:用于消费产品
- 当仓库没有产品时,则消费者线程需要等待,直到有产品时才能消费
- 当仓库已经装满产品时,则生产者线程需要等待,直到消费者线程消费产品之后
生产者与消费者模型同步
- 示例
- 基于互斥锁实现生产者与消费者模型主线程为消费者
- n 个子线程作为生产者
原理
- 当仓库没有产品时,则消费者线程需要等待,直到有产品时才能消费
- 当仓库已经装满产品时,则生产者线程需要等待,直到消费者线程消费产品之后
#include<stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
static int product = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *do_task(void *arg){
int val = *(int*)arg;
while(1){
pthread_mutex_lock(&mutex);
fprintf(stdout,"product add <%d>\n",val);
product += val;
pthread_mutex_unlock(&mutex);
}
pthread_exit(NULL);
}
int main(){
int err[3] = {0};
pthread_t tid[3] = {0};
for(int i = 0;i < 3;i ++){
int num = i + 1;
err[i] = pthread_create(&tid[i],NULL,do_task,(void*)&num);
if(err[i] != 0){
perror("[ERROR] pthread_create");
exit(EXIT_FAILURE);
}
}
for(;;){
pthread_mutex_lock(&mutex);
if(product > 0){
printf("custmer minus <%d>\n",1);
product -= 1;
}
pthread_mutex_unlock(&mutex);
if(product == 0){
sleep(1);
}
}
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
pthread_join(tid[2],NULL);
return 0;
}
条件变量
条件变量介绍
- 不足:
- 主线程(消费者线程)需要不断查询是否有产品可以消费,如果没有产品可以消费,也在运行程序,包括获得互斥锁、判断条件、释放互斥锁,非常消耗 cpu 资源
- 条件变量 允许一个线程就某个共享变量的状态变化通知其他线程,并让其他线程等待这一通知
- 条件变量初始化
- 条件变量的本质为 pthread_cond_t 类型的变量,其他线程可以阻塞在这个条件变量上,或者唤醒阻塞在这个条件变量上的线程
- 条件变量的初始化分为 静态初始化 与动态初始化
- 静态初始化
pthread_cond_t cond = PTHREAD_COND_INITALIZER; - 动态初始化
- pthread_cond_init 函数
- 函数头文件
- #include <pthread.h>
- 函数原型
- int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
- 函数功能初
- 始化条件变量
- 函数参数
- cond : 条件变量指针
- attr : 条件变量属性
- 函数返回值
- 成功: 返回 0
- 失败 : 返回错误码
- 函数头文件
- pthread_cond_destroy
- 函数头文件
- #include <pthread.h>
- 函数原型
- int pthread_cond_destroy(pthread_cond_t *cond);
- 函数功能
- 销毁条件变量
- 函数参数
- cond :条件变量指针
- 函数返回值
- 成功: 返回 0
- 失败 : 返回错误码
- 函数头文件
- pthread_cond_init 函数
- 静态初始化
条件变量原理
-
原理分析
- 基于条件变量的阻塞与唤醒具体的原理如下图:
- 基于条件变量的阻塞与唤醒具体的原理如下图:
-
step 1: 消费者线程判断消费条件是否满足(仓库是否有产品),如果有产品可以消费,则可以正常消费产品,然后解锁
-
step 2: 当条件不能满足时(仓库产品数量为0),则调用 pthread_cond_wait 函数,这个函数具体做的事情如下
- 在线程睡眠之前,对互斥锁解锁
- 让线程进入到睡眠状态
- 等待条件变量收到信号时,该函数重新竞争锁,并获取锁后,函数返回
-
step 3:重新判断条件是否满足,如果不满足,则继续调用 pthread_cond_wait 函数
-
step 4:唤醒后,从 pthread_cond_wait 返回,消费条件满足,则正常消费产品
-
step 5 : 释放锁,整个过程结束
-
问题 1: 为什么条件变量需要与互斥锁结合起来使用?解答:
- 防止在调用 pthread_cond_wait 函数等待一个条件变量收到醒信号,另外一个线程发送信号在第一个线程实际等待它之前
- 线程还没有完全进入到睡眠状态,其他线程发送唤醒信号
-
问题2:在判断条件时,为什么需要使用 while(number == 0)而不是 if(解答:
- 防止虚假唤醒
- 能够唤醒的情况如下:
- 被信号唤醒,并非由条件满足而唤醒
- 条件变量状态改变时,一次唤醒多个线程,但是被其他线程先消费完产品,等到当前线程执行时,条件已经不满足
- 下面是官方帮助文档的解释如下:
- 能够唤醒的情况如下:
- 防止虚假唤醒
pthread_cond_wait
- 函数头文件
- #include <pthread.h>
- 函数原型
- int pthread_cond_wait(pthread cond_t *restrict cond, pthread_mutex_t *restrict mutex):
- 函数功能
- 阻塞线程,等待唤醒
- 函数参数
- cond : 条件变量指针
- mutex: 关联互斥锁指针
- 函数返回值
- 成功 : 返回 0
- 失败 : 返回错误码
注意:
- 条件变量需要与互斥锁结合使用,先获得锁才能进行条件变量的操作
- 调用函数后会释放锁,并阻塞线程
- 一旦线程唤醒,需要重新竞争锁,重新获得锁之后, pthread_cond_wait 函数返回0
pthread_cond_broadcast 与 pthread_condsignal
- pthread_cond_broadcast
- 函数头文件
- #include <pthread.h>
- 函数原型
- int pthread_cond_broadcast(pthread_cond_t *cond);
- 函数功能
- 唤醒所有阻塞在某个条件变量上的线程
- 函数参数
- cond : 条件变量指针
- 函数返回值
- 成功: 返回 0
- 失败: 返回 错误码
- 函数头文件
- pthread_cond_signal
- 函数头文件
- #include <pthread.h>
- 函数原型
- int pthread_cond_signal(pthread_cond_t *cond):
- 函数功能
- 唤醒所有阻塞在某个条件变量上的线程
- 函数返回值
- 成功: 返回 0
- 失败: 返回 错误码
- 函数头文件
注意
- pthread_cond_signal 函数主要 适用等待线程都在执行完全相同的任务
- pthread_cond_broadcast 函数 主要适用等待线程都执行不相同的任务
- 条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制,发送信号时若无任何线程在等待该条件变量,则会被忽略
- 条件变量代表是一个通讯机制,用于传递通知与等待通知,用户可以设定条件来发送或者等待通知
#include<stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <pthread.h>
#include <unistd.h>
static int number = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void *thread_handler(void *arg){
int cnt = atoi((char *)arg);
int i,tmp;
for(i = 0;i < cnt;i++){
pthread_mutex_lock(&mtx);
printf("线程[%ld]生产一个产品,产品数量为%d\n",pthread_self(),++number);
pthread_mutex_unlock(&mtx);
pthread_cond_signal(&cond);
}
pthread_exit((void*)0);
}
int main(int argc,char *argv[]){
pthread_t tid;
int err;
int total_of_product = 0;
int total_of_consume = 0;
bool done = false;
for(int i = 0;i < argc;i++){
total_of_product += atoi(argv[1]);
err = pthread_create(&tid,NULL,thread_handler,(void*)argv[i]);
if(err != 0 ){
perror("[ERROR] pthread_create()");
exit(EXIT_FAILURE);
}
}
for(;;){
pthread_mutex_lock(&mtx);
while(number == 0)
pthread_cond_wait(&cond,&mtx);
while(number > 0){
total_of_consume ++;
printf("消费一个产品,产品数量为%d\n",--number);
done = total_of_consume >= total_of_product;
}
pthread_mutex_unlock(&mtx);
if(done)
break;
}
}
- 函数唤醒所有阻塞的消费者线程
- 实现多个生产者与多个消费者模型,在示例的基础上进行修改,提示,需要使用 pthread cond _broadcast