Linux线程控制

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:设置线程的属性attrNULL 表示使用默认属性。

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 获得。

线程终止和等待

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 线程函数调用 return 。这种方法对主线程不适用 ,从 main 函数 return 相当于调用exit

  2. 线程可以调用 pthread_ exit 终止自己。

  3. 一个线程可以调用 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 ;失败返回错误码。

调用该函数的线程将挂起等待,直到 idthread 的线程终止。

结合之前的接口进行测试:

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 得到的终止状态是不同的,总结如下:

  1. 如果 thread 线程通过 return 返回pthread_join 的实参 value_ ptr 所指向的单元里存放的是 thread 线程函数的返回值

  2. 如果 thread 线程被别的线程 调用 pthread_cancel 异常终结value_ ptr指向的单元里存放的是常数 PTHREAD_ CANCELED ,这个宏是 -1 的另一种解释。

  3. 如果 thread 线程是自己调用 pthread_exit 终止的value_ptr 所指向的单元存放的是传给 pthread_exit 的参数。

  4. 如果 thread 线程的终止状态不感兴趣 ,可以传 NULLvalue_ ptr 参数。

之前已经验证了 returnpthread_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 进行描述。

相关推荐
Brilliantwxx1 小时前
【Linux】 开发|Git 版本控制与 gdb/cgdb 调试
linux·笔记·git
allforgood1 小时前
管理 SELinux 安全性
linux·运维·网络
Darkwanderor1 小时前
多线程的互斥问题和互斥锁解决互斥问题
linux·c语言·c++
键盘会跳舞1 小时前
C++:std::pair 源码级深度剖析 —— 关联容器的基石
开发语言·c++·pair·关联容器
(Charon)2 小时前
【C++】手写线程安全队列:mutex + condition_variable 实现生产者消费者模型
c语言·开发语言·c++
数智工坊2 小时前
UECP:不确定性增强的协同感知 | 基于物理先验的BEV融合新范式
linux·ubuntu·3d·机器人
ltl11 小时前
内核追踪:ftrace、kprobe、uprobe、tracepoint 生产实战
linux
·薯条大王11 小时前
经济实惠玩云服务器|一台云服务器多人共用,子账号配置教程
java·linux·运维·服务器·汇编·c++·python
小小、码农14 小时前
Linux进程控制:从fork到exec,手写一个简易Shell
linux·运维·服务器