【Linux系统篇】:Linux线程控制基础---线程的创建,等待与终止

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh--CSDN博客
✨ 文章所属专栏:Linux篇--CSDN博客

文章目录

POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以pthread_开头的;
  • 要使用这些线程的相关函数,需通过引入头文件<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的-lpthread选项。

一.线程创建

线程创建函数

cpp 复制代码
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

功能:创建一个新的线程

参数

  • thread:返回线程的ID;
  • attr:设置线程的属性,为空时(nullptr)表示使用默认属性;
  • start_routine:函数地址,线程启动后要执行的函数
  • arg:传入线程启动函数的参数

返回值:成功返回0;失败返回错误码

测试代码:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

// 新线程的执行函数
void *pthreadRoution(void *args){
    while(true){
        cout << "new thread, pid: " << getpid() << endl;
        sleep(1);
    }
}

int main(){
    pthread_t tid;
    // 主线程创建一个新线程
    pthread_create(&tid, nullptr, pthreadRoution, nullptr);

    // 主线程
    while(true){
        cout << "main thread, pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

结合创建的线程重新认识一下线程的相关概念:

1.任何一个线程被干掉,其余线程包括整个进程都会被干掉,所以这就是为什么线程的健壮性很差

2.在多线程情况下,一个方法可以被多个执行流同时执行,这种情况就是show函数被重入了

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;

// show函数方法
void show(const string &name){
    cout << name << "say# " << "hello thread" << endl;
}

// 新线程的执行函数
void *pthreadRoution(void *args){
    while(true){
        //cout << "new thread, pid: " << getpid() << endl;
        show("[new thread]");
        sleep(2);
    }
}

int main(){
    pthread_t tid;
    // 主线程创建一个新线程
    pthread_create(&tid, nullptr, pthreadRoution, nullptr);

    // 主线程
    while(true){
        //cout << "main thread, pid: " << getpid() << endl;
        show("[main thread]");
        sleep(2);
    }

    return 0;
}

3.未初始化和已初始化的全局变量在所有线程中是共享的

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;

int g_val = 100;
// 新线程的执行函数
void *pthreadRoution(void *args){
    while(true){
        printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
        sleep(2);
    }
}

int main(){
    pthread_t tid;
    // 主线程创建一个新线程
    pthread_create(&tid, nullptr, pthreadRoution, nullptr);

    // 主线程
    while(true){
        printf("main thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
        sleep(2);
        g_val++;
    }

    return 0;
}

根据上面的例子可以发现,线程之前想要通信会变得非常简单,因为线程之间天然的就具有共享资源

二.线程等待

一般而言,主线程一定会是最后退出的,因为其他线程是由主线程创建的,主线程就要对创建出来的新线程做管理,和父进程等待回收子进程一样,主线程也要等待其他线程进行回收,否则就会造成类似于僵尸进程的问题,比如内存泄漏;同理,主线程创建新线程肯定是要执行一些任务,最后新线程的执行情况也是要返回给主线程的。

所以线程等待和进程等待同理,两个目的:

1.防止新线程造成内存泄露(主要目的)

2.如果需要,主线程也可以获取新线程的执行结果

线程等待函数

cpp 复制代码
#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

参数

  • thread:线程ID
  • value_prt:二级指针,指向一个指针的地址,输出型参数,用来获取线程的返回值;如果不关心返回值,可以直接设置为空指针

返回值:成功返回0;失败返回错误码。

调用该函数的线程将挂起等待,直到ID为thread的线程终止;一般都是主线程调用,所以主线程等待时默认是阻塞式等待

测试代码:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;

int g_val = 100;
// 新线程的执行函数
void *pthreadRoution(void *args){
    int cnt = 5;
    while (true)
    {
        printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
        sleep(2);
        cnt--;
        if (cnt == 0){
            break;
        }
    }

    return (void *)100;
}

int main(){
    pthread_t tid;
    // 主线程创建一个新线程
    pthread_create(&tid, nullptr, pthreadRoution, nullptr);

    void *retval;
    pthread_join(tid, &retval);

    cout << "main thread quit ..., ret: " << (long long int)retval << endl;

    return 0;
}

为什么线程等待时不用考虑异常呢?

因为根本做不到,一旦其中一个线程出现异常,整个进程也就直接终止退出了。异常问题是由进程考虑的,线程只需要考虑正常情况即可。

三.线程终止

线程终止时直接使用return语句返回是其中一种方法,除了这个还用其他方法,

先测试使用exit终止线程:

cpp 复制代码
//线程等待的测试代码中使用exit终止退出
void *pthreadRoution(void *args){
    int cnt = 5;
    while (true)
    {
        printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
        sleep(1);
        cnt--;
        if (cnt == 0){
            break;
        }
    }
    exit(11);
    //return (void *)100;
}

最后的结果现象就是,新线程终止退出后,主线程并没有回收新线程,这是因为调用exit函数使整个进程都终止退出了。

任何一个线程调用exit,都表示整个进程终止;exit是用来终止进程的,不能用来终止线程。

线程终止可以使用线程库中的pthread_exit函数

线程终止函数

cpp 复制代码
void pthread_exit(void *value_ptr);

参数value_ptr:指向线程终止时返回值的地址;注意,要返回的指针不能指向一个局部变量

返回值:无返回值,线程结束的时候无法返回到他的调用者。

测试代码:

cpp 复制代码
void *pthreadRoution(void *args){
    int cnt = 5;
    while (true)
    {
        printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
        sleep(1);
        cnt--;
        if (cnt == 0){
            break;
        }
    }
    pthread_exit((void *)100);
    // exit(11);
    // return (void *)100;
}

如果主线程先退出,创建出的新线程后退出,最后的现象就是一旦主线程,其余的线程都会退出,也就是整个进程退出

cpp 复制代码
int main(){
    pthread_t tid;
    // 主线程创建一个新线程
    pthread_create(&tid, nullptr, pthreadRoution, nullptr);
    
    // 主线程一秒后退出
    sleep(1);
    return 0;

    void *retval;
    pthread_join(tid, &retval);

    cout << "main thread quit ..., ret: " << (long long int)retval << endl;

    return 0;
}

因为主线程是在main函数中,在main函数return 相当于调用exit函数终止整个进程;

需要注意的是,使用pthread_exit或者return这两种方式来终止线程时,返回的指针所指向的内存单元必须是全局的或者是在堆区上分配的,不能在线程当前执行的函数的栈上分配,因为一旦线程结束函数调用时,栈上分配的空间就会自动释放

一个线程终止退出,除了上面的的returnpthread_exit函数两种方式以外,还有一种退出方式:线程取消,调用pthread_cancel函数

cpp 复制代码
int pthread_cancel(pthread_t thread);

参数thread:线程ID

返回值:成功返回0;失败返回错误码

线程取消是由主线程调用pthread_cancel函数像目标线程发送一个终止请求,测试代码:

cpp 复制代码
int main(){
    pthread_t tid;
    // 主线程创建一个新线程
    pthread_create(&tid, nullptr, pthreadRoution, nullptr);
    
    sleep(1);
    pthread_cancel(tid);

    void *retval;
    pthread_join(tid, &retval);

    cout << "main thread quit ..., ret: " << (long long int)retval << endl;

    return 0;
}

线程取消后,退出结果就会设置成一个宏PTHREAD_CANCELED(表示-1),线程等待就会获取到退出结果-1。

总结:如果需要只终止某个线程而不终止整个进程,有三种方式

1.线程执行的函数return;主线程不适用。

2.线程调用pthread_exit终止自己。

3.一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

四.扩展内容

1.重谈pthread_create函数

前面提到过pthread_create函数的第四个参数和返回值都是void*类型,采用该类型主要是通过泛型的思想,适配任何指针类型。

其中pthread_create函数的第四个参数和返回值除了传递普通的内置类型(比如整形,字符串型等),还可以传递自定义类型(类和对象)

通过一段代码来测试:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;

class Request{
public:
    Request(int start, int end, const string &threadname)
    :_start(start),_end(end),_threadname(threadname)
    {}
public:
    int _start;
    int _end;
    string _threadname;
};

class Response{
public:
    Response(int result, int exitcode)
    :_result(result),_exitcode(exitcode)
    {}
public:
    int _result;
    int _exitcode;
};

void *sumcount(void *args){
    Request *rq = static_cast<Request *>(args);
    Response *rsp = new Response(0, 0);

    for (int i = rq->_start; i <= rq->_end;i++){
        rsp->_result += i;
    }
    delete rq;
    pthread_exit(static_cast<void *>(rsp));
}

int main(){
    pthread_t tid;
    Request *rq = new Request(1, 100, "thread 1");
    // 主线程创建一个新线程
    pthread_create(&tid, nullptr, sumcount, rq);


    // 主线程回收新线程
    void *retval;
    pthread_join(tid, &retval);
    Response *rsp = static_cast<Response *>(retval);
    cout << "rsp->result: " << rsp->_result << " rsp->exitcode: " << rsp->_exitcode << endl;
    delete rsp;
    return 0;
}

在上面的测试代码中,传递指针时,将自定义类型(rq)的指针强制转换为void*;然后在线程函数中,将void*强制转换为原始类型。

自定义类型的对象都是通过malloc或者new在堆上开辟空间的。

而堆空间也是被所有线程共享的,但是共享堆空间不等于自动共享数据

  • 堆空间的共享性:所有线程共享同一进程的堆内存区域,但堆上的数据必须通过指针来定位,需要明确直到其地址才能访问;
  • 数据的隔离性:虽然所有线程共享堆空间,但是线程之间默认不知道对方在堆中创建了哪些数据对象。

所以给线程的执行函数传参时,也可以传入自定义类型的对象的指针。

2.C++11线程库

这里先简单的了解即可,之后学C++11时会再重点讲解更详细的使用

C++11的线程库简单使用:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <string>
#include <thread>
using namespace std;

void threadrun(){
    while(true){
        cout << "I am a new thread for C++" << endl;
        sleep(1);
    }
}

int main(){
    // C++11的线程库
    thread t1(threadrun);

    t1.join();

    return 0;
}

C++11里的线程库本质上还是封装的原生线程库,所以使用C++11的线程库编译时还是需要带上-lpthread选项,此外还要带上-std=c++11选项

3.线程栈结构

每个线程在运行时都要有自己独立的栈结构,因为每一个线程都会有自己的调用链,也就注定了每一个线程都要有一个调用链对应的独立栈帧结构,这个栈结构会保存任意一个执行流在运行过程中所有的临时变量,比如压栈时传参形成的临时变量;返回时的返回值以及地址,包括线程自己在函数中定义的临时变量,所以每个线程都要有自己独立的栈结构。

其中主线程直接使用地址空间中提供的的栈结构,这就是系统真正意义上的进程

除了主线程外,其他线程的独立栈结构,都在共享区,具体来说是在pthread库中,每一个线程都有一个线程控制块tcb(由线程库维护),线程的栈结构就存储在tcb中,而tcb的起始地址就是线程的tid

上面讲解线程创建时函数的第一个参数线程ID,就是这个线程的tid地址。后续线程的所有操作都是根据这个线程ID来实现的。

线程库中还提供了pthread_self函数,可以获取线程自身的ID:

cpp 复制代码
pthread_t pthread_self(void);

多线程测试栈区独立:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>
using namespace std;

#define NUM 3

class threadData{
public:
    threadData(int number)
    :threadname("thread-"+to_string(number))
    {}
public:
    string threadname;
};

string toHex(pthread_t tid){
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);
    return buffer;
}

// 所有线程都会调用这个函数
void *threadRoutine(void *args){
    threadData *td = static_cast<threadData *>(args);
    int i = 0;
    // 每个线程都创建一个test_i变量
    int test_i = 0;    
    while (i < 5)
    {
        cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self())
             << ", threadname: " << td->threadname
             << ", test_i: " << test_i << ", &test_i: " << toHex((pthread_t)&test_i) << endl;
        sleep(1);
        i++;
        test_i++;
    }

    delete td;
    return nullptr;
}

int main(){
    // 创建多线程
    vector<pthread_t> tids;
    for (int i = 0; i < NUM; i++){
        pthread_t tid;
        threadData *td = new threadData(i);  // 在堆区创建

        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
        sleep(1);
    }

    for (int i = 0; i < tids.size(); i++){
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

根据上面的测试就可以证明,每个线程都有自己的独立栈结构

如果某个线程想访问另一个线程栈区上的变量,也是可以实现的:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>
using namespace std;

#define NUM 3
 
int *p = nullptr;

class threadData{
public:
    threadData(int number)
    :threadname("thread-"+to_string(number))
    {}
public:
    string threadname;
};

string toHex(pthread_t tid){
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);
    return buffer;
}

// 所有线程都会调用这个函数
void *threadRoutine(void *args){
    threadData *td = static_cast<threadData *>(args);
    int i = 0;
    int test_i = 0;    // 该变量在每个线程的栈区创建

    if(td->threadname=="thread-2"){
        p = &test_i;
    }

    while (i < 5)
    {
        cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self())
             << ", threadname: " << td->threadname
             << ", test_i: " << test_i << ", &test_i: " << &test_i << endl;
        sleep(1);
        i++;
        test_i++;
    }

    delete td;
    return nullptr;
}

int main(){
    // 创建多线程
    vector<pthread_t> tids;
    for (int i = 0; i < NUM; i++){
        pthread_t tid;
        threadData *td = new threadData(i);  // 在堆区创建

        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
        sleep(1);
    }

    sleep(1);
    cout << "main thread get a local value,val: " << *p << ", &val: " << p << endl;
    
    for (int i = 0; i < tids.size(); i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

所有线程本来就共享代码区,全局变量区,堆区,共享区等,而对于线程独立的栈结构上的数据,也是可以被其他线程看到并访问的,所以线程和线程之间,几乎没有秘密。

4.线程局部存储

全局变量可以被所有线程看到并访问的

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>
using namespace std;

#define NUM 3

//int *p = nullptr;
int g_val = 0;

class threadData{
public:
    threadData(int number)
    :threadname("thread-"+to_string(number))
    {}
public:
    string threadname;
};

string toHex(pthread_t tid){
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);
    return buffer;
}

// 所有线程都会调用这个函数
void *threadRoutine(void *args){
    threadData *td = static_cast<threadData *>(args);
    int i = 0;
    //int test_i = 0;    // 该变量在每个线程的栈区创建

    // if(td->threadname=="thread-2"){
    //     p = &test_i;
    // }

    while (i < 5)
    {
        cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self())
             << ", threadname: " << td->threadname
             << ", g_val: " << g_val << ", &g_val: " << &g_val << endl;
        sleep(1);
        i++;
        //test_i++;
        g_val++;
    }

    delete td;
    return nullptr;
}

int main(){
    // 创建多线程
    vector<pthread_t> tids;
    for (int i = 0; i < NUM; i++){
        pthread_t tid;
        threadData *td = new threadData(i);  // 在堆区创建

        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
        sleep(1);
    }

    sleep(1);
    //cout << "main thread get a local value,val: " << *p << ", &val: " << p << endl;
    
    for (int i = 0; i < tids.size(); i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

在上面的测试代码中,g_val是一个全局变量,被所有线程共享访问,所以这个g_val其实就是一个共享资源。

如果线程想要一个私有的全局变量,如何实现?

直接在定义的全局变量之前加__thread即可:

cpp 复制代码
__thread int g_val=0;

每一个线程都访问的是同一个全局变量,但是每一个全局变量对于每一个线程来讲,都是各自私有一份,这种技术就是线程的局部存储

__thread不是C语言或C++的关键字,而是编译器提供的一个编译选项,编译的时候会默认将这个g_val变量给每一个线程在独立的栈结构上申请一份空间。

注意点:

__thread定义线程的局部存储变量时只能用来定义内置类型,不能用来修饰自定义类型:

5.分离线程

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏问题;
  • 但是如果线程等待时,并不关心线程的返回值,此时join就是一种负担,这个时候,我们可以使用pthread_detach函数分离线程,告诉系统当线程退出时,自动释放线程资源。
cpp 复制代码
int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离(比如主线程使某个线程分离),也可以是线程自己分离(在线程的执行函数中调用pthread_detach(pthread_self()))。

注意:joinable和分离是冲突的,一个线程不能既是joinable又是分离的

pthread_joinpthread_detach两个函数不能对同一个线程使用。

以上就是关于线程控制部分的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!

相关推荐
ALex_zry25 分钟前
告别手动输入密码:基于SSHPass的自动化文件传输实践告别手动输入密码:基于SSHPass的自动化文件传输实践
运维·自动化
@CLoudbays_Martin1127 分钟前
CF后台如何设置TCP 和 UDP 端口?
大数据·运维·服务器·网络·数据库
chennalC#c.h.JA Ptho39 分钟前
Centos系统详解架构详解
linux·经验分享·笔记·系统架构·系统安全
独行soc1 小时前
2025年渗透测试面试题总结-某步在线面试(题目+回答)
linux·网络·安全·web安全·面试·职场和发展·渗透安全
mixboot1 小时前
ping_test_parallel.sh 并行网络扫描脚本
linux·ping·ip在线扫描
搬码临时工2 小时前
如何设置内网映射端口到外网访问?哪些软件可以进行端口映射?
服务器·网络·智能路由器·访问公司内网
蓑笠翁0012 小时前
Python异步编程入门:从同步到异步的思维转变
linux·前端·python
爱奥尼欧2 小时前
【Linux】Linux工具(1)
linux·运维·服务器
文牧之2 小时前
PostgreSQL 的 pg_current_logfile 函数
运维·数据库·postgresql
Stark-C2 小时前
7400MB/s&5050TBW完美结合,全新希捷酷玩530R SSD体验评测
服务器·网络·数据库