✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉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
:线程IDvalue_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
这两种方式来终止线程时,返回的指针所指向的内存单元必须是全局的或者是在堆区上分配的,不能在线程当前执行的函数的栈上分配,因为一旦线程结束函数调用时,栈上分配的空间就会自动释放。
一个线程终止退出,除了上面的的return
和pthread_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_join
和pthread_detach
两个函数不能对同一个线程使用。
以上就是关于线程控制部分的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!