1.线程控制
上一篇讲了线程/轻量级进程的概念,这篇讲讲线程的控制:退出,等待......
1.1 线程创建(pthread_create)
线程创建上一篇已经讲过了:
创建线程使用到的库函数接口,man pthread_create

- pthread_t *thread:线程标识符tid,是一个输出型参数。
- const pthread_attr_t* attr:线程属性,当前阶段一律设成nullptr。
- void* (*start_routine)(void *):是一个函数指针,线程执行的就是该函数中的代码。
- void* arg:传给线程启动函数的参数,是上面函数指针指向函数的形参。
- 返回值:线程创建成功返回0,失败返回错误码。
看一下上篇出现线程异常的场景:
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *threadRun(void *args)
{
const string name = (char *)args;
while (true)
{
cout << name << ", pid: " << getpid() << endl;
sleep(1);
static int cnt = 0;
if (cnt++ == 7)
{
int *p = nullptr;
*p = 777;
}
}
}
int main()
{
pthread_t tid[5];
char name[64];
for (int i = 0; i < 5; i++)
{
snprintf(name, sizeof(name), "%s-%d", "thread", i); // 特定内容格式化到name中
pthread_create(tid + i, nullptr, threadRun, (void *)name);
sleep(1); // 缓解传参的bug
}
while (true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}

出现异常全部线程都退出了,有没有办法只让其中一个线程退出?
试试exit();

还是全部线程都退出了,这也再次认识了 exit(); 就是终止进程的,所以不建议用
1.2 线程结束(pthread_exit)
基于前面,threadRun函数还有个是函数指针的返回值,返回个空指针试试:


可以发现名字为thread-1的线程好像退出了,其它线程没退出,就创建一个新线程看看:
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *threadRun(void *args)
{
const string name = (char *)args;
while (true)
{
cout << name << ", pid: " << getpid() << endl;
sleep(1);
static int cnt = 0;
if (cnt++ == 7)
{
// int *p = nullptr;
// *p = 777;
// exit(10);
return nullptr;
}
}
}
int main()
{
// pthread_t tid[5];
// char name[64];
// for (int i = 0; i < 5; i++)
// {
// snprintf(name, sizeof(name), "%s-%d", "thread", i); // 特定内容格式化到name中
// pthread_create(tid + i, nullptr, threadRun, (void *)name);
// sleep(1); // 缓解传参的bug
// }
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
while (true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}

可以发现新线程退出了,主线程还没退出。
POSIX线程库专门提供了一个接口来结束线程:pthread_exit()结束线程:man pthread_exit

- void* retval:返回线程结束信息,当前阶段设置成nullptr即可。
调用该接口的线程会结束。
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *threadRun(void *args)
{
const string name = (char *)args;
while (true)
{
cout << name << ", pid: " << getpid() << endl;
sleep(1);
static int cnt = 0;
if (cnt++ == 7)
{
// int *p = nullptr;
// *p = 777;
// exit(10);
//return nullptr;
pthread_exit(nullptr);
}
}
}
int main()
{
// pthread_t tid[5];
// char name[64];
// for (int i = 0; i < 5; i++)
// {
// snprintf(name, sizeof(name), "%s-%d", "thread", i); // 特定内容格式化到name中
// pthread_create(tid + i, nullptr, threadRun, (void *)name);
// sleep(1); // 缓解传参的bug
// }
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
while (true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}

同样,7秒后,新线程会调用该接口,然后就只剩下主线程了,新线程结束了。
1.3 线程等待(pthread_join)
和进程一样,线程也是需要等待的,如果不等待会造成内存泄漏,也就是结束掉的线程PCB不会被回收(类似僵尸进程),但是我们看不到没有回收的现象。
线程等待系统调用:

- 第一个参数pthread_t thread:要等待的线程tid。
- 第二个参数void** retval:线程结束的信息返回,这是一个输出型参数。
- 返回值:等待成功返回0,等待失败返回错误码。
演示一下使用:
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *threadRun(void *args)
{
const string name = (char *)args;
while (true)
{
cout << name << ", pid: " << getpid() << endl;
sleep(1);
static int cnt = 0;
if (cnt++ == 7)
{
pthread_exit((void*)777);
}
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
int *ret = nullptr;
pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出
cout << "main quit ...: new thead quit : " << (long long)ret << endl;
// linux下64位的,指针是8个字节,所以强转成long long 8个字节
while (true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}

可以看到,主线程在执行到线程等待的时候,会阻塞等待,不再往下执行,直到新线程都等待成功才会继续向下执行。

在主线程的栈区中有一个void类型的指针变量,新线程中返回的void类型指针会放到这个ret中。
- pthread线程库中有一个void** 类型的二级指针变量retval。
- pthread_join()系统调用将主线程中void*类型的指针变量的地址传给了pthread线程库中的二级指针变量,此时主线程就和线程库建立了联系。
- 将新线程中返回到线程库中的void*指针变量中的返回值,通过这种联系放到主线程中指针变量中----也就是 *retval = ret。
这样,我们就可以成功的获取到新线程退出时的返回信息了,桥梁就是pthread_join()系统调用。
在学习进程等待的时候,我们不仅可以获得进程的退出信息,还能获得进程的退出信号,但是在线程退出时就没有获得线程退出信号,这是为什么呢?
因为信号是发给进程的,整个进程都会被退出,线程要退出信号也没有意义了。
而且pthread_join默认是能够等待成功的,并不考虑异常的问题,异常是进程要考虑的事,线程不用考虑。
1.4 线程取消(pthread_cancel)
Linux提供了线程结束的其它方式:线程取消,线程取消的接口:

- 参数:要取消的线程tid。
- 返回值:取消成功返回0,失败返回错误码。
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *threadRun(void *args)
{
const string name = (char *)args;
while (true)
{
cout << name << ", pid: " << getpid() << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
int cnt = 0;
while (true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(1);
if(cnt++ == 5)
{
break;
}
}
pthread_cancel(tid);
cout << "pthread cancel: " << tid << endl;
int *ret = nullptr;
pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出
cout << "main quit ...: new thead quit : " << (long long)ret << endl;
// linux下64位的,指针是8个字节,所以强转成long long 8个字节
return 0;
}

可以看见,如果一个线程是被取消结束的,它的退出码就是-1。它其实是一个宏定义:#defin PTHREAD_CANCELED -1。
线程取消也是一种线程结束的方式,放在这里是为了能够通过线程等待看线程退出的退出码。
1.5 线程tid (pthread_self())
有没有看见退出得到的tid是一个很大的整数?这个整数实际上是一个地址。
我们还可以通过系统接口pthread_self在上面代码基础上打印自己的tid:



tid的值是一个地址:

我们知道,Linux内核中是没有线程概念的,也没有对应的TCB结构。
- 用户创建线程时使用的是POSIX线程库提供的接口。
- 线程库中会调用clone()系统调用接口,在内核中创建线程复用的PCB结构。
- 这些轻量级进程共用一个进程地址空间。
系统中肯定不只一个线程存在,大量的线程势必要管理起来,管理的方式同样是先描述再组织 。既然Linux内核中只有轻量级进程的PCB,那么描述线程的TCB结构就只能存在于线程库中。
线程库中的TCB里,存放着线程的属性,这里的TCB被叫做用户级线程。
Linux线程方案:用户级线程和用户关心的线程属性都在线程库中,内核提供线程执行流的调度。
一个线程的所有属性描述是由两部组成的,一部分就是在pthread线程库中的用户级线程,另一部分就是Linux中的轻量级进程,它们俩的比例大约是1比1。

pthread线程库从磁盘上加载到内存中后,通过页表再将虚拟地址空间和物理地址映射起来。
线程库最终是映射在虚拟地址空间中的共享区中的mmap区域。
线程库是映射在共享区的,那么线程库所维护的TCB结构也就一定在共享区。

如上图所示,将映射到共享区的动态线程库放大。
线程库中存在多个TCB结构来描述线程。每个TCB的地址就是线程id。
线程tid的本质就是虚拟地址共享区中TCB结构体的地址。
线程的栈也在共享区中,而不在栈中。
虚拟地址空间中的栈是主线程的栈,共享区中动态库中的栈是新线程的栈。
所以说,线程的栈结构是相互独立的,因为存在于不同的TCB中(主线程除外)。
1.6 线程局部存储(__thread)
在共享区线程库中的TCB里,有一个线程的局部存储属性,它是一个介于全局变量和局部变量之间线程特有的属性。
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int g_val = 0;
void *threadRun(void *args)
{
const string name = (char *)args;
while (true)
{
//cout << name << ", pid: " << getpid() << " tid: " << pthread_self() << endl;
cout << name << " -> g_val: " << g_val++ << " &g_val: " << &g_val << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
int cnt = 0;
while (true)
{
cout << "main thread -> g_val: " << g_val++ << " &g_val: " << &g_val << endl;
sleep(1);
if(cnt++ == 5)
{
break;
}
}
pthread_cancel(tid);
cout << "pthread cancel: " << tid << endl;
int *ret = nullptr;
pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出
cout << "main quit ...: new thead quit : " << (long long)ret << endl;
// linux下64位的,指针是8个字节,所以强转成long long 8个字节
return 0;
}

主线程和新线程打印的地址都是一样的,说明主线程和新线程共用一个全局变量。
那如果此时新线程仍然想用这个变量名,但是又不想影响其他线程,也就是让这个全局变量独立出来,该怎么办呢?此时就可以使用线程的局部存储属性了。
在全局变量 g_val前面加__thread (两个下划线),此时这个全局变量就具有了局部存储的属性。


主线程和新线程打印出来的全局变量的地址不相同了,说明此时用的并不是同一个全局变量。
而且新线程修改这个值,主线程不受影响。
将全局变量或者static变量添加 __thread,设置位线程局部存储。
此时每个线程的TCB中都会有一份该变量,相互独立,并不会互相影响。
1.7 线程分离(pthread_detach)
前面线程等待的时候,主线程就需要阻塞式等待线程的释放,主线程什么都干不了。能不能像进程那样不需要阻塞式等待(将SIGCHID信号设置为忽略),等新线程结束以后自动释放呢?(尤其是不需要关心线程返回值的时候,join是一种负担。)
当然可以,将需要自动释放的线程设置成分离状态,将线程设置成分离状态意味着不需要主线程再关心该线程的状态,它会自动释放。
线程分离的接口:man pthread_detach:

- 参数 pthread_t thread:要分离的线程tid。
- 返回值 int:成功返回0,不成功返回错误码。
可以是线程组内其他线程对目标线程进行分离,但一般是线程自己分离自己:
cpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
using namespace std;
__thread int g_val = 0;
void *threadRun(void *args)
{
pthread_detach(pthread_self());
const string name = (char *)args;
while (true)
{
//cout << name << ", pid: " << getpid() << " tid: " << pthread_self() << endl;
cout << name << " -> g_val: " << g_val++ << " &g_val: " << &g_val << endl;
sleep(5);
pthread_exit((void*)777);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
int cnt = 0;
while (true)
{
cout << "main thread -> g_val: " << g_val++ << " &g_val: " << &g_val << endl;
sleep(1);
if(cnt++ == 5)
{
break;
}
}
int *ret = nullptr; // 新线程自己分离了,但是主线程非要等待呢?
int n = pthread_join(tid, (void **)&ret); // 默认会阻塞等待新线程退出
if(n == 0)
{
cout << "main quit ...: new thead quit : " << (long long)ret << endl;
}
else
{
cout << "n :" << n << "errstring: " << strerror(n) << endl;
}
// linux下64位的,指针是8个字节,所以强转成long long 8个字节
return 0;
}

可以看到,此时主线程在进行线程等待的时候就会失败,而且返回错误码。
2. C++的多线程
C++也是可以多线程编程的,而且提供了多线程的库,而无论什么编程语言,什么库,在Linux系统上的多线程本质上都是对pthread原生线程库的封装。

简单演示一下:
Makefile:
cpp
mythread:mythread.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f mythread
cpp
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;
void fun()
{
while(true)
{
cout << "hello new thread" << endl;
sleep(1);
}
}
int main()
{
std::thread t(fun);
std::thread t1(fun);
std::thread t2(fun);
std::thread t3(fun);
std::thread t4(fun);
while(true)
{
cout << "hello main thread" << endl;
sleep(1);
}
t.join();
}

此时就发现运行不了了,改下Makefile:
cpp
mythread:mythread.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f mythread

此时程序就能正常运行了,演示这个主要为了说明无论什么编程语言,什么库,在Linux系统上的多线程本质上都是对pthread原生线程库的封装。