humid1ch blogs

本篇文章

手机用户建议
PC模式 或 横屏
阅读


Linux系统 2023 年 4 月 14 日

[Linux] 多线程控制分析:获取线程ID、线程退出分析、自动回收线程、线程分离...

我们知道, 进程有自己相关控制接口, 等待、创建等 而线程作为轻量级的进程, 其实也是有控制接口的.
Linux系统中, 线程是轻量级的进程. 我们已经介绍过了线程的相关概念, 见过了线程再Linux操作系统中的存在形式.
我们知道, 进程有自己相关控制接口, 等待、创建等
而线程作为轻量级的进程, 其实也是有控制接口的.

线程控制

在介绍线程的相关概念的时候, 我们简单的演示了一下, 线程的创建和回收. 以及使用ps命令 展示了操作系统中正在运行的线程.

线程的创建与回收演示

使用 pthread_create()pthread_join() 两个接口来创建和回收线程已经演示过了:
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void* callBack1(void* args) {
    string str = (char*)args;
    while (true) {
        cout << str << ": " << getpid() << " " << endl;
        sleep(1);
    }
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");

    while (true) {
        cout << " 主线程运行: " << getpid() << " " << endl;
        sleep(1);
    }

    pthread_join(tid1, nullptr);

    return 0;
}
这段代码的执行结果, 是两个线程同时运行.
pthread_create() 的使用并不复杂, 只需要接收线程id, 并指定线程需要执行的回调函数 和 参数就可以了.
pthread_join() 这个回收线程的函数也不复杂. 只不过 此函数的第二个参数是一个 二级指针.
int pthread_join(pthread_t thread, void** retval);
void** retval 参数是一个输出型参数, 是用来接收数据的.
不过为什么, retval是一个二级指针的类型呢?
其实原因很简单, 因为我们在使用 pthread_create() 接口创建线程的时候, 给线程指定的回调函数的返回值是 void* 类型的.
pthread_join() 的作用是回收线程, 既然要回收线程那么就一定要接收到线程运行的结果, 即 需要接收 线程执行的回调函数的返回值.
函数的参数要接收一个指针类型的内容, 就是要用二级指针来接收.
即, pthread_join() 接口的第二个参数, 实际上可以接收线程执行的回调函数的返回值.
我们可以使用下面这段代码测试一下:
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void* callBack1(void* args) {
    string str = (char*)args;
    int cnt = 5;
    while (cnt) {
        cout << str << ": " << getpid() << " " << endl;
        sleep(1);
        cnt--;
    }

    return (void*)"thread_1 is over";
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");

    void* ret = nullptr;
    pthread_join(tid1, &ret);

    cout << "main thread join thread_1 , ready to print thread_1 ret" << endl; 

    sleep(2);
    cout << (char*)ret << endl;
    
    return 0;
}
这段代码的执行结果是:
thread_join_retval
thread_join_retval
可以看到, 我们输出传入 join接口的参数, 得到的结果是 thread_1 执行的回调函数的返回值.

pthread_create()pthread_join() 这两个接口的使用都不算困难.
其实这两个接口都不是系统调用, 而是第三方库 pthread 中的接口. 不过虽然他们不是系统调用, 却胜似系统调用. 因为 Linux中其实是必须装载这个库的.
这两个不是系统调用, Linux操作系统提供的真正的系统调用是什么呢?
Linux操作系统的线程是轻量级进程, 也就是说 Linux其实并没有提供创建线程的系统调用, 因为根本就没有独立的线程这个概念.
所以, Linux操作系统给我们提供的创建线程的系统调用接口:
vfork():
这个系统调用接口是用来创建与父进程共享进程地址空间的子进程的, 其实就是一个线程.
clone():
还有clone() 系统调用. 这个接口作用是创建一个子进程来模拟线程.
不过看参数就可以看出来这个函数, 太麻烦了!
还需要自己定义方法, 自己定义一个子进程的栈(线程栈)等内容.
这个接口是为了更加细粒度的定制创建一个线程. 就是太麻烦了. 非常的麻烦. 看看就好.
我们更常用的还是 pthread库中的接口, pthread_create

