Linux线程控制
Linux线程控制
POSIX 线程库:与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以 "pthread_" 开头的。
要使用这些函数库,就需要先通过引入头文 pthread.h ,链接这些线程函数库时要使用编译器命令的 -lpthread 选项,因为 pthread 库是动态库。
对线程的控制 ,可以参考进程控制的概念,就是对线程从创建 、工作 和终结 这一整个生命周期的宏观调控。
pthread_create创建线程
功能:创建一个新的线程。
原型(参考 man 3 pthread_create ):
c
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;失败返回错误码。
错误检查:
-
传统的一些函数是,成功返回 0,失败返回 -1,并且对全局变量
errno赋值以指示错误。 -
pthreads函数出错时不会设置全局变量errno,而是将错误代码通过返回值返回。而大部分其他 POSIX 函数会这样做。 -
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小。
这个函数将展示每个线程的 errno 不是同一个的情况。
c
#include <errno.h>
#include <iostream>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <unistd.h>
using std::cerr;
void *thread_func(void *arg) {
int fd = -1;
sleep(2);
// 故意执行一个会失败的系统调用
if (write(fd, "test", 4) == -1) {
// 直接使用 errno 即可,它指向的是本线程的私有存储
fprintf(stderr, "Thread %ld: write failed with errno = %d (%s)\n",
*((int *)arg), errno, strerror(errno));
}
return NULL;
}
int main() {
pthread_t t1, t2;
int id1 = 1, id2 = 2;
pthread_create(&t1, NULL, thread_func, &id1);
sleep(1);
cerr << errno << "\n";
sleep(2);
pthread_create(&t2, NULL, thread_func, &id2);
sleep(1);
cerr << errno << "\n";
sleep(2);
cerr << errno << "\n";
return 0;
}
交互界面:
cpp
[Bjarne@VM-8-8-centos cppTest]$ make
g++ a.cpp -o a.exe -std=c++11 -g -lpthread
./a.exe
0
Thread 1: write failed with errno = 9 (Bad file descriptor)
0
Thread 2: write failed with errno = 9 (Bad file descriptor)
0
[Bjarne@VM-8-8-centos cppTest]$
若是多个线程 ,则哪一个线程会先被调度取决于操作系统内的调度算法。操作系统调度多个线程的本质是会以纳秒或皮秒级别的时间间隔,受理硬件发送给 CPU 的时钟中断。
所以理论上存在一种操作,这个操作会关掉或阻止时钟中断 将信息传输给 CPU ,使得 CPU 的资源长期被某个线程给占据 。这种操作是实现操作系统内核并发控制的基石之一,它被称为中断屏蔽(Interrupt Masking)。后续若有机会会单独开一篇。
pthread_self获取自身ID
线程库的后续操作是根据该线程 ID 来操作线程的。线程库 NPTL 提供了 pthread_ self 函数,可以获得线程自身的 ID :
cpp
#include <pthread.h>
pthread_t pthread_self(void);
pthread_t 是什么类型,取决于实现。对于 Linux 目前实现的 NPTL 实现而言,pthread_t 类型的线程ID,本质就是一个进程地址空间上的一个地址。目前通过 vscode 了解到有定义 typedef unsigned long int pthread_t; ,而 long 在不同的操作系统平台下,有的是 4 byte ,有的则是 8 byte 。
示例:
cpp
#include <cstdio>
#include <cstring>
#include <errno.h>
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using std::cout;
void *thread_func(void *arg) {
cout << (char *)(arg) << ' ' << pthread_self() << '\n';
}
int main() {
pthread_t tid = pthread_t();
pthread_create(&tid, nullptr, thread_func, (void *)("thread1"));
sleep(2);
cout << "main thread" << ' ' << pthread_self() << '\n';
cout << "thread1 tid" << ' ' << tid << '\n';
return 0;
}
根据输出结果,tid 和子线程调用 pthread_self 得到的线程 ID 是一样的。
bash
[Bjarne@VM-8-8-centos cppTest]$ make
g++ a.cpp -o a.exe -std=c++11 -g -lpthread
./a.exe
thread1 140004570998528 # 等于thread1 tid
main thread 140004588386112
thread1 tid 140004570998528 # 等于thread1
[Bjarne@VM-8-8-centos cppTest]$
轻量级进程 ID (LWP ID) 与进程 ID (PID) 的区别:
- 进程 ID 是整个线程组,或整个进程的唯一标识符。所有同组线程调用
getpid会返回同一个进程的进程 ID 。 - 轻量级进程 ID (LWP ID) 则是 PCB (Linux 中对应
task_struct) 中的pid字段,用于标识单个线程或单个轻量级进程。在 Linux ,每个线程都有独立的轻量级进程 ID ,可通过gettid获得。
线程终止和等待
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
-
线程函数调用
return。这种方法对主线程不适用 ,从main函数return相当于调用exit。 -
线程可以调用
pthread_ exit终止自己。 -
一个线程可以调用
pthread_ cancel终止同一进程中的另一个线程。
任何线程调用 exit ,相当于终止进程。且线程没有异常退出,因为获取异常没意义,线程一旦出问题,主线程就跟着挂掉了,所以这个函数连返回的机会都没有。
pthread_exit自我终结
功能:让线程能做到自我终结。
原型(参考 man 3 pthread_exit):
c
#include <pthread.h>
void pthread_exit(void *value_ptr);
value_ptr:返回一个和线程退出信息有关的对象的地址 。value_ptr 不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。
pthread_exit 或者 return 返回的指针所指向的内存单元 必须是全局 的或者是用 malloc 在堆区分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
详细使用见后续代码。
pthread_cancel取消线程
功能:向线程发送取消请求。
原型(参考 man 3 pthread_cancel):
c
#include <pthread.h>
int pthread_cancel(pthread_t thread);
thread:要取消的线程的 ID。
返回值:成功返回 0 ;失败则返回错误码。
使用参考:
cpp
#include <cstdio>
#include <cstring>
#include <errno.h>
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using std::cout;
void *thread_func(void *arg) {
while (1)
;
}
int main() {
pthread_t tid = pthread_t();
pthread_create(&tid, nullptr, thread_func, (void *)("thread1"));
int t = 4;
while (t--) {
cout << t << ' ';
fflush(stdout); // 不刷新缓冲区的话会等结束后再一次性输出
sleep(1);
}
t = pthread_cancel(tid); // 取消线程
cout << "\n被取消的线程的返回值:" << t << "\n";
return 0;
}
输出:
bash
[Bjarne@VM-8-8-centos cppTest]$ make
g++ a.cpp -o a.exe -std=c++11 -g -lpthread
./a.exe
3 2 1 0
被取消的线程的返回值:0
[Bjarne@VM-8-8-centos cppTest]$
若新线程取消主线程(main 函数所在的线程),部分 Linux 会继续等待新线程终结。
cpp
#include <cstdio>
#include <cstring>
#include <errno.h>
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using std::cout;
pthread_t mainid;
void *f(void *args) {
cout << "子线程即将取消主线程\n";
sleep(2);
pthread_cancel(mainid);
cout << "子线程取消了主线程\n";
for (int i = 1; i <= 5; i++) {
cout << "子线程在运行ing\n";
sleep(1);
}
return nullptr;
}
int main() {
pthread_t tid;
mainid = pthread_self();
pthread_create(&tid, nullptr, f, nullptr);
while (1) {
cout << "主线程在运行ing\n";
sleep(1);
}
return 0;
}
输出:
bash
[Bjarne@VM-8-8-centos cppTest]$ make
g++ a.cpp -o a.exe -std=c++11 -g -lpthread
./a.exe
主线程在运行ing
子线程即将取消主线程
主线程在运行ing
主线程在运行ing
子线程取消了主线程
子线程在运行ing
子线程在运行ing
子线程在运行ing
子线程在运行ing
子线程在运行ing
[Bjarne@VM-8-8-centos cppTest]$
也有部分 Linux ,主线程承担着特殊的角色。当主线程退出时,无论其他线程是否还在运行,C 运行时库通常会通过调用 exit() 来终止整个进程 。这意味着,如果新线程取消了主线程,且没有其他存活的线程,进程就会立刻结束。如果有其他线程在运行,主线程退出同样会触发进程终止,这些线程也会被一并强制结束。
pthread_join线程等待
需要线程等待是因为:
-
已经退出的线程 ,其空间没有被释放,仍然在进程的地址空间内。
-
创建新的线程不会复用 刚才退出线程的地址空间。
一个子进程退了,但父进程没退,则它的 PCB 会暂时处于一个僵尸状态。类似的,线程默认返回的时候会被等待 。线程如果退出 ,没有让主线程去等待 线程的话,也会导致类似进程的僵尸问题,但很难被观测到。
这里可通过 pthread_join 等待线程并获取线程的返回值。
功能:等待线程结束,并通过输出型参数从线程内带回数据。
原型:
c
#include <pthread.h>
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:要等待的线程 ID 。value_ptr:它指向一个指针,后者指向线程的返回值。若上传nullptr,则不带回返回值。
返回值:成功返回 0 ;失败返回错误码。
调用该函数的线程将挂起等待,直到 id 为 thread 的线程终止。
结合之前的接口进行测试:
cpp
#include <cstdio>
#include <cstring>
#include <errno.h>
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using std::cout;
void *thread_fib(void *arg) {
long long dp[101] = {0, 1};
for (int i = 2; i <= 100; i++)
dp[i] = dp[i - 1] + dp[i - 2];
cout << "算法thread_fib完成,即将返回\n";
return (new long long(114514)); // 返回值不可为局部变量
}
void *thread_fib2(void *arg) {
long long dp[101] = {0, 1};
for (int i = 2; i <= 100; i++)
dp[i] = dp[i - 1] + dp[i - 2];
cout << "算法thread_fib2完成,即将返回\n";
pthread_exit((void *)"2345678"); // 字符串常量存储在静态区
}
int main() {
pthread_t tid1 = pthread_t(), tid2 = pthread_t();
void *p1 = nullptr, *p2 = nullptr;
int t1 = pthread_create(&tid1, nullptr, thread_fib, nullptr);
int pj1 = pthread_join(tid1, &p1);
cout << (*((long long *)(p1))) << '\n';
cout << "线程1的2个系统调用的返回值:" << t1 << ' ' << pj1 << '\n';
t1 = pthread_create(&tid2, nullptr, thread_fib2, nullptr);
pj1 = pthread_join(tid2, &p2);
cout << (char *)(p2) << '\n';
cout << "线程2的2个系统调用的返回值:" << t1 << ' ' << pj1 << '\n';
return 0;
}
交互界面:
bash
[Bjarne@VM-8-8-centos cppTest]$ make
g++ a.cpp -o a.exe -std=c++11 -g -lpthread
./a.exe
算法thread_fib完成,即将返回
114514
线程1的2个系统调用的返回值:0 0
算法thread_fib2完成,即将返回
2345678
线程2的2个系统调用的返回值:0 0
[Bjarne@VM-8-8-centos cppTest]$
thread 线程以不同的方法终止,通过 pthread_join 得到的终止状态是不同的,总结如下:
-
如果
thread线程通过return返回 ,pthread_join的实参value_ ptr所指向的单元里存放的是thread线程函数的返回值。 -
如果
thread线程被别的线程 调用pthread_cancel异常终结 ,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED,这个宏是 -1 的另一种解释。 -
如果
thread线程是自己调用pthread_exit终止的 ,value_ptr所指向的单元存放的是传给pthread_exit的参数。 -
如果对
thread线程的终止状态不感兴趣 ,可以传NULL给value_ ptr参数。
之前已经验证了 return 和 pthread_exit 的情况,这里验证 pthread_cancel 的情况。
c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void *thread3(void *arg) {
while (1) {
printf("thread 3 is running ...\n");
sleep(1);
}
return NULL;
}
int main(void) {
pthread_t tid;
void *ret;
// thread 3 cancel by other
pthread_create(&tid, NULL, thread3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &ret);
if (ret == PTHREAD_CANCELED) // 这个宏是-1
printf("线程返回, 线程 id %X, 返回值:PTHREAD_CANCELED\n", tid);
else
printf("线程返回, 线程 id %X, 返回值:NULL\n", tid);
return 0;
}
运行结果:
bash
[Bjarne@VM-8-8-centos cppTest]$ make
g++ a.cpp -o a.exe -std=c++11 -g -lpthread
./a.exe
thread 3 is running ...
thread 3 is running ...
thread 3 is running ...
线程返回, 线程 id E5AB4700, 返回值:PTHREAD_CANCELED
[Bjarne@VM-8-8-centos cppTest]$
线程阻塞等待
这里创建出一个类似线程等待的场景:
cpp
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using std::cout;
void *thread3(void *arg) {
while (1) {
printf("thread 3 is running ...\n");
sleep(1);
}
return NULL;
}
int main(void) {
pthread_t tid;
void *ret;
// thread 3 cancel by other
pthread_create(&tid, NULL, thread3, NULL);
pthread_join(tid, nullptr);
cout << "debug\n";
return 0;
}
这里 debug 不会被输出。因为线程是个死循环,永远不会结束,主线程会一直阻塞在 pthread_join 处。所以在线程是死循环的情况下 ,主线程调用 pthread_join ,则会进行阻塞等待。
bash
[Bjarne@VM-8-8-centos cppTest]$ make
g++ a.cpp -o a.exe -std=c++11 -g -lpthread
./a.exe
thread 3 is running ...
thread 3 is running ...
thread 3 is running ...
thread 3 is running ...
thread 3 is running ...
thread 3 is running ...
thread 3 is running ...
^Cmake: *** [a.exe] Interrupt
[Bjarne@VM-8-8-centos cppTest]$
pthread_detach线程分离
默认情况下,新创建的线程是 joinable 的。在 POSIX 线程(pthread)中,每个线程创建时默认处于 joinable (可连接 ) 状态。这意味着线程退出后 ,需要对其进行 pthread_join 操作 ,否则无法释放资源,从而造成系统的资源泄漏。这种情况类似于 "僵尸进程" 的问题(占着茅坑不拉sh)。
做软件开发的时候,很多软件是一启动就不退出的,除非用户想把它关掉。一个软件起来之后,它一直在运行,这才是常态。
当任务到来时,或者是有客户想要完成某种任务时,就可以创建一个线程。新线程处理任务时,主线程可以等待(
pthread_join),也可以做其他任务。
若不关心线程的返回值,join 是一种负担,这个时候,我们可以告诉系统 ,当线程退出时 ,自动释放线程资源 。这个行为模式叫线程分离。
分离线程可通过 pthread_detach 实现。
c
#include <pthread.h>
int pthread_detach(pthread_t thread);
pthread_detach 函数将由 thread 标识的线程标记为已分离 。当已分离的线程终止 时,其资源会自动释放回系统,无需其他线程与该已终止的线程连接 。thread 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
joinable 和分离是冲突的,一个线程不能既是 joinable 又是分离的。例如这个测试样例,会输出等待失败。
c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void *thread_run(void *arg) {
pthread_detach(pthread_self()); // 自我分离
printf("%s\n", (char *)arg);
return NULL;
}
int main(void) {
pthread_t tid;
if (pthread_create(&tid, NULL, thread_run, (void *)("thread1 run...")) !=
0) {
printf("创建线程错误\n");
return 1;
}
int ret = 0;
sleep(1); // 很重要,要让线程先分离,再等待
// 线程tid已自我分离,此时只会等待失败
if (pthread_join(tid, NULL) == 0) {
printf("线程等待成功\n");
ret = 0;
} else {
printf("线程等待失败\n");
ret = 1;
}
return ret;
}
输出:
bash
[Bjarne@VM-8-8-centos cppTest]$ make
g++ a.cpp -o a.exe -std=c++11 -g -lpthread
./a.exe
thread1 run...
线程等待失败
make: *** [a.exe] Error 1
[Bjarne@VM-8-8-centos cppTest]$
线程的局部性存储
pthread 库在共享区开辟的描述线程的内存块中还有一个线程局部存储。线程局部存储 (Thread-Local Storage,TLS) 是一种变量存储模型,它使得程序中的变量在每个线程中都拥有独立的副本 。

