线程概念
线程是进程中的⼀个执行单元,负责当前进程中程序的执行,⼀个进程中至少有⼀个线程
⼀个进程中是可以有多个线程
多个线程共享同一个进程的资源,每个线程参与操作系统的统一调度
进程相当于是由进程资源+主线程+子线程,组合而来
程序由进程进行执行,进程由线程执行
线程与进程区别
内存空间
一个进程中多个线程共享同一个内存空间
多个进程拥有独立的内存空间
进程/线程间通讯
线程间通讯方式简单
进程间通讯方式复杂
并发操作,线程比进程更节约资源
联系紧密的任务在并发时优先选择多线程,如果任务之间比较独立,在并发时建议选择多进程
线程资源分配
共享进程的资源:
同一块地址空间
文件描述符表
每种信号的处理方式(如:SIG_DFL,SIG_IGN或者自定义的信号优先级)
当前工作目录
用户id和组id
独立的资源:
线程栈
每个线程都有私有的上下文信息。
线程ID
寄存器的值
errno变量
信号屏蔽字以及调度优先级
线程相关指令
在 Linux 系统有很多命令可以查看进程,例如 pidstat 、top 、ps ,也可以查看一个进程下的线程
1.pidstat
选项
-t:显示指定进程所关联的线程
-p:指定进程pid
2.top 命令
top 命令查看某一个进程下的线程,需要用到 -H 选项在结合 -p 指定 pid
3.ps命令
ps 命令结合 -T 选项就可以查看某个进程下所有线程
线程的创建
创建线程调用 pthread_create 函数
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *
(*start_routine) (void *), void *arg);
thread:线程ID变量指针
attr:线程属性,默认属性可设置为NULL
start_routine:线程执行函数
arg:线程执行函数的参数
一旦子线程创建成功,则会被独立调度执行,并且与其他线程并发执行
pthread_t thread_id_a,thread_id_b;
int result = pthread_create(&thread_id_a,NULL,do_thread_funA,NULL);
注意:在编译时需要链接 -lpthread[Compile and link with -pthread]
线程的退出
线程退出使用 pthread_exit 函数
函数原型 void pthread_exit(void *retval);
retval:线程返回值,通过指针传递
1.当主线程调用pthread_exit函数时,进程不会结束,也不会导致其他子线程退出
2. 任何线程调用exit函数会让进程结束
线程的等待
主线程需要等待子线程退出,并释放子线程资源
线程等待调用 pthread_join函数,会阻塞调用线程
int pthread_join(pthread_t thread, void **retval);
thread:线程 ID
retval:获取线程退出值的指针
// 线程执行函数
void* do_thread_function(void* args)
{
printf("do thread ....\n");
pthread_exit(NULL);
}
int main()
{
pthread_t thread_id;
int result = pthread_create(&thread_id,NULL,do_thread_function,NULL);
if(result!=0)
{
fprintf(stderr,"pthread error:%s\n",strerror(result));
exit(EXIT_FAILURE);
}
printf("thread id is %ld\n",thread_id);
pthread_join(thread_id,NULL);
return 0;
}
线程分离
线程分为可结合的与可分离的
可结合
可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源
(如栈)是不释放的。
线程创建的默认状态为可结合的,可以由其他线程调用 pthread_join函数等待子线程退出并
释放相关资源
可分离
不能被其他线程回收或者杀死的,该线程的资源在它终止时由系统来释放
pthread_detach 函数
int pthread_detach(pthread_t thread);
设置在线程退出后,由操作系统自动释放该线程的资源
注意:线程分离函数不会阻塞线程的执行
多个线程的创建
创建多个线程时,一般由主线程统一创建,并等待释放资源或者分离线程,不要递归创建
1. 多个线程如果任务相同,则可以使用同一个线程执行函数
2. 多个线程如果任务不同,则可以使用不同的线程执行函数
// 线程执行函数
void* do_thread_funA(void* args)
{
printf("do thread A\n");
pthread_exit(NULL);
}
void* do_thread_funB(void* args)
{
printf("do thread b\n");
pthread_exit(NULL);
}
线程间通信
为什么需要线程通信?
线程是操作系统调度的最小单元,有自己的栈空间,可以按照既定的代码逐步的执行,但是如果每个线程间都孤立的运行,那就会造资源浪费。所以在现实中,我们需要这些线程间可以按照指定的规则共同完成一件任务,所以这些线程之间就需要互相协调,这个过程被称为线程的通信。
线程通信就是当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺。
主线程向子线程传递
还记得pthread_create函数么
可以传很多不止int,如果你想传多种数据,可以创建结构体,传指针就行了
子线程向主线程传递
在子线程将需要返回的值存储在 pthread_exit 函数中的 retval 参数中
在主线程中通过 pthread_join 函数的第2个参数 retval 得到返回, pthread_join 函数会将线程的
返回值(指针)保存到 retval 中
线程同步
线程同步:是指在互斥的基础上,通过其它机制实现访问者对 资源的有序访问。
条件变量:线程库提供的专门针对线程同步的机制
线程同步比较典型的应用场合就是 生产者与消费者
线程互斥锁
线程的主要优势在于能够通过全局变量来共享信息,不过这种便捷的共享是有代价的:
1.必须确保多个线程不会同时修改同一变量
2.某一线程不会读取正由其他线程修改的变量,实际就是 不能让两个线程同时对临界区进行访问
3.线程互斥锁则可以用于解决多线程资源竞争问题
举个例子
int global = 0;
//线程执行函数
void* do_thread(void* argv)
{
//循环对global进行加1操作
int loops = *(int*)argv;
for(int i=0;i<loops;i++)
{
int temp = global;
temp++;
global=temp;
}
pthread_exit(NULL);
}
int main(int argc,char* argv[])
{
if(argc !=2)
{
fprintf(stderr,"arguments must be 2:< cmd > <count>\n");
exit(EXIT_FAILURE);
}
// 获取循环次数
int loopCount = atoi(argv[1]);
// 循环的方式创建两个线程
pthread_t tids[2]={0};
int err;
for(int i=0;i<2;i++)
{
err = pthread_create(tids+i,NULL,do_thread,&loopCount);
if(err!=0)
{
fprintf(stderr,"pthread_create
failed:%s\n",strerror(err));
exit(EXIT_FAILURE);
}
}
pthread_join(tids[0],NULL);
pthread_join(tids[1],NULL);
// 打印全局变量
printf("global=%d\n",global);
return 0;
}
这里需要将数字字符串转换为整数 :int atoi(const char *nptr);
当循环多次后
线程之间资源的竞争会导致其出错
这时就要用到互斥锁
1. 线程互斥锁工作机制
当线程A获得锁,另外一个线程B在获得锁时则会阻塞,直到线程A释放锁,线程B才会获得锁。
2. 线程互斥锁工作原理
本质上是一个pthread_mutex_t类型的变量,假设名为 v
当 v = 1,则表示当前临界资源可以竞争访问 ,得到互斥锁的线程则可以访问,此时 v = 0
当 v = 0,则表示临界资源正在被某个线程访问,其他线程则需要等待
3. 线程互斥锁的特点
互斥锁是一个 pthread_mutex_t类型的变量,就代表一个互斥锁
如果两个线程访问的是同一个 pthread_mutex_t 变量,那么它们访问了同一个互斥锁
对应的变量定义在 pthreadtypes.h 头文件中,是一个共用体中包含一个结构体
互斥锁的初始化
线程互斥锁的初始化方式主要分为两种
1. 静态初始化
定义 pthread_mutex_t 类型的变量,然后对其初始化为 PTHREAD_MUTEX_INITIALIZER
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER
2. 动态初始化
动态初始化主要涉及两个函数 pthread_mutex_init 函数 与 pthread_mutex_destroy 函数
pthread_mutex_init
函数原型
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
mutex:线程互斥锁对象指针
attr:线程互斥锁属性
pthread_mutex_destroy
函数原型
int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex:线程互斥锁指针
互斥锁的操作
线程互斥锁的操作主要分为 获取锁(lock) 与 释放锁(unlock)
获取锁的函数:pthread_mutex_lock
int pthread_mutex_lock(pthread_mutex_t *mutex);
mutex:线程互斥锁指针
释放锁的函数:pthread_mutex_unlock
int pthread_mutex_unlock(pthread_mutex_t *mutex);
解除互斥锁锁定状态,解除后,所有线程可以重新竞争锁
mutex:线程互斥锁对象的指针
静态:
// 互斥锁静态初始化
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
//线程执行函数
void* do_thread(void* argv)
{
//循环对global进行加1操作
int loops = *(int*)argv;
for(int i=0;i<loops;i++)
{
// 获取锁,一旦获取成功,则获取临界资源的访问资格,否则会阻塞当前线程
pthread_mutex_lock(&mtx);
int temp = global;
temp++;
global=temp;
// 释放锁
pthread_mutex_unlock(&mtx);
}
pthread_exit(NULL);
}
动态:
// 互斥锁变量
pthread_mutex_t mtx;
int main(int argc,char* argv[])
{
if(argc !=2)
{
fprintf(stderr,"arguments must be 2:< cmd > <count>\n");
exit(EXIT_FAILURE);
}
pthread_mutex_init(&mtx,NULL);
// 获取循环次数
int loopCount = atoi(argv[1]);
// 循环的方式创建两个线程
pthread_t tids[2]={0};
int err;
for(int i=0;i<2;i++)
{
err = pthread_create(tids+i,NULL,do_thread,&loopCount);
if(err!=0)
{
fprintf(stderr,"pthread_create
failed:%s\n",strerror(err));
exit(EXIT_FAILURE);
}
}
pthread_join(tids[0],NULL);
pthread_join(tids[1],NULL);
pthread_mutex_destroy(&mtx);
return 0;
}
生产者消费者模型
1. 在这个模型中,包括生产者线程与消费者线程。通过线程来模拟多个线程同步的过程
2. 在这个模型中,需要以下组件
仓库 : 用于存储产品,一般作为共享资源
生产者线程 : 用于生产产品
消费者线程 : 用于消费产品
3. 原理
当仓库没有产品时,则消费者线程需要等待,直到有产品时才能消费
当仓库已经装满产品时,则生产者线程需要等待,直到消费者线程消费产品之后
生产者与消费者模型同步
基于互斥锁实现生产者与消费者模型实现
// 静态互斥锁
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static int product_number=0;
void* thread_handle(void* arg)
{
// 生产产品的数量
int cnt = atoi((char*)arg);
for(int i=1;i<=cnt;i++)
{
// 获取锁
pthread_mutex_lock(&mutex);
printf("thread[%ld] produces a product, the number of
products is %d.\n",pthread_self(),++product_number);
// 释放锁
pthread_mutex_unlock(&mutex);
}
pthread_exit(NULL);
}
// command format[./a.out 1 2 3],1 2 3表示生产者线程生产的产品数量
int main(int argc,char* argv[])
{
int total_of_production= 0;// 生产产品的数量
int total_of_consumption=0;// 消费产品的数量
if(argc < 2)
{
fprintf(stderr,"command argument: ./a.out <...product
quantity>\n");
exit(EXIT_FAILURE);
}
pthread_t tid;
for(int i=1;i<argc;i++)
{
total_of_production += atoi(argv[i]);
int err = pthread_create(&tid,NULL,thread_handle,
(void*)argv[i]);
if(err > 0)
{
fprintf(stderr,"pthread_create
error:%s\b",strerror(err));
exit(EXIT_FAILURE);
}
}
// 消费产品
while(1)
{
pthread_mutex_lock(&mutex);
while(product_number > 0)
{
printf("consumption a product ,the number of products
is %d.\n",--product_number);
total_of_consumption++;
sleep(1);
}
pthread_mutex_unlock(&mutex);
if(total_of_production == total_of_consumption)
{
break;
}
}
return 0;
}
条件变量
上面示例代码的不足:
主线程(消费者线程)需要不断查询是否有产品可以消费,如果没有产品可以消费,也在运行程序,包括获
得互斥锁、判断条件、释放互斥锁,非常消耗 cpu 资源。
使用条件变量进行修改
条件变量 允许一个线程就某个共享变量的状态变化通知其他线程,并让其他线程等待这一通知
初始条件变量
条件变量的本质为 pthread_cond_t 类型的变量,其他线程可以阻塞在这个条件变量上,或者唤醒阻塞在这个条件变量上的线程。
条件变量的初始化分为静态初始化与动态初始化(与互斥锁相似)
1. 静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
2. 动态初始化
pthread_cond_init 函数
int pthread_cond_init(pthread_cond_t *restrict cond,const
pthread_condattr_t *restrict attr);
cond:条件变量指针
attr:条件变量属性
pthread_cond_destroy函数
int pthread_cond_destroy(pthread_cond_t *cond);
1:消费者线程判断消费条件是否满足(仓库是否有产品),如果有产品则可以消费,然后解锁
2 :当条件满足时(仓库产品数量为0),则调用 pthread_cond_wait 函数,这个函数具体做的事
情如下:
在线程睡眠之前,对互斥锁解锁
让线程进入到睡眠状态
等待条件变量收到信号时,该函数重新竞争锁,并获取锁
3 :重新判断条件是否满足,如果满足,则继续调用 pthread_cond_wait 函数
4 :唤醒后,从 pthread_cond_wait 返回,条件不满足,则正常消费产品
5 :释放锁,整个过程结束
实现这个模型还需要几个函数
pthread_cond_wait
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict
mutex);
函数功能
阻塞线程,等待唤醒
函数参数
cond:条件变量指针
mutex:相关联的互斥锁指针
1.条件变量需要与互斥锁结合使用,先获得锁才能进行条件变量的操作
2.调用函数后会释放锁,并阻塞线程
3.一旦线程唤醒,需要重新竞争锁,重新获得锁之后,pthread_cond_wait 函数返回
pthread_cond_signa
int pthread_cond_signal(pthread_cond_t *cond);
函数功能
唤醒所有阻塞在某个条件变量上的线程
pthread_cond_broadcast函数
int pthread_cond_broadcast(pthread_cond_t *cond);
函数功能
唤醒所有阻塞在某个条件变量上的线程
pthread_cond_signal 函数主要适用等待线程都在执行完全相同的任务
pthread_cond_broadcast 函数主要适用等待线程都执行不相同的任务
条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制,发送信号时若无任何线
程在等待该条件变量,则会被忽略
条件变量代表是一个通讯机制,用于传递通知与等待通知,用户可以设定条件来发送或者等待通知