获取线程id

pthread_create() 接口的第一个参数是一个输出型参数, 其实就是为了接收线程id的.
也就是说, 创建完线程之后 其实就已经得到了创建的线程的id:
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void* callBack1(void* args) {
    string str = (char*)args;
    int cnt = 5;
    while (cnt) {
        cout << str << ": " << getpid() << " " << endl;
        sleep(1);
        cnt--;
    }

    return (void*)"thread_1 is over";
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");

    cout << "thread_1 id: " << tid1 << endl;

    while (true) {
        cout << " 主线程运行: " << getpid() << " " << endl;
        sleep(1);
    }

    return 0;
}
thread_id
thread_id
代码的执行结果显示, 线程的id是一个非常长的数值. 我们暂时不考虑其有什么含义.
上面这种方法是通过创建线程时接收到的id 来获取线程id.
除此之外, pthread 还提供了一个获取线程自己id的接口: pthread_self()

pthread_self() 获取线程id

此接口作用是:获取调用此接口的线程的ID, 并作为返回值返回.
那么, 我们就可以定义一个函数, 来输出哪个线程和此线程的ID.
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t &tid) {
    cout << threadName << " is runing, " << "tid: " << tid << endl;
}

void* callBack1(void* args) {
    char* threadName = (char*)args;
    int cnt = 5;
    while (cnt) {
        printTid(threadName, pthread_self());
        sleep(1);
        cnt--;
    }

    return (void*)"thread_1 is over";
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");

    while(true) {
        cout << "主线程运行: " << getpid() << endl;
        sleep(1);
    }

    pthread_join(tid1, nullptr);

    return 0;
}
代码的执行结果为:
pthread_self
pthread_self
线程成功获取了自己的ID

线程的状态

Linux中 不能使用ps像查看进程状态那样细致的查看线程的状态.
不过还是可以简单的判断一下的.
对于线程, 如果线程退出了但是没有回收, 线程会怎么样呢?
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t &tid) {
    cout << threadName << " is runing, " << "tid: " << tid << endl;
}

void* callBack1(void* args) {
    char* threadName = (char*)args;
    int cnt = 5;
    while (cnt) {
        printTid(threadName, pthread_self());
        sleep(1);
        cnt--;
    }
    cout << "thread_1 is over" << endl;

    int* ret = new int(123);
    return (void*)ret;				// 返回一个堆区数据 123
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");
    
    sleep(15);  // 等15s,让thread_1运行完

    void* ret = nullptr;
    pthread_join(tid1, &ret);
    cout << "main thread join thread_1 , ready to print thread_1 ret" << endl;
    
    sleep(2);

    cout << "print thread_1 ret: " << *((int*)ret) << endl;
    delete (int*)ret; 		// 释放堆区数据

    return 0;
}
我们可以使用命令行监控脚本, 然后在执行这段代码.
// 监控线程脚本
while :; do ps -aL |head -1 && ps -aL |grep myThread |grep -v grep; sleep 1; done;
线程退出之后, 但还未join
线程退出之后, 但还未join
执行和监控结果是, 当线程退出但还未被回收时, 线程会立刻从ps打印的列表中消失.
这说明什么呢?能说明 线程会被操作系统自动回收, 我们不用join吗?
不能, 因为ps终究只是一个软件. 没有证据可以证明 线程退出之后会被立刻回收, 不用手动回收.
事实也的确如此, 线程退出之后, 并不是自动回收的, 如果不手动join, 就可能会造成类似进程不回收一般的内存泄漏问题

线程与进程共享信号处理方法

线程和进程是共享信号处理方法的. 这个概念在上一篇线程概念的文章中就已经提到过, 但是没有演示.
在本篇文章中, 我们可以演示一下这个特点.
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t &tid) {
    cout << threadName << " is runing, " << "tid: " << tid << endl;
}

