目录
一、线程概念
在操作系统课本中讲到线程是比进程更加轻量化的一种执行流,即线程是在进程内部执行的一种执行流。对于具体的Linux系统,线程是CPU调度的基本单位,进程是承担系统资源的基本实体。
进程中有PCB包含内核数据结构,指向地址空间、页表等。那么在进程的数据结构层面上,只创建PCB,和父进程指向同一块地址空间。在资源划分上,代码和数据划分成不同部分,每个进程的数据私有,执行一部分代码。在执行进程时,只用按照顺序执行这个PCB的代码部分即可。我们把这种PCB(task_struct)称为轻量级进程(Light Weight Process,LWP)。一个轻量级进程就是一个执行流,之前讲解的进程是一种内部只有一个执行流的进程。今天的进程是内部有多个执行流的进程。
- 因为线程和父进程指向同一块地址空间,同一个进程的线程大部分资源都是共享的。
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
- 一切进程至少都有一个执行线程。
- 线程在进程内部运行,本质是在进程地址空间内运行。
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
二、线程特性
2.1 进程更加轻量化
1. 线程切换时不用更换地址空间、页表,即不用切换所有的寄存器,只用把一些临时变量的寄存器更换即可,而进程切换要所有的相关寄存器全部切换。
2. 线程级切换不需要切换cache,进程级切换需要切换cache,因为原本数据不需要也没意义了。
补充:
CPU中有一个cache用来保存一些热数据(把保存在cache的一部分代码和数据叫做热数据),高频访问的数据和较大概率访问的数据(当前代码的上下文)缓存到cache,如果缓存失效就重新缓存。这使用了局部性原理,给预加载机制,提供理论基础。
时间局部性(Temporal Locality):这是指如果一个数据项被访问了一次,那么它在不久的将来很可能再次被访问。因此,将这些数据保存在缓存中可以提高访问速度,因为下一次访问时很可能直接从缓存中获取,而不是从更慢的内存中获取。
空间局部性(Spatial Locality):这是指如果一个数据项被访问了,那么与它相邻的数据项也很可能被访问。因此,当CPU访问一个数据项时,它可能会预先加载该数据项附近的多个数据项到缓存中,以便在未来需要时快速访问。
2.2 线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多。
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
- 线程占用的资源要比进程少很多。
- 能充分利用多处理器的可并行数量。
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
2.3 线程的缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。 - 健壮性降低(一个线程崩溃,整个进程就崩溃了)
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 - 缺乏访问控制(共享内存,数据可被多个线程访问)
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。 - 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
2.4 线程的异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程是承担分配系统资源的基本实体,线程也是申请资源的一部分,进程终止,该进程内的所有线程也就随即退出。
2.5 线程用途
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
- 多CPU系统中,使用线程提高CPU利用率:对于多核心cpu来说,每个核心都有一套独立的寄存器用于进行程序处理,因此可以同时将多个执行流的信息加载到不同核心上并行运行,充分利用cpu资源提高处理效率
- 耗时的操作使用线程,提高应用程序响应。使用多线程可以更加充分利用cpu资源,使任务处理效率更高,进而提高程序响应。
三、进程和线程
1. 进程是资源分配的基本单位。
2. 线程是调度的基本单位。
3. 线程共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器(保存上下文数据)
栈(独立的栈结构)
errno
信号屏蔽字
调度优先级4. 进程的多个线程共享同一个地址空间。线程只是在进程虚拟地址空间中拥有相对独立的一块空间,但是本质上说用的是同一个地址空间。因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数handler表)
当前工作目录(cwd)
用户id和组id5. 线程和进程都可并发执行。
6. 线程的粒度小于进程,占用资源更少,因此通常多线程比多进程并发性更高。
7. 进程是资源的分配单位,所以线程并不拥有系统资源,而是共享使用进程的资源,进程的资源由系统进行分配。
8. 线程使用公共变量/内存时需要使用同步机制,因为他们在同一地址空间内。
进程和线程的关系如下图:
四、线程控制
4.1 包含线程的编译链接
Linux没有真正的线程呢,只有轻量级进程的概念。所以Linux OS只会提供轻量级进程创建的系统调用,不会直接提供线程创建的接口。为了和其它OS统一,Linux实现了一个软件层,对上提供了线程的控制接口,使得用户可以像使用其他操作系统一样创建和管理线程,用户认为自己创建了一个线程,实际上该线程在内核对应成一个LWP。软件层不属于OS,是由系统调用者封装的一个库:pthread原生线程库。后面讲到创建线程和创建LWP是同一个含义。
这也是Linux的一大亮点,实现了软件分层,接口和实现分离,很容易解耦,未来原生线程库想更新,也不会影响内核。每一款Linux系统都要配备pthread库,因此它叫原生线程库。
因此它不属于OS,也不属于C/C++,所以编译链接时要加上 -lpthread 选项指定库。
-l选项后面跟的是库的名称,不包含前缀lib和后缀.a或.so
库的名字是去掉前缀lib、去掉后缀版本和.so,即pthread
该库在如下路径中:
举例创建线程:
mythread:testThread.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f mythread
#include <iostream>
#include <unistd.h>
#include <pthread.h>
void *NewTread(void *arg)
{
const char *threadName = (const char *)arg;
while(true)
{
std::cout << "I am a new thread: " <<threadName << std::endl; sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, NewTread, (void *)"thread 1");
while(true)
{
std::cout << "Main tread" << std::endl;
sleep(1);
}
return 0;
}
使用ps -aL查看进程状态,LWP对应是轻量级进程的编号。同一进程的多个线程PID相同,主线程的PID和LWP相同,可以用来判断线程是否为主线程。
4.2 创建线程
功能:创建一个新的线程
原型#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数
thread:输出型参数,返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:函数地址,线程启动后要执行的函数。
arg:传给线程启动函数的参数
返回值:成功返回0,失败返回错误码
start_routine:函数地址,线程启动后要执行的函数。
这个函数的返回值和参数都是void * ,它可以接受任意类型的参数和返回任意类型的结果。
错误检查:
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
- pthreads同样也提供了线程库内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过检查返回值来确定函数是否成功执行,因为读取返回值要比读取线程库内的errno变量的开销更小。
创建多线程:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <functional>
#include <string>
#include <vector>
using func_t = std::function<void()>;
const int threadNum = 5;//创建线程的数量
class ThreadData
{
public:
ThreadData(const std::string& name, const uint64_t& ctime, func_t f)
:threadName(name)
,createTime(ctime)
,func(f)
{}
public:
std::string threadName;
uint64_t createTime;
func_t func;
};
void Print()
{
std::cout << "线程执行中......" << std::endl;
}
void* ThreadRoutine(void* args)
{
int a = 10;
ThreadData* ptd = static_cast<ThreadData*>(args);
while(true)
{
std::cout << "new thread, name: " << ptd->threadName << " createTime: " << ptd->createTime << std::endl;
ptd->func();
if(ptd->threadName == "thread-4")
{
std::cout << ptd->threadName << " 触发了异常!!!!!" << std::endl;
a /= 0;//制造异常
}
sleep(1);
}
}
int main()
{
std::vector<pthread_t> pthreads;
for(size_t i = 0; i < threadNum; i++)
{
char threadName[20];
snprintf(threadName, sizeof(threadName),"%s-%lu","thread",i);
pthread_t tid;
ThreadData* ptd = new ThreadData(threadName, (uint64_t)time(nullptr), Print);
pthread_create(&tid, nullptr, ThreadRoutine, ptd);
pthreads.push_back(tid);
sleep(1);
}
std::cout << "thread id : ";
for(const auto& tid:pthreads)
{
std::cout << tid <<" ";
}
std::cout << std::endl;
while(true)
{
std::cout << "main thread" << std::endl;
sleep(1);
}
return 0;
}
如果不制造异常,正常运行线程tid如下:
线程ID和LWP编号不同
- 线程ID(TID):在Linux中,当使用如pthread_self()这样的函数获取线程ID时,得到的是一个线程标识符,它实际上是一个指向线程控制块(Thread Control Block, TCB)的指针,这个指针在内存地址空间中是唯一的。因此将其打印出来时,会看到一个像140653110572800这样的内存地址值(16进制:0x7FEC 5AB1 3700)。这个值对于用户空间程序来说并没有直接的用途,除了调试目的之外。
- 轻量级进程(LWP):在Linux的NPTL(Native POSIX Thread Library)实现中,每个线程在内核级别上都有一个与之关联的轻量级进程(LWP)。LWP是内核用来调度线程的资源,它使得线程看起来就像是一个普通的进程(尽管它们共享相同的地址空间)。当你使用ps -aL或类似的命令查看线程时,LWP列显示的是这些轻量级进程的ID,这些ID在内核级别上是唯一的,并且与用户空间的线程ID不同。
4.3 获得线程自身的ID
pthread_ self函数
功能:获取调用该函数的线程的线程ID。
原型:
#include <pthread.h>
pthread_t pthread_self(void);
返回值:
返回一个指向pthread_t变量的指针,其中存储了调用线程的线程ID
4.4 线程终止
在Linux中,如果需要让线程终止,不能直接使用exit函数,因为exit是用于进程终止的。
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2. 线程可以调用pthread_ exit终止自己。
3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit函数
功能:线程终止
原型:#include <pthread.h>
void pthread_exit(void *retval)
参数:
retval:输出型参数,输出线程启动后执行函数的返回值。不要指向一个局部变量。
返回值:
无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
void* ThreadRunning(void* args)
{
string name = static_cast<const char*>(args);
int cnt = 5;
while (cnt--)
{
cout << "new thread is running, thread name: " << name << "thread id: " << ToHex(pthread_self()) << endl;
sleep(1);
}
//return (void*)"thread-1 done";//返回字符串常量的起始地址
pthread_exit((void*)"thread-1 done");//两种退出方式结果相同
}
pthread_cancel函数
功能:取消一个执行中的线程
原型#include <pthread.h>
int pthread_cancel(pthread_t thread);<