一般情况下全局变量是会被所有线程共享。但可以增加修饰前缀 __thread ,表示每个线程都拥有一份独属于自身的全局变量副本。
例如这个代码,给全局变量 p 加修饰前缀 __thread ,和另一个全局变量 q 做对比。
cpp
#include <cstdio>
#include <cstring>
#include <errno.h>
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using std::cout;
using std::string;
__thread int p = 114514;
int q = 666;
void *f(void *args) {
p--;
q--;
cout << string((char *)(args)) << p << ' ' << q << "\n";
cout << "新线程的&p,&q=" << &p << ' ' << &q << "\n";
sleep(1);
cout << string((char *)(args)) << p << ' ' << q << "\n";
cout << "新线程的&p,&q=" << &p << ' ' << &q << "\n";
return nullptr;
}
int main() {
pthread_t tid;
pthread_create(&tid, nullptr, f, (void *)("新线程的p,q="));
sleep(1);
++p;
++q;
cout << "主线程的p,q=" << p << ' ' << q << "\n";
cout << "主线程的&p,&q=" << &p << ' ' << &q << "\n";
sleep(1);
return 0;
}
交互界面:
bash
[Bjarne@VM-8-8-centos cppTest]$ make
g++ a.cpp -o a.exe -std=c++11 -g -lpthread
./a.exe
新线程的p,q=114513 665
新线程的&p,&q=0x7f07b0bb66fc 0x6020b4
主线程的p,q=114515 666
主线程的&p,&q=0x7f07b1c4b77c 0x6020b4
新线程的p,q=114513 666
新线程的&p,&q=0x7f07b0bb66fc 0x6020b4
[Bjarne@VM-8-8-centos cppTest]$
可以看到,q 依旧被所有线程共享,但 p 却是每个线程独有。
但修饰前缀 __thread __thread 是 gcc 早期提供的线程局部存储扩展,它要求修饰的变量具有静态初始化和平凡析构的特性 。而 std::string 拥有非平凡的构造函数和析构函数,它需要在运行时动态分配内存、管理资源,因此无法直接使用 __thread 修饰,否则就会编译错误。
bash
[Bjarne@VM-8-8-centos cppTest]$ make
g++ a.cpp -o a.exe -std=c++11 -g -lpthread
a.cpp:11:17: error: non-local variable 'st' declared '__thread' needs dynamic initialization
__thread string st;
^
a.cpp:11:17: note: C++11 'thread_local' allows dynamic initialization and destruction
make: *** [a.exe] Error 1
[Bjarne@VM-8-8-centos cppTest]$ cat a.cpp
# 这里省略头文件等信息
__thread string st;
int main() {
return 0;
}[Bjarne@VM-8-8-centos cppTest]$
C++11 标准引入了 thread_local 关键字可以实现动态 TLS。这些将在 C++11 进行描述。