void* callBack1(void* args) {
    char* threadName = (char*)args;
    while (true) {
        printTid(threadName, pthread_self());s
        sleep(1);
     }
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");

    while(true) {
        printTid("main thread", pthread_self());
        sleep(1);
    }
}
线程和进程接收信号相同状态
线程和进程接收信号相同状态
我们向进程发送19信号, 所有线程都会暂停运行.
我们向进程发送一个18信号, 所有线程又会恢复运行.

ps列表中, 表示进程状态的一栏 l 即表示 存在轻量化进程, 即多线程进程

+ 表示前台进程

线程异常

线程异常会影响整个进程, 原因是线程异常影响的是整个进程的代码和数据.
所以, 如果线程异常 则整个进程都会异常
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t &tid) {
    cout << threadName << " is runing, " << "tid: " << tid << ", pid: " << getpid() << endl;
}

void* callBack1(void* args) {
    char* threadName = (char*)args;
    int cnt = 5;
    while (true) {
        printTid(threadName, pthread_self());
        sleep(1);
        cnt--;
        if(cnt == 0) {
            int i = 1;
            i /= 0;
            // 段错误
            //int* pi = nullptr;
            //*pi = 123;
        }
     }
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");

    while(true) {
        printTid("main thread", pthread_self());
        sleep(1);
    }
    return 0;
}
线程浮点异常
线程浮点异常
线程发生段错误
线程发生段错误
当线程出现不同的异常, 会影响进程的终止.
这其实是线程的 健壮性较低

不过这其实还是因为出bug了.

线程退出

介绍进程时, 我们分析进程退出存在三种情况:
  1. 代码跑完, 结果正确, 正常退出
  2. 代码跑完, 结果不正确, 正常退出
  3. 代码没跑完, 进程异常退出
其实线程也是一样的, 线程退出也分为这三种.
其实, 这三种情况可以统称为 执行流的退出情况
在父子进程中, 子进程退出不论是正常退出还是异常退出, 都会向父进程发送退出信息.
而 线程中, 只有线程正常退出且回收时, 主线程可以接收到线程的退出信息.
那么, 为什么线程异常时 主线程不会接收到来自线程的退出信息?
答案其实很简单, 因为线程异常, 也就是进程异常, 进程也会随之退出. 接不接受线程的退出信息已经没有意义了

以正常退出的情况来说, 除了回调函数内 return.
线程退出还可以使用接口退出.

pthread_exit()

pthread_exit() 接口也是 pthread 库提供的, 作用就是 以指定的退出信息使线程退出.
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t &tid) {
    cout << threadName << " is runing, " << "tid: " << tid << ", pid: " << getpid() << endl;
}

void* callBack1(void* args) {
    char* threadName = (char*)args;
    int cnt = 5;
    while (true) {
        printTid(threadName, pthread_self());
        sleep(1);
        cnt--;
        if(cnt == 0) {
            pthread_exit((void*)123);
        }
     }
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");
    sleep(10);

    void* ret = nullptr;
    pthread_join(tid1, &ret);
    cout << "main thread join thread_1 , ready to print thread_1 ret" << endl;
    sleep(2);
    cout << "print thread_1 ret: " << (long long)ret << endl;
    
    return 0;
}
线程退出
线程退出

pthread_cancel()

pthread_cancel() 接口可以向指定的线程发送取消请求.
此接口可以**同一进程内, 任意线程调用并向任意线程发送**. 即, 可以在主线程中调用向新线程发送请求. 也可以自己想自己发送请求
并且, 如果线程是被取消的, 那么此线程的退出信息就是-1.
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t &tid) {
    cout << threadName << " is runing, " << "tid: " << tid << ", pid: " << getpid() << endl;
}

void* callBack1(void* args) {
    char* threadName = (char*)args;
    int cnt = 5;
    while (true) {
        printTid(threadName, pthread_self());

        sleep(1);
        cnt--;
        if(cnt == 0) {
            pthread_exit((void*)123);
        }
     }
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");
    sleep(2);
	 pthread_cancel(tid1); 						// 主线程向新线程发送取消请求
   	cout << "main thread cancel thread_1" << endl;

    void* ret = nullptr;
    pthread_join(tid1, &ret);
    cout << "main thread join thread_1 , ready to print thread_1 ret" << endl;
    sleep(2);
    cout << "print thread_1 ret: " << (long long)ret << endl;

    return 0;
}
首先是, 主线程向新线程发送取消请求
主线程向新线程发送取消请求
主线程向新线程发送取消请求
新线程被取消 退出, 退出信息为-1. 即为 取消退出.
即 主线程是可以向新线程发送取消信号的. 但是 有可能发生错误.
我们上述代码中, 向新线程发送取消信号的动作 是在创建新线程2s之后执行的.
如果将那2s的暂停取消(主线程中, cancel动作前的 sleep(2)):
主线程向新线程发送取消请求, 出现错误
主线程向新线程发送取消请求, 出现错误
可以发现, 线程、进程都不正常的退出了.

同一进程内的所有线程都可以调用pthread_cancel()像任意线程发送取消信号.

甚至可以向主线程发送取消信号. 线程自己也可以向自己发送取消信号.

可以自己去测试一下.

被取消的线程的退信息

线程被成功的取消, 我们用 pthread_join() 接收线程的退出信息. 结果得到的退出信息是-1
那, 这个-1是从哪里来的呢?
我们知道, Linux中线程是由PCB模拟实现的, PCB中维护的都有自己执行流的退出信息.
我们return也好 或者 调用pthread_exit() 也好, 实际上都会修改PCB中维护的退出信息.
pthread_cancel() 也是如此, 如果取消线程成功了, 操作系统就会修改线程PCB中的退出信息.
将退出信息改为 PTHREAD_CANCELED . 这是一个 pthread 库提供的宏, 其实就是 ((void*)-1) 的宏定义:

线程分离

操作系统中的线程, 在默认情况下是 joinable 的.
即, 线程退出之后 是需要调用 pthread_join 进行接收线程信息和资源回收的, 否则可能会造成内存泄漏问题.
不过, 如果一个线程不需要关心返回值, 如果不是需要回收资源, 其实 join 的必要没有那么大.
那么, 对于不关心返回值的线程, 可否不用 join回收资源, 可否让线程自动回收资源呢
是可以的. 这样的操作叫 线程分离

pthread_detach()

此接口的作用, 其实可以理解为 将线程与主线程分离. 即 主线程不在管这个线程, 主线程也就不关心退出信息, 不关心资源回收.
这个接口一般线程自己调用或主线程调用.
不过, joinable 和 分离 是冲突的. 毕竟 joinable 表示线程需要调用join回收, 分离线程 则表示此线程是自动回收的. 很明显是两个冲突的状态
线程join成功
线程join成功
这是线程被正常join的现象.
如果我们设置分离:
#include <iostream>
#include <cstring>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t& tid) {
    printf("%s is runing, tid: %lu, pid: %d\n", threadName, tid, getpid());
}

void* startRoutine(void* args) {
    pthread_detach(pthread_self());			// 线程分离
    string name = (char*)args;
    int cnt = 1;
    while (cnt--) {
        printTid(name.c_str(), pthread_self());
        sleep(1);
    }
    printf("%s is over\n", name.c_str());

    return nullptr;
}

