目录
线程概念
1. 线程和进程的关系
在Linux中,除了进程的概念,其实还有一个 轻量级进程(LWP) 的概念,也就是线程。(Linux操作系统内核中是没有线程概念的)
- 线程依附进程存在,没有进程就没有线程;
- 多线程的出现是为了提高进程的运行效率;
- 线程也可以称为执行流,其也在执行用户代码;
- 进程是资源分配的基本单位,线程是调度的基本单位;
2. 重识PID,认识TGID
在之前讲解 进程概念 的博客中,说到:
- 一个进程是OS通过 PCB进程控制块 来进行管理的;
- 在Linux中,PCB在底层上是通过一个名为
task_struct
的结构体组织; - 在
task_struct
中,有这个进程的PID(进程标识符); - 而且每个进程的PID都是不同的(以此区分);
【图示】
但其实,上述的概念是不正确的,或者说是有偏颇的。有了多线程后,需要重新更新一些概念:
-
一般来说,PCB(进程控制块)结构包含了许多 TCB(线程控制块);
-
每个进程至少有一个线程,就是 主线程(执行main函数的执行流);
我们之前所变编写的代码,全部是只有主线程的单线程进程,因此用ps命令或者getpid()等接口查询进程id时,内核返回给我们的PID也正是这个TGID;
-
对于多线程进程,除了主线程,其余线程被称为 工作线程(程序员创建的线程);
-
在Linux操作系统中,没有线程概念(只有轻量级进程),没有专门的线程TCB控制块,而是用进程的PCB去模拟线程这个概念;
因此我们可以简单的认为,在Linux中:
- 轻量级进程(lwp)= 线程,每个线程的ID就是
PID
; - 轻量级进程组,也叫线程组(thread group)= 进程,每个进程的ID就是
TGID
; - 每一个线程都有一个
task_struct
结构体来描述,且每个线程都有自己的标识符(PID); - 主线程的 PID = TGID;
- 一个进程内的不同线程,其TGID相同,因为同属于一个线程组;
- 同一个进程的所有线程公用同一块虚拟地址空间;
【图示】
3. 一个进程内线程的关系
一个线程的PCB中的除了PID、TGID、内存指针,还有许多,对于同一个进程下的线程来说,有些内容是共享的,有些内容是独有的;
- 一个进程下的不同线程,指向同一块虚拟内存;
- 每个线程 在虚拟内存的共享区 都有一段自己的空间,存放了每个线程独有内容;
共享内容:
独有内容:
4. 线程优缺点
线程控制
- 下列线程控制接口的头文件均为
<pthread.h>
; - 注意涉及线程的代码,编译时需添加参数
-lpthread
;
1. 线程创建
原型:pthread_create()
(1)原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
(2)参数:
thread
:一个出参,获取线程标识符;
attr
:设置线程的属性,一般值为NULL使用默认属性即可;
start_routine
:线程的入口函数地址,一般由程序员自定义函数并传参;
arg
:传给上述线程启动函数start_routine
的参数;
(3)返回值:
接口测试
几条Linux指令:
-
ps -aux | grep 源文件名
可以查看该进程的PID(这里的PID其实就是主线程的PID,也就是进程的TGID);
-
pstack + PID
可以查看该进程的调用堆栈;
-
top -H -p PID
可以动态观察该进程的全部线程信息;
(1)创建多线程进程,工作线程启动函数参数传递临时变量(错误示范);
【测试源码】
//Test_1:传递临时变量作为参数给工作线程入口函数
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
// 工作线程的入口函数
void* pthread_start(void* arg)
{
int* i = (int*)arg;
// 工作线程死循环不退出
cout << "I'm a work thread!" << *i << endl;
sleep(1);
}
int main()
{
for(int i = 0; i < 5; ++i)
{
//1.创建一个工作线程
pthread_t thread;
//这种直接将main的栈上的局部变量作为参数传递给工作线程非常不安全
//有可能当变量i跳出for循环时,变量i地址的访问已不合法
//但工作线程仍会访问局部变量i
int ret = pthread_create(&thread, NULL, pthread_start, (void*)&i);
if(ret < 0)
{
cout << "pthread_create Error!" << endl;
return 0;
}
// 如果让主线程每隔1s再创建新线程,这样新线程就可以在i还未更新的情况得到CPU资源
//sleep(1);
}
//2.主线程循环不退出,观察线程信息
while(1)
{
cout << "I'm a main thread!" << endl;
sleep(1);
}
return 0;
}
【测试结果】
【修改测试】
但是!,上述代码有非常严重的隐藏,那就是为工作线程的入口函数,传递了临时变量!
这种直接将main的栈上的局部变量作为参数传递给工作线程非常不安全;
上述用例中,主线程的变量i 跳出for循环后,对变量i 的访问已不合法,但工作线程仍可以通过参数地址访问(因为工作线程无法得知该空间是否有效);
【结论】
- 对于工作线程入口函数的参数,我们可以传递:main()函数中保证有效的临时变量、该文件全局变量、堆上动态开辟的变量;
- 更推荐使用堆上变量作为参数传递给工作线程入口函数(下面测试);
(2)创建多线程进程,工作线程启动函数参数传递堆上变量;
【测试源码】
//Test_2:传递堆参数给工作线程入口函数
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
// 工作线程的入口函数
void* pthread_start(void* arg)
{
int* i = (int*)arg;
// 工作线程死循环不退出
cout << "I'm a work thread!" << *i << endl;
//堆上开辟的变量在工作线程使用完后记得释放
//因为主线程并不清楚工作线程什么时候用完该变量,因此只能由工作线程释放
delete i;
sleep(1);
return NULL;
}
int main()
{
for(int i = 0; i < 5; ++i)
{
//1.创建一个工作线程
pthread_t thread;
//这里在堆上动态开辟
int* p = new int;
*p = i;
int ret = pthread_create(&thread, NULL, pthread_start, (void*)p);
if(ret < 0)
{
cout << "pthread_create Error!" << endl;
return 0;
}
//这里不让主线程阻塞等待工作线程,所有工作线程抢占执行
//sleep(1);
}
//2.主线程循环不退出,观察线程信息
while(1)
{
cout << "I'm a main thread!" << endl;
sleep(1);
}
return 0;
}
【输出结果】:各线程并不是按照顺序执行(抢占执行),但打印的数值并不重复;
【结论】
- 若给工作线程的入口函数传递了堆上变量,一定要 在工作线程内释放该堆空间参数;
- 因为主线程并不清楚工作线程什么时候用完该变量,因此只能由工作线程释放 ;
2. 线程终止
- 一般用来终止工作线程,而不是主线程(主线程终止,整个进程退出)
原型-1:pthread_exit()
(1)原型:void pthread_exit(void *value_ptr)
- 作用:谁调用谁退出;
- 该接口一般由工作线程自己调用该函数后终止自己;
(2)参数:value_ptr
- 一个出参,线程退出时,传递给等待线程的推出信息;
value_ptr
不要指向一个局部变量,一定是在堆上开辟的空间;- 因为当其它线程得到这个返回指针时线程函数已经退出了;
(3)返回值:
测试-1
创建一个工作线程,测试接口pthread_exit();
【测试源码】
#include<iostream>
#include<pthread.h>
#include<unistd.h> //sleep()函数使用
using namespace std;
// 测试线程退出接口pthread_exit():谁调用谁退出
// 工作线程的入口函数
void* pthread_start(void* arg)
{
int count = 3;
while(count--)
{
// 工作线程死循环不退出
cout << "I'm a work thread!" << endl;
sleep(1);
}
// 调用退出接口
pthread_exit(NULL);
// 若成功退出,后面代码该线程不会执行
while(1)
{
cout << "work thread still run, pthread_exit() Error!" << endl;
sleep(1);
}
}
int main()
{
//1.创建一个工作线程
pthread_t thread;
int ret = pthread_create(&thread, NULL, pthread_start, NULL);
if(ret < 0)
{
cout << "pthread_create Error!" << endl;
return 0;
}
//2.主线程循环不退出,观察线程信息
while(1)
{
cout << "I'm a main thread!" << endl;
sleep(1);
}
return 0;
}
【测试结果】
【总结】
- 在任何一个线程中调用
exit
函数都会导致进程结束。进程一旦结束,那么进程中的所有线程都将结束; - 主线程中,main函数里直接
return
或是调用了exit
函数,则主线程退出,且整个进程也会终止(即进程中的所有线程终止); - 主线程中调用
pthread_exit
, 则仅主线程结束,进程不会结束,进程内的其他线程也不会结束;
原型-2:pthread_cancel()
(1)原型:int pthread_cancel(pthread_t thread)
- 作用:一个线程(主线程或工作线程)调用该函数可终止同一进程的另一个线程或自己线程;
- 该线程被其他线程突然终止,不会有退出信息(因此可能导致僵尸线程);
- 一个线程使用该函数终止自己的线程,不会像 pthread_exit 函数立刻终止;
- 主线程使用该函数终止自己的线程,是有一个过程的;
(2)参数:thread
:被终止线程的线程标识符(线程ID);
(3)返回值:
测试-2
- 接口
pthread_t pthread_self(void)
获取自己的线程标识符;
(1)工作线程调用pthread_cancel()接口终止自己;
【测试源码】
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;
// 测试线程退出接口pthread_cancel(pthread_t thread):通过线程标识符结束任意线程
//
// case_1.工作线程自己调
// tip:接口pthread_t pthread_self(void) 获取自己的线程标识符
// 工作线程的入口函数
void* pthread_start(void* arg)
{
int count = 5;
while(count--)
{
// 工作线程死循环不退出
cout << "I'm a work thread!" << endl;
sleep(1);
}
// 调用退出接口退出自己线程
pthread_cancel(pthread_self());
// 若成功退出,后面代码该线程不会执行
while(1)
{
cout << "work thread still run, this pthread not exit now!" << endl;
// sleep(1);
}
}
int main()
{
//1.创建一个工作线程
pthread_t tid;
int ret = pthread_create(&tid, NULL, pthread_start, NULL);
if(ret < 0)
{
cout << "pthread_create Error!" << endl;
return 0;
}
//2.主线程循环不退出,观察线程信息
while(1)
{
printf("I'm a main thread!\n"); //注意,这里不能使用cout打印!否则看不到主线程的打印内容
sleep(1);
}
return 0;
}
【测试结果】
【Tip】cout不是线程安全的,printf是线程安全的 !涉及多线程的打印最好使用printf()接口;
上述例子中,如果将主线程的打印语句修改为
cout << I'm a main thread!" << endl;
我们就会发现,该程序的结果发生问题:
原因分析:
- 在多线程环境下,I/O流对于不同线程来说也是一种互斥资源;
cout输出流
内部并没有实现线程安全机制,因此当主线程和工作线程均为cout输出流
打印时,会造成线程访问输出流异常;
解决方案:
- 让主线程或工作线程的任意一个打印方式改为
printf()
接口即可;- 因为printf是线程安全的,也就是自己做了线程同步的处理;
- 最好所有线程均采用
printf()
接口方式,因为std::cout
与printf
混用,在多线程环境下可能会导致coredump;
若想了解详细原因可以参考这篇讲解:C/C++的流(stream)对象 、c++ cout 多线程
【结论-1】
- 工作线程调用
pthread_cancel()
可以关闭自己,但并不是立即像pthread_exit()
一样立即执行,而是有一个过程,该过程(很短)中工作线程仍在运行;
(2)主线程调用pthread_cancel()接口终止工作线程;
【测试源码】
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;
// 测试线程退出接口pthread_cancel(pthread_t thread):通过线程标识符结束任意线程
//
// case_2.主线程调,结束工作线程
// 可以成功结束。
// 工作线程的入口函数
void* pthread_start(void* arg)
{
while(1)
{
// 工作线程死循环不退出
cout << "I'm a work thread!" << endl;
sleep(1);
}
}
int main()
{
//1.创建一个工作线程
pthread_t tid;
int ret = pthread_create(&tid, NULL, pthread_start, NULL);
if(ret < 0)
{
cout << "pthread_create Error!" << endl;
return 0;
}
getchar();
// 作用:让主线程阻塞等待,当得到用户输入的字符后才能继续向下执行
// 该语句影响主线程后续代码执行,但完全不影响工作线程
pthread_cancel(tid); //终止线程ID为tid的工作线程
//2.主线程循环不退出,观察线程信息
while(1)
{
// cout << "I'm a main thread!" << endl;
printf("I'm a main thread!\n");
sleep(1);
}
return 0;
}
【测试结果】
查看输入字符前的堆栈情况:
输入字符后的堆栈情况:
【结论-2】
- 通过主线程调用
pthread_cancel()
关闭一个工作线程,由于该工作线程是突然被关闭的,并没有产生任何退出信息,因此工作线程立即正常退出;
(3)主线程调用pthread_cancel()接口终止主线程自己;
【测试源码-1】工作线程死循环,主线程调用pthread_cancel
接口后,仍有其他代码;
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;
// 测试线程退出接口pthread_cancel(pthread_t thread):通过线程标识符结束任意线程
//
// case_3.主线程调,结束自己线程
// 3.1 工作线程循环,主线程调用结束接口后还有代码
// 3.2 工作线程循环,。。。。。。。。。。没有代码
// 3.3 工作线程可以自己退出。
// 工作线程的入口函数
void* pthread_start(void* arg)
{
while(1)
{
// 工作线程死循环不退出
printf("I'm a word thread!\n");
sleep(1);
}
}
int main()
{
//1.创建一个工作线程
pthread_t tid;
int ret = pthread_create(&tid, NULL, pthread_start, NULL);
if(ret < 0)
{
cout << "pthread_create Error!" << endl;
return 0;
}
getchar(); //作用:让主线程阻塞等待,当得到用户输入的字符后才能继续向下执行
pthread_cancel(pthread_self()); //主线程终止自己线程
//2.主线程循环不退出,观察线程信息
while(1)
{
printf("I'm a main thread ! \n");
sleep(1);
}
return 0;
}
【测试结果-1】
【结果分析-1】
getchar()前,观察该进程堆栈情况:
主线程调用cancel关闭自己后,发现:
- 该进程变为僵尸进程;
- 进程堆栈调用情况无法查到;
- 查看进程的线程情况,发现主线程为僵尸状态,工作线程也存在;
所有主线程调用cancel接口关闭自己线程后,一定会形成僵尸线程?再观察下面两种测试;
【测试源码-2】工作线程非死循环,主线程调用pthread_cancel
接口后,仍有其他代码;
在【测试源码-1】的基础上,改变工作线程的while(1)循环条件;
【测试结果-2】
【结果分析-2】
实际上,在main函数调用cancel接口之前,该进程正常运行两个线程:
当main函数调用cancel接口之后,该进程退出,但工作线程仍在运行,主线程仍为僵尸进程:
但当工作线程执行完毕后,该进程立马正常结束:
【测试源码-3】工作线程死循环,主线程调用pthread_cancel
接口后,不执行任何代码;
在【测试源码-1】的基础上,注释掉主线程cancel之后的代码;
【测试结果-3】虽然工作线程死循环,但仍被强制退出:
【结果分析-3】
- 主线程执行
cancel接口
时是有一个过程的,虽然这个过程很短暂; - 调用
cancel接口
后只有return 0;
一条语句,因此这个过程中,主线程直接调用了return 0
退出该进程; - 也就是说该程序根本没有执行
cancel接口
,而是通过return正常退出;
【总结】
pthread_cancel()
接口执行是有一个过程,该过程(很短)中被关闭的线程仍在运行;- 主线程调用
pthread_cancel()
接口关闭工作线程,被关闭的工作线不会产生退出状态信息; - 主线程调用
pthread_cancel()
接口直接关闭自己,而工作线程正常运行产生的退出状态信息,得不到任何线程的回收,因此就会产生僵尸线程;
3. 线程等待
为什么要线程等待?
(1)线程本质:
在Linux中,新建的线程并不是在原先的进程中,而是系统通过 一个系统调用clone()。该系统copy了一个和原先进程完全一样的进程,并在这个进程中执行线程函数。不过这个copy过程和fork不一样。 copy后的进程和原先的进程共享了所有的变量,运行环境。所以线程是共享全局变量和环境的。
(2)僵尸线程产生原因:
-
线程被创建出来的默认属性是joinable属性,表示该线程退出时,依赖其他线程回收资源(线程的共享区空间之类);
-
若主线程先退出,那么工作线程的退出状态信息无法被回收,那么就会产生僵尸线程;
-
僵尸线程危害:
僵尸线程属于已经退出的线程,但其空间没有被释放,仍然在进程的地址空间内,需要其他线程释放;
创建新的线程不会复用刚才退出线程的地址空间,因此若所有空间不回收,会造成内存溢出;
(3)线程等待的作用:
- 主线程调用
pthread_join
接口,用来等待一个线程结束,避免出现僵尸线程; - 若等待的线程没有退出,当前线程阻塞;
原型: pthread_join()
(1)原型:int pthread_join(pthread_t thread, void **value_ptr);
- 作用:用来等待一个线程结束,以此避免僵尸线程的出现;
- 这是一个阻塞调用接口,若等待的线程没有退出,当前线程阻塞,直至线程退出;
(2)参数:
thread
:被终止线程的线程标识符(线程ID);value_ptr
:它指向一个指针,指向退出线程的退出信息;
(3)返回值:
测试
主线程退出前先调用pthread_join
回收工作线程;
【测试源码】
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;
// 测试线程等待pthread_join(pthread_t thread,void** retval);
//
// case_3.主线程调,结束自己线程
// 工作线程的入口函数
void* pthread_start(void* arg)
{
// 工作线程
int count = 30;
while(count--)
//while(1)
{
// 工作线程死循环不退出
printf("I'm a word thread!\n");
sleep(1);
}
}
int main()
{
//1.创建一个工作线程
pthread_t tid;
int ret = pthread_create(&tid, NULL, pthread_start, NULL);
if(ret < 0)
{
cout << "pthread_create Error!" << endl;
return 0;
}
getchar(); //作用:让主线程阻塞等待,当得到用户输入的字符后才能继续向下执行
// 在主线程调用cancel接口退出之前,先让主线程调用等待接口
pthread_join(tid, NULL);
cout << "main use pthread_cancel()" << endl;
pthread_cancel(pthread_self()); //主线程终止自己线程
sleep(1);
cout << "pthread_cancel error !" << endl;
while(1)
{
printf("I'm a main thread! \n");
sleep(1);
}
return 0;
}
【测试结果】:不会产生僵尸线程
4. 线程分离
什么是线程分离?
原型: pthread_detach()
(1)原型:int pthread_detach(pthread_t thread);
- 作用:将某线程设置分离属性;
- 线程组内其他线程对目标线程进行分离,也可以是线程自己分离;
(2)参数:thread
:被终止线程的线程标识符(线程ID);
(3)返回值: