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