int main() {
    pthread_t tid1, tid2, tid3, tid4;

    pthread_create(&tid1, nullptr, startRoutine, (void*)"thread_1");
    pthread_create(&tid2, nullptr, startRoutine, (void*)"thread_2");
    pthread_create(&tid3, nullptr, startRoutine, (void*)"thread_3");
    pthread_create(&tid4, nullptr, startRoutine, (void*)"thread_4");

    sleep(2);

    int joinRet = pthread_join(tid1, nullptr);
    cout << strerror(joinRet) << endl;
    joinRet = pthread_join(tid2, nullptr);
    cout << strerror(joinRet) << endl;
    joinRet = pthread_join(tid3, nullptr);
    cout << strerror(joinRet) << endl;
    joinRet = pthread_join(tid4, nullptr);
    cout << strerror(joinRet) << endl;

    return 0;
}
上面的这段代码, 在线程需要执行的回调函数内 调用pthread_detach(pthread_self());将线程自己分离, 然后主线程内依旧使用 pthread_join() 回收. 不过接收返回值, 判断join执行的结果:
分离和joinable状态不可共存
分离和joinable状态不可共存
可以看到, 主线程中的 pthread_join() 并没有成功的将4个线程回收掉. 而是报出了 Invalid argument 无效参数 的错误.
这其实就意味着, 分离过的线程 在运行结束之后就自动被回收了, 无法再用 pthread_join() 回收.

上面举得例子是正确使用的情况.
如果, 我们将主线程中的 sleep(2); 语句删除了, 并且我们只创建、回收一个线程:
#include <iostream>
#include <cstring>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t& tid) {
    printf("%s is runing, tid: %lu, pid: %d\n", threadName, tid, getpid());
}

void* startRoutine(void* args) {
    pthread_detach(pthread_self());
    string name = (char*)args;
    int cnt = 1;
    while (cnt--) {
        printTid(name.c_str(), pthread_self());
        sleep(1);
    }
    printf("%s is over\n", name.c_str());

    return nullptr;
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, startRoutine, (void*)"thread_1");

    int joinRet = pthread_join(tid1, nullptr);
    cout << strerror(joinRet) << endl;

    return 0;
}
我们依旧在 回调函数内将线程分离, 那么这段代码的执行结果是:
分离了, 但是join执行成功
分离了, 但是join执行成功
惊奇的发现, 线程退出之后成功得被 join 回收了.
这是什么原因?我们不是在线程内分离线程了吗?
其实是因为, 在主线程内 我们创建了线程之后 没有使主线程暂停一会, 直接就继续执行了 pthread_join(). 线程还没来得及 将自己分离.
所以线程退出时, 又会被主线程中的 pthread_join() 回收成功
要避免这种情况, 可以在创建线程之后将主线程等一会, 让新线程执行完分离再让主线程继续执行.
或者, 可以直接在主线程内将线程分离.
#include <iostream>
#include <cstring>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t& tid) {
    printf("%s is runing, tid: %lu, pid: %d\n", threadName, tid, getpid());
}

void* startRoutine(void* args) {
    string name = (char*)args;
    int cnt = 1;
    while (cnt--) {
        printTid(name.c_str(), pthread_self());
        sleep(1);
    }
    printf("%s is over\n", name.c_str());

    return nullptr;
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, startRoutine, (void*)"thread_1");
    pthread_detach(tid1);
    cout << "main thread detach thread_1" << endl;

    int joinRet = pthread_join(tid1, nullptr);
    cout << strerror(joinRet) << endl;
	
    sleep(5); 		// 防止主线程先退出
    
    return 0;
}
在主线程中分离线程
在主线程中分离线程
这样就可以解决 join 时, 新线程还未分离的问题.

线程分离之后, 主线程就不再管分离的线程了. 即使主线程先退出了, 也不会管分离的线程. 即主线程先退出, 被分离的线程可能还会运行.
所以, 存在线程被分离时, 我们的一般会将主线程不退出, 常驻内存
线程分离, 就像是给线程设置了一下让线程如何退出. 等线程执行完毕之后, 自动退出回收.
所以, 其实**线程分离也可以看作是线程的第四种退出方式, 延迟退出**

第一种是, 回调函数返回

第二种是, pthread_exit()

第三种是, pthread_cancel()


到这里, 线程的概念和控制基本上介绍完了.
但是还有一个问题, 就是 如何理解线程id. 不过会是下一篇文章的内容.
感谢阅读~
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)

作者: 哈米d1ch 发表日期:2023 年 4 月 14 日