Linux学习:线程控制
目录
- 1. 线程创建
- 1.1 接口使用
- 1.2 进程的PID与轻量级进程的LWP
- 1.3 线程函数的字符串传参
- 2. 线程退出
- 2.1 线程的退出方案
- 2.2 线程之间的关系
- 3. 线程等待
- 4. 线程分离
- 5. 线程的优缺点
- 5.1 线程的优点
- 5.2 线程的缺点
- 5.3 进程与线程的编码区别
- 6. C++11中的线程
- 7. 如何理解原生线程库与线程id
1. 线程创建
1.1 接口使用
#include <pthread.h>
//gcc/g++编译时,添加-lpthread选项
int pthread_create(pthread_t* thread, const pthread_atter_t* attr, void*(*start_routine)(void*), void* arg);
- 参数1
pthread_t* thread
: 输出型参数,返回用户级标识线程的编号 - 参数2
const pthread_atter_t* attr
: 线程属性,现阶段使用时暂直接设置为nullptr - 参数3
void*(*start_routine)(void*)
: 线程需要执行任务,函数指针 - 参数4
void* arg
: 线程执行函数的参数 - 返回值
int
: 成功时返回0,失败时返回错误码
#include <pthread.h>
pthread_t pthread_self();
此接口用于获得当前线程的pthread_id
。 pthread_id
用户级原生线程库中用于标识线程的id。
1.2 进程的PID与轻量级进程的LWP
string ToHex(int tid)
{char ret[64];snprintf(ret, sizeof(ret), "0x%x", tid);return ret;
}void* newthreadrun(void* args)
{while(true){cout << "this is a new thread, pid is : " << getpid() << ", tid is :" << ToHex(pthread_self()) << endl;sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, newthreadrun, nullptr);while(true){cout << "this is main thread, pid is : " << getpid() << ", tid is : " << ToHex(tid) << endl;sleep(1);}return 0;
}
程序运行后,主线程与新线程的PID
相同,这是因为Linux中PID
是用来描述进程的标识。但实际上,Linux的底层实现使用的是轻量化进程,CPU也是用轻量级进程的标识LWP
来调度程序。此前,之所以单执行流进程可以被CPU使用PID
调度,是因为只有一个执行流时,其PID
与LWP
相同。使用指令ps -ajx
只能查看到一个进程,只有指令ps -aL
才能查看到一个进程中的多个线程。
1.3 线程函数的字符串传参
当我们给新线程函数传递字符串参数时,本质上传递的是字符串的地址。但在如上循环创建线程的场景下,用来第一字符串的变量是一个局部变量,当每轮循环结束其都会被销毁。而在下一轮循环中,原变量的地址处又会创建新的变量覆盖写入新的值。这样就会导致被创建的新线程中,字符串参数变量的值会被不断改变,且循环结束后访问空间不合法。此种方式无法达成传参的效果,所以给线程函数传参时,类似场景下,最好使用STL中的容器,或是自己在堆上开辟空间定义变量。
2. 线程退出
2.1 线程的退出方案
- 1. 线程函数结束: 线程函数正常运行结束,函数返回
void* newthrreadrun(void* arg)
{cout << "this is a new thread" << endl;sleep(1);return nullptr;
}
- 2. pthread_exit退出: 线程不能使用
exit
接口退出,而是有其专用的pthread_exit(退出码)
接口用于线程退出
#include <pthread.h>
void* newthreadrun(void* arg)
{//...pthread_exit(0);
}
- 3. pthread_cancel取消线程: 使用接口
pthread_cancel(pthread_id)
接口,可以直接将指定线程取消,以此方式退出的线程,退出结果为PTHREAD_CANCEL:(void*)-1
。
//新线程自己取消
void* newthreadrun(void* arg)
{//...pthread_cancel(pthread_self());
}//主线线程取消
int main()
{pthread_t tid;pthread_create(&tid, nullptr, newthreadrun, nullptr);pthread_cancel(tid);return 0;
}
主线程与新线程都可以调用pthread_cancel
接口结束指定的线程,此种结束线程的方式与kill命令机制类似。主线程在使用pthread_cancel
结束其他线程时,一定要确定其他线程已经启动,否则调用此接口会出错。结束线程的三种方式中,前两者推荐使用,pthread_cancel
的方式不推荐使用。
2.2 线程之间的关系
同一进程资源内的线程大体其实被分为两类,一是执行main
函数的主线程,二是除主线程之外的其他线程。其他的线程都由主线程创建,多个线程之间虽没有像进程创建那样的父子进程的关系,但线程之间却也有特殊的联系。
- 1. 主线程退出,相当于整个进程退出,进程为承担系统资源的基本实体,进程退出后其所属的资源也会被释放。所以,进程退出,所有的线程都会退出,也因此,一般程序都需要主线程最后结束。
- 2. 多线程中的任何一个线程出现异常,都会导致整个进程退出。线程是进程的一个执行分支,线程出现异常就相当于是整个进程出现异常。也因此,线程的健壮性并不好。
- 3. 对于线程而言,同一个进程内的线程,其大部分的资源都是共享的。所有线程共用同一个地址空间,所以,地址空间上的资源都是共享的。也因为所有线程共用一个地址空间,同一进程内的线程之间可以做到瞬间通信。
新线程的独立栈:
虽然所有线程共用一个地址空间,所有线程都可以通过取地址的方式访问地址空间中的任意一处资源,但一般情况线程之间都不会通过此种方式去访问其他线程内的资源。主线程在创建局部变量与开辟函数栈帧时,都是直接在地址空间中的栈区进行。但,其他线程并不和主线程共用此栈区,每一个其他线程都会在共享区中创建出一块属于自己的独立栈空间来自己使用。
线程之间不能共享栈空间,这是因为各个线程在切换的过程中无法获知其他线程对栈的使用范围。这就导致等待线程切换回来之后,其原本的栈帧范围已经被其他线程使用过了,其中的资源被修改过了,使得整个程序无法运行。
3. 线程等待
主线程之外的其他线程在运行结束后,类似进程其也需要被主线程等待回收,否则也会导致内存泄漏。用于等待回收线程的接口如下:
#include <pthread.h>
int pthread_join(pthread_t thread, void** retval);
- 参数1
pthread_t thread
: 指定等待的线程 - 参数2
void** retval
: 输出型参数,获取线程的退出码 - 返回值
int
: 成功时返回0,失败时返回错误码
线程的返回值不同于进程,线程函数的返回值单纯只是退出码,不包含信号、core dump标志位等信息。这是因为线程若是收到信号,那么,就相当于是整个进程收到了信号并处理。
void
类型不能定义变量,但void*
可以用来定义变量,void*
是指针类型。
4. 线程分离
pthread_join
接口等待回收进程采用的是阻塞式等待,此种方式在普通的学习练习代码中并没有什么影响。但实际上,大部分的软件都是不需要自己主动执行结束的,其一般都是常驻任务死循环。此种场景下,阻塞式的等待回收线程就是不可行的了,因为主线程一般都还有自己的任务要去执行。那么,线程有没有类似进程非阻塞等待回收资源的方式呢?
多线程中存在这样一个接口pthread_detach
,此接口的作用为线程分离。其作用为让新线程分离出去,主线程不用再去关心此线程的回收,当此线程运行结束后,其资源会被自动回收销毁。 我们将没有被分离的进程称为joinable
的。
#include <pthread.h>
int pthread_detach(pthread_t thread);
- 参数1
pthread_t thread
: 指定需要分离的线程 - 返回值
int
: 成功时返回0,失败时返回错误码
给线程设置分离时,即可以通过主线程设置,也可以新线程自己设置。分离后的线程,仍处于同一份进程资源内,只是主线程不需要再去阻塞式等待回收这一线程了,其的其他特性依旧,主线程结束时,其仍旧会同样结束。detach
后的线程不能被join
,若对其进行join
操作就会出错。
5. 线程的优缺点
5.1 线程的优点
- 1. 创建成本小,多个线程之间资源共享,不用创建额外的内核数据结构,进行从磁盘到内存的数据加载
- 2. 线程之间的切换相较于进程操作系统需要做的工作会少很多
线程之间在切换时,其只用进行上下文数据的保护,地址空间与页表等内核级数据结构不需要进行切换。但,对于这些内核数据结构的切换只是改变寄存器中对应存储的地址与数据,这些操作的工作量并不多。线程切换相较于进程节省的操作主要为中缓存数据的切换。
CPU中存在着一个硬件cache(缓存),此缓存会将CPU当前执行进程所需的指定数据提前加载。这样,CPU在执行程序时,可以直接从缓存中获取数据,而不用再去从内存中寻找读取,效率提高。缓存加载数据时,不是仅仅只加载需要的数据,而是会将指定数据周边可能用到的数据一起加载进来,此操作称之为局部性原理。
进程在切换时,CPU中缓冲的数据也要全部切换,但线程之间的切换其缓存中的数据则不需要切换,因为线程的资源存储地很靠近,一次缓存有效数据地命中率很高。缓存的大小比寄存器大的多,一次切换的工作量不少,缓存资源的切换才是线程相较于进程切换工作步骤少很多的主要原因。
- 3. 多线程相较于多进程占有的资源少很多
- 4. 可充分利用多处理器的任务可并行数量(多进程同样)
- 5. 在等待慢速IO操作的同时,程序可以执行其他的计算任务(多进程同样)
以多执行流的适配性为标准,程序可以被分为计算密集型与IO密集型
- 计算密集型: 程序多执行数学算法运算类的操作
- IO密集型: 程序多执行网络通信,下载类的操作
对于计算密集型的程序并不是线程数量越多越好,线程的切换本身就有消耗成本,一般计算机内有几核,计算密集型就创建几个线程。而对于IO密集型的程序,线程的数量最好就要多一些,因为在等待IO操作的时间是重叠的。
5.2 线程的缺点
- 1. 性能损失,线程越多,切换的成本就越高,更多的调度与切换
- 2. 程序的健壮性降低,多线程之间资源共享
- 3. 缺乏访问控制,多个线程之间没有执行的先后顺序
- 4. 多线程的编程难度高
5.3 进程与线程的编码区别
- 1. 进程更强调独立性
- 2. 线程更强调资源共享
- 3. 线程大部分资源共享但也有一些资源是私有的
线程中的私有资源有,管理数据结构及其中的属性字段(优先级、描述id),线程硬件的上下文数据是私有的(调度),线程有独立的栈结构(常规运行)。
线程中的共享资源有,地址空间中的代码区、数据区(全局区),文件描述符表,信号处理方式,进程所在的当前工作目录。
什么是线程安全问题:
一个线程出现问题,导致其他线程也出现了问题,从而使得整个进程退出,此种情况被称之为线程安全问题。
多线程中的公共函数重入:
多线程中,公共函数被多个线程同时进入,我们就称该函数被重入了。
6. C++11中的线程
C++11中添加了线程相关的内容<thread>
,那么,其线程相关的这部分底层是如何实现的呢?
C++中线程相关的内容,Linux操作系统下其底层实现是对Linux原生线程库pthread
的再封装,即使是使用C++中线程的语法g++
编译时同样要加上-lpthread
选项。C++为了支持语言跨平台性,线程部分根据不同的操作系统对其底层线程实现方式都做了封装支持。其他支持多线程内容的编程语言,在Linux下也都要使用其实现的线程接口,是在这底层唯一方式的基础上对其进行了再封装。
7. 如何理解原生线程库与线程id
Linux操作系统中没有线程的概念,只有轻量级进程。但用户却要以线程的方式来进行多执行流的编程。为此Linux设计了原生线程库pthread
通过封装轻量级进程相关的数据结构与接口,从而实现线程相应的数据结构与接口,支持用户使用。
因为Linux操作系统中没有线程的概念,所以,自然也不会对线程进行管理。但线程相关的数据结构也需要被管理,线程由原生线程库pthread
实现,其的管理工作自然就需要此库去支持。原生线程库pthread
本身就是一个动态库,在调用时会被加载到内存的共享区中,线程的数据结构也就在共享区中的库空间内被创建与管理。库中维护方法,也维护数据结构,线程的数据结构大致如下:
一个线程的数据结构由三部分组成struct pthread
描述线程的数据结构(类似于windows中对进程用于管理的TCB)、线程的局部存储、线程的独立栈。而库中用于标识线程的pthread_id
就是这样一个线程数据结构的起始地址(进程地址空间中的虚拟地址)。在创建轻量级进程的接口clone
中就有需要传递的参数void* stack
,因此内核中LWP可以借此很轻松的找到自己创建的独立栈空间。
线程的局部存储:
程序中的全局变量会被所有线程共享,而在全局变量前,添加__thread
的编译选项后,不同线程再使用此全局变量时就会发生局部存储。编译时,会将此全局变量拆出来,拷贝成几份到各自调用的线程的局部存储中,再次访问时,各个线程就访问自己局部存储中的变量。
线程的局部存储只能用于C语言的内置类型。使用变量保存各个线程的启动时间时,可以采用局部存储,从而不再需要每个进程都进行定义,避免我们编写时的多份定义。
__thread int create_time = 0;void* newthreadrun(void* arg)
{create_time = time(nullptr);while(true){cout << "new thread create time : " << create_time << endl;sleep(1);}
}int main()
{for(int i = 0; i < 5; i++){pthread_t tid;pthread_create(&tid, nullptr, newthreadrun, nullptr);sleep(1);}while(true){sleep(1);}return 0;
}