当前位置: 首页 > news >正文

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

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

在这里插入图片描述

文章目录

  • 一.线程创建
  • 二.线程等待
  • 三.线程终止
  • 四.扩展内容
    • 1.重谈`pthread_create`函数
    • 2.C++11线程库
    • 3.线程栈结构
    • 4.线程局部存储
    • 5.分离线程

POSIX线程库

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

一.线程创建

线程创建函数

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;失败返回错误码

测试代码:

#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函数被重入了

#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.未初始化和已初始化的全局变量在所有线程中是共享的

#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.如果需要,主线程也可以获取新线程的执行结果

线程等待函数

#include <pthread.h>int pthread_join(pthread_t thread, void **retval);

参数

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

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

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

测试代码:

#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终止线程:

//线程等待的测试代码中使用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函数

线程终止函数

void pthread_exit(void *value_ptr);

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

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

测试代码:

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;
}

在这里插入图片描述

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

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函数

int pthread_cancel(pthread_t thread);

参数thread:线程ID

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

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

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函数的第四个参数和返回值除了传递普通的内置类型(比如整形,字符串型等),还可以传递自定义类型(类和对象)

通过一段代码来测试:

#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的线程库简单使用:

#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:

pthread_t pthread_self(void);

在这里插入图片描述

多线程测试栈区独立:

#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>
using namespace std;#define NUM 3class 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;
}

在这里插入图片描述

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

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

#include <iostream>
#include <unistd.h>
#include <vector>
#include <string>
#include <pthread.h>
using namespace std;#define NUM 3int *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.线程局部存储

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

#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即可:

__thread int g_val=0;

在这里插入图片描述

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

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

注意点:

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

在这里插入图片描述

5.分离线程

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

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

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

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

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

相关文章:

  • UDP 通信详解:`sendto` 和 `recvfrom` 的使用
  • 【重走C++学习之路】27、C++IO流
  • 市面上所有大模型apikey获取指南(持续更新中)
  • 【Mytais系列】Datasource模块:数据源连接
  • 动态规划之路劲问题3
  • GitHub Actions 和 GitLab CI/CD 流水线设计
  • 基于 SAFM 超分辨率上采样模块的 YOLOv12 改进方法—模糊场景目标检测精度提升研究
  • Qt开发:按钮类的介绍和使用
  • java_Lambda表达式
  • 关于算法设计与分析——拆分表交换问题
  • 学习黑客风险Risk
  • MCP 探索:browser tools MCP + Cursor 可以实现哪些能力
  • 计算机总线系统入门:理解数据传输的核心
  • 【Mytais系列】缓存机制:一级缓存、二级缓存
  • Servlet (一)
  • 18、状态库:中央魔法仓库——React 19 Zustand集成
  • 二叉树 - JS - 2
  • CGI 协议是否会具体到通讯报文?
  • 计组复习笔记 3
  • Linux 网络与操作系统核心知识体系概览(大框架)
  • AI世界的年轻人|研究不止于实验室,更服务于遥远山区
  • 英伟达:美国无法操纵监管机构在AI领域取胜,美企应专注创新而不是编造荒诞谣言
  • 9米长林肯车开进“皖南川藏线”致拥堵数小时,车主回应称将配合调查
  • 贵州赤水丹霞大瀑布附近山体塌方车辆被埋,景区:无伤亡,道路已恢复
  • 山东省委组织部办公室主任吴宪利已任德州市委常委、组织部部长
  • 万达电影去年净利润亏损约9.4亿元,计划未来三年内新增25块IMAX银幕