- 个人主页 :超人不会飞)
- 本文收录专栏:《Linux》
- 如果本文对您有帮助,不妨点赞、收藏、关注支持博主,我们一起进步,共同成长!
理解线程需要和进程的概念紧密联系。
线程是操作系统中的抽象概念,用于实现多任务并发执行。不同的操作系统可以有不同的线程实现方法和模型。例如,在Windows操作系统中,与进程PCB对标的,构建了描述线程的数据结构 —— 线程控制块,但这样子设计有以下几个缺点:
- 创建线程在Windows中开销较大,因为它涉及到较多的内核资源和数据结构的分配
- 线程与进程无法统一组织起来
- 线程的调度效率低
Linux的设计者发现,线程控制块与进程控制块(PCB)大部分描述属性相同,且进程与其内部创建的线程看到的都是同一个地址空间。因此,在Linux中,线程控制块直接复用了PCB的代码,也就是说,Linux底层并没有真正的“线程”,这种复用之后的线程称之为轻量级进程。
那我们之前讨论的进程是什么?这里都是轻量级进程的话,需要另有一个进程PCB来管理整个进程吗?
答案是不用。事实上,在Linux中,因为每个进程都至少有一个线程,即主线程(主执行流),这个线程的LWP和PID是相同的,因此,我们之前讨论的进程PCB,实际上就是这个主线程的task_struct。
ps -aL
命令查看系统中的轻量级进程。
测试:在一个进程中,创建了10个线程,并用ps -aL
命令查看。可以看到有一个主线程和10个新线程,主线程的PID和LWP相同。
线程的调度成本低于进程,是因为同一个进程中的线程共享同一个地址空间,因此这些线程的调度只需要保存和更改一些上下文信息、CPU寄存器即可,如pc指针。而进程的调度需要修改较多的内存资源,如页表、地址空间等,而开销更大的是修改cache缓存的数据。
cache缓存
CPU内部的高速存储器中,保存着一些频繁访问的指令和数据,基于局部性原理,这些数据可能是未来将要被访问的,也可能是当前正在访问的。这么做的目的是减少CPU与内存的IO次数,以便快速响应CPU的请求,而不必每次都从较慢的内存中获取数据。不同进程的cache缓存数据是不同的,因此调度进程是需要切换这部分数据,而同一个进程的不同线程的cache缓存相同。
CPU根据PID和LWP的对比,区分当前调度是线程级还是进程级,进而执行对应的调度策略。
⭕补充:
线程发生异常(如野指针、除零错误等),会导致线程崩溃,进而引发整个进程退出。从宏观角度,因为线程是进程的一个执行分支,线程干的事就是进程干的事,因此线程异常相当于进程异常,进程就会退出。从内核角度,线程出错,OS发送信号给进程,而不是单发给线程。
进程是资源分配的基本单位,线程是调度的基本单位。一个进程中的多个线程共享线程数据,当然也有自己独立的数据。
线程的独立资源:
线程的共享资源:
Liunx中,提供给用户层进行线程控制的函数被打包在一个动态库中 —— pthread。使用线程控制接口时,需要包含头文件
pthread.h
,并在gcc/g++编译时加上-l pthread
选项确定链接动态库。
在/lib64
目录下找到pthread库:
编译时应该添加的选项:
g++ threadTest.cc -o threadTest -l pthread # -lpthread也可以
pthread_create
功能:
创建一个线程
接口:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数:
thread:线程库中定义了一个线程ID类型phtread_t,这里的thread是一个输出型参数,函数会向其指向的空间写入创建线程的ID
attr:线程的属性,一般设为nullptr即可
start_routine:线程执行的函数,是一个返回值类型void*,参数类型void*的函数指针
arg:传入start_routine的参数,使用前后一般需要类型转换。
返回值:
RETURN VALUE
On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.
关于线程退出的问题:
同子进程退出,需要父进程回收,线程也需要被另外的线程回收。回收的原因如下:1. 一个线程退出后,对应的资源不会被释放,而是留存在地址空间中。一个进程能运行的线程数是有限的,如果不加以回收,可能会导致内存泄漏!2. 一个线程退出后,其它线程可能需要获取其执行任务的结果。
pthread_join
功能:
阻塞等待一个线程
接口:
int pthread_join(pthread_t thread, void **retval);
参数:
thread:线程ID
retval:指向的空间中存储的是线程返回的结果(注意类型转换),因为线程函数的返回结果是void*类型,所以要用二级指针接收。如果不关心回收线程的结果,则设置为nullptr。
返回值:
RETURN VALUE
On success, pthread_join() returns 0; on error, it returns an error number.
pthread_exit
线程函数中,可以直接用return退出线程并返回结果(可以被其它线程join接收)
void *run(void *arg)
{
int cnt = 5;
while (cnt--)
{
cout << "I am new thread" << endl;
sleep(1);
}
return nullptr; //
}
也可以用pthread_exit
函数。
void pthread_exit(void *retval); //和return一样,返回一个void*指针
Linux中,线程只有joinable和unjoinable两种状态。默认情况下,线程是joinable状态,该状态下的线程退出后,占有资源不会被释放,必须等待其它线程调用pthread_join回收它,释放资源,或者进程退出,资源全部被释放。当然,可以通过调用pthread_detach分离线程,将线程设置为unjoinable状态,使其无需被等待回收,退出即被系统自动释放资源。
pthread_detach
功能:
分离线程ID为thread的线程,使其无需被join等待。
接口:
int pthread_detach(pthread_t thread);
返回值:
RETURN VALUE
On success, pthread_detach() returns 0; on error, it returns an error number.
线程分离可以由别的线程分离,也可以自己分离。
pthread_self
功能:
获取当前线程的线程ID
接口:
pthread_t pthread_self(void);
⭕测试
void *run(void *arg)
{
int cnt = 10;
while(cnt--)
{
cout << "I am new thread, cnt: " << cnt << endl;
sleep(1);
}
pthread_exit(nullptr);
}
int main()
{
cout << "I am main thread" << endl;
pthread_t tid;
pthread_create(&tid, nullptr, run, nullptr);
int n = pthread_join(tid, nullptr);
if (n != 0)
{
cout << "join new thread fail!!" << endl;
exit(1);
}
cout << "join new thread success!!" << endl;
return 0;
}
主线程创建新线程后,调用pthread_join会阻塞等待新线程退出。运行结果如下:
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread
I am main thread
I am new thread, cnt: 9
I am new thread, cnt: 8
I am new thread, cnt: 7
I am new thread, cnt: 6
I am new thread, cnt: 5
I am new thread, cnt: 4
I am new thread, cnt: 3
I am new thread, cnt: 2
I am new thread, cnt: 1
I am new thread, cnt: 0
join new thread success!!
可以在主线程中detach线程ID为tid的新线程,也可以在新线程中detach自己。
void *run(void *arg)
{
//pthread_detach(pthread_self()); // 在新线程中detach自己
int cnt = 10;
while(cnt--)
{
cout << "I am new thread, cnt: " << cnt << endl;
sleep(1);
}
pthread_exit(nullptr);
}
int main()
{
cout << "I am main thread" << endl;
pthread_t tid;
pthread_create(&tid, nullptr, run, nullptr);
pthread_detach(tid); // 在主线程中detach线程ID为tid的新线程
int n = pthread_join(tid, nullptr);
if (n != 0)
{
cout << "join new thread fail!!" << endl;
exit(1);
}
cout << "join new thread success!!" << endl;
return 0;
}
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread
I am main thread
join new thread fail!! #等待失败,pthread_join无法等待已分离的线程,返回值非0
如果在新线程中detach自己,可能依然能够join成功。要想成功detach线程,必须在join之前detach,因为调用pthread_join函数时,已经将线程视为joinable并阻塞等待了,此后再detach是无效的。上面代码中,如果在新线程中detach自己,由于主线程和新线程调度的先后顺序不确定性,很可能线程先join再detach,此时的detach是无效的。
pthread_cancel
功能:
撤销(终止)一个线程ID为thread的线程
接口:
int pthread_cancel(pthread_t thread);
返回值:
RETURN VALUE
On success, pthread_cancel() returns 0; on error, it returns a nonzero error number.
撤销一个线程后,如果有另外的线程join该线程,那么其收到的退出结果是
PTHREAD_CANCELED
。
#define PTHREAD_CANCELED ((void *) -1)
⭕测试
void *run(void *arg)
{
while (true)
{
cout << "I am new thread" << endl;
sleep(1);
}
pthread_exit(nullptr);
}
int main()
{
cout << "I am main thread" << endl;
pthread_t tid;
pthread_create(&tid, nullptr, run, nullptr);
sleep(3);
pthread_cancel(tid);
void *ret = nullptr;
int n = pthread_join(tid, &ret);
if (n != 0)
{
cout << "join new thread fail!!" << endl;
exit(1);
}
if (ret == PTHREAD_CANCELED)
{
cout << "new thread is canceled" << endl;
}
cout << "join new thread success!!" << endl;
return 0;
}
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread
I am main thread
I am new thread
I am new thread
I am new thread
new thread is canceled #新线程被撤销了
join new thread success!!
pthread库的线程控制接口,都不是直接操作Linux底层的轻量级进程,而是操作用户级线程。pthread库将底层的轻量级进程封装成为用户级线程,用户看到的便是线程而不是所谓的轻量级进程。动态库load到进程的共享区中,因此,用户级线程的空间也是load到进程的共享区中,线程的大部分独立资源保存在这块空间中,包括线程栈。
线程库是怎么管理用户级线程的?
先描述再组织。 创建类似TCB的数据结构来描述线程,并将这些数据结构组织为一张表,如下。
前面使用接口获取到的线程tid,其实就是该线程的用户级页表的首地址,只不过将其转换成整型的格式。
int g_val = 100;
string toHex(pthread_t tid)
{
char buf[64];
snprintf(buf, sizeof(buf), "0x%x", tid);
return string(buf);
}
void *run(void *arg)
{
cout << toHex(pthread_self()) << endl;
pthread_exit(nullptr);
}
int main()
{
pthread_t t1;
pthread_t t2;
cout << "&g_val: " << &g_val <<endl;
pthread_create(&t1, nullptr, run, nullptr);
pthread_create(&t2, nullptr, run, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
return 0;
}
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread
&g_val: 0x6020cc #全局数据区
0x4b30f700 #共享区
0x4ab0e700 #共享区
全局变量默认是所有线程共享的,开发者需要处理多线程竞争问题。有些情况下我们需要保证一个线程独享一份数据,其它线程无法访问。这时候就要用到线程局部存储。gcc/g++编译环境中,可以用__thread
声明一个全局变量,从而每个线程都会独有一个该全局变量,存储在线程局部存储区中。
__thread int g_val = 0; //__thread修饰全局变量,可以理解为从进程的全局变量变成线程的全局变量
string toHex(pthread_t tid)
{
char buf[64];
snprintf(buf, sizeof(buf), "0x%x", tid);
return string(buf);
}
void *run(void *arg)
{
cout << "g_val: " << ++g_val << " " << "&g_val: " << &g_val << endl;
pthread_exit(nullptr);
}
int main()
{
pthread_t t1;
pthread_t t2;
pthread_t t3;
pthread_create(&t1, nullptr, run, nullptr);
pthread_create(&t2, nullptr, run, nullptr);
pthread_create(&t3, nullptr, run, nullptr);
cout << "g_val: " << ++g_val << " " << "&g_val: " << &g_val << endl;
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
return 0;
}
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread #使用了线程局部存储
g_val: 1 &g_val: 0x7fcb7cfcb77c
g_val: 1 &g_val: 0x7fcb7bf366fc
g_val: 1 &g_val: 0x7fcb7b7356fc
g_val: 1 &g_val: 0x7fcb7af346fc
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread #未使用线程局部存储
g_val: 1 &g_val: 0x6021d4
g_val: 2 &g_val: 0x6021d4
g_val: 3 &g_val: 0x6021d4
g_val: 4 &g_val: 0x6021d4
每个线程都有一个独立的栈结构,用于存储运行时的临时数据和压入函数栈帧。注意,主线程的栈就是进程地址空间中的栈。
ENDING…