前面我们介绍了线程的基础概念,即线程为进程内部的执行分支。下面我们将介绍一下具体的线程控制相关函数。
目录
1、铺垫
2、线程创建
3、线程等待
4、线程异常
5、线程退出
<1>线程函数返回退出
<2>pthread_exit
<3>pthread_cancel
6、线程优点
7、线程的缺点
8、Linux进程VS线程
1、铺垫
在正式介绍线程控制之前,我们需要一点铺垫知识。在我们上层的用户看来,其实系统中是存在线程和进程的区分。而实际上,Linux中并没有真正的线程,所以Linux系统并没有提供相应的系统调用。为了让相应接口符合用户的认知,我们就必须在内核上层封装一层线程库,把相应的系统调用变成线程。这个线程库我们称为原生线程库,每个Linux操作系统在安装时,都必须自带。在我们编写多线程的代码时,都必须手动去连接该库,否则会出现异常。
2、线程创建
第一个参数为输出型参数,我们可以理解成线程id,我们需要定义一个pthread_t 的变量,然后将该变量的地址传入进去。第二个参数用于设置线程的属性,不过这里我们一般设置为nullptr即可。第三个参数是一个函数指针,在新线程被创建之后,会去执行函数所指向的代码。第四个参数为第三个参数中的变量。
我们以一个小样例来演示一下该接口的用法
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
void* handler(void *arg)
{
while (1)
{
sleep(1);
std::cout << "I am pthread-1 " << "pid is : " << getpid() << endl;
}
}
int main()
{
pthread_t thread;
pthread_create(&thread, nullptr, handler, nullptr);
while(1)
{
sleep(1);
std::cout << "I am pthread -2 " << "pid is : " << getpid() << endl;
}
return 0;
}
运行之后的结果
我们可以看见,两个线程的pid是一样的,这也侧面说明了线程是进程内部的一个执行分支。如果我们想在shell会话中看到两个线程相关的信息,我们可以使用ps -aL命令。这两线程调用的先后由调度器决定,不必纠结其顺序。
其中LWP代表轻量级进程的意思,这个参数我们可以认为它是线程的id(内核中),我们cpu在调度时,也是使用这个参数。需要注意的是,如果进程中只有一个线程,线程的id会和进程id一致。上图中的主线程id也和进程的pid数值是一样的。
上面所述的id用于内核标识线程,而用户标识线程的id,一般是在pthread_create接口的第一个参数上获取。当然,我们也可以使用pthread_self也可以获取用户级别的id。至于这个id的具体是什么东西,这里暂不介绍。
这里我们可以验证一下线程和进程之间的一些异同点,就比如主线程(main函数中的线程)退出,其他的线程是否会退出呢?我们稍微修改一下代码,将主线程休眠2秒后结束。
我们可以发现,在主线程退出后,其他的线程也就跟着退出了。所以我们可以得出一个结论:主线程退出 = 主进程退出 = 所有的线程都退出。而这就要求我们的主线程一般就是最后退出,而且线程也需要被wait,否则也造成类似于内存泄露的问题。
3、线程等待
第一个参数就是我们自己创建pthread_t变量,这里需要传入该变量在线程创建函数后的值。第二个参数我们不做详细的介绍,这是一个指针的地址,该指针用于存储线程函数的返回值(也就是上新建线程所执行函数的返回值),这里我们设置为nullptr即可。
等待成功返回0,失败返回错误码。
4、线程异常
在正常情况下,线程退出一般就有三种情况:一是代码跑完,结果是正确的;二是代码跑完,结果不正确;三是代码出现异常。前面的两种情况,我们可以通过新线程执行函数的返回值来进行判断,后面一种情况出现,进程会直接退出,所有的线程都会挂掉。这是因为当线程出现异常时,系统会检测到异常并向进程发送信号,进程收到信号,就会被终止。无论哪个线程出异常,代表就是整个进程出现异常。这也是为什么pthread_join没有对线程异常做处理的原因,这是因为出现异常,根本就轮不到pthread_join来回收。而这个异常就需要其父进程来考虑了。
同一进程内的数据大部分是被线程共享的(这里可以通过访问同一个全局变量证明,让单个线程对变量修改,观察另外的线程中该全局变量是否改变),虽然这方便了各个线程之间进行数据传输,但是这也削弱了线程的健壮性。
5、线程退出
线程退出有多种方式,这里简单介绍一下。
<1>线程函数返回退出
当线程函数执行完毕时,通过返回函数,线程会自行退出。
<2>pthread_exit
注意区分和exit的区别,exit是用来让进程退出的,而pthread_exit才是用来让线程退出的。
参数是void* 类型,我们可以这样使用(void*)+数字;
<3>pthread_cancel
这种退出方式,我们需要确保新线程已经被创建运行。
传入需要取消线程的id变量。成功返回0,失败返回错误码(这种方式不推荐)
6、线程优点
1、创建一个新线程的代价要比创建一个新进程小得多
2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少
3、很多线程占用的资源要比进程少很多能充分利用多处理器的可并行数量
4、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
5、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
6、I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
优点主要集中在前两点,第一个优点已经提到过,下面着重介绍一下第二个优点。
这个优点不单单体现在让寄存器少了几次交互(这个不是重点),而是我们cpu中存在一块存储空间较大的缓存(cache),cpu要执行每条命令都需要在内存中进行读取,为了提高效率,cpu会直接读取一条代码后面的一大段代码,大概率后面要执行的内容就在这段代码当中。如果没有命中,也没有关系,后续直接跳转到需要执行的地方(概率比较低)。这块缓存的大小一般比较大,常是kb或mb。如果进程进行切换,这块缓存上的数据就需要重新加载。而同一进程线程在切换时,由于是共享绝大部分的资源,所以在cache中的数据大概率可以不用重新加载,这个重新加载的过程一般是很慢的。这才是影响其效率的主要原因。
7、线程的缺点
1、性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
2、健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
3、缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4、编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
8、Linux进程VS线程
1、进程是资源分配的基本单位线程是调度的基本单位
2、线程共享进程数据,但也拥有自己的一部分数据:
<1>线程ID
<2>一组寄存器(cpu中的硬件上下文)
<3>栈(地址空间中的独立线程栈)
<4>errno
<5>信号屏蔽字
<6>调度优先级
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
<1>文件描述符表
<2>每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
<3>当前工作目录
<4>用户id和组id