Linux《线程(下)》
通过之前进程(上)的学习我们已经了解了线程基本的概念以及在Linux当中线程是如何实现的,还了解了pthread库当中线程相关的接口,那么接下来在本篇当中将继续进行线程相关的学习,在此会继续学习线程相关的接口,并且还要了解线程在内存当中具体的空间布局是什么样的,最后将试着使用pthread库当中提供的线程的接口实现一个类似C++11线程当中的线程库。接下来就开始本篇的学习吧!!!
1.线程接口补充
在上一篇章当中我们已经学习了pthread_create线程超级、pthread_join线程等待的相关的接口,那么接下来还需要学习以下的接口。
1.1 线程终止
在一个线程当中要退出可以有以下的三种方式可以实现:
1.在线程执行所在的函数但这个调用return,但是该方法在main函数所在的主线程当中不适用,因为在主线程当中使用return相当与调用exit。2.在线程执行所在的函数当中调用pthread_exit
3.在主线程当中调用pthread_cancel函数来终止指定的线程
pthread_exit函数如下所示:
#include <pthread.h>void pthread_exit(void *value_ptr);参数:
value_ptr:value_ptr不要指向⼀个局部变量。返回值:
⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调用者(自身)
例如以下代码:
#include<iostream>
#include<pthread.h>
#include<unistd.h>void* routine(void* args)
{int cnt=5;while(cnt--){const char* s=static_cast<const char*>(args);std::cout<<"我是线程:"<<s<<" 正在运行"<<std::endl;sleep(1);}return nullptr;}int main()
{pthread_t t1;pthread_create(&t1,nullptr,routine,(void*)"pthread-1");while(1){std::cout<<"我是主线程,真正运行当中……"<<std::endl;sleep(1);};pthread_join(t1,nullptr);return 0;
}
通过输出的结果机就可以看出我们创建出来的线程在5s之后就退出了
pthread_cancel函数
#include<pthread.h>
功能:取消⼀个执⾏中的线程
int pthread_cancel(pthread_t thread);参数:
thread:线程ID返回值:成功返回0;失败返回错误码
例如以下示例:
#include<iostream>
#include<pthread.h>
#include<unistd.h>void* routine(void* args)
{while(1){const char* s=static_cast<const char*>(args);std::cout<<"我是线程:"<<s<<" 正在运行"<<std::endl;sleep(1);}return nullptr;}int main()
{pthread_t t1;pthread_create(&t1,nullptr,routine,(void*)"pthread-1");int cnt=0;while(1){std::cout<<"我是主线程,真正运行当中……"<<std::endl;sleep(1);++cnt;if(cnt==3)pthread_cancel(t1);};pthread_join(t1,nullptr);return 0;
}
以上的代码当中在主线程当中在3s之后就将创建出来的线程1给终止了,运行程序输出的结果如下所示:
1.2 分离线程
通过之前的学习我们知道在线程创建出来之后都是需要主线程进行线程等待的,否则就会出现内存泄漏的问题,但是实际上如果主线程对其创建出来的线程的退出码不关心,那么这时候子再进行线程的等待就对主线程是一种负担,因此在pthread库当中提供的对应进行线程分离的函数pthreqad_detach,那么调用该函数之后线程在退出之后就会自动释放自身的资源。
#include<pthread.h>int pthread_detach(pthread_t thread);参数:
thread:要分离的线程 ID。返回值:
0 → 成功
非 0 → 错误码(如 ESRCH:线程不存在,EINVAL:线程已经是分离状态)。
例如以下代码:
#include <iostream>
#include <pthread.h>
#include <unistd.h>void *routine(void *args)
{const char *s = static_cast<const char *>(args);std::cout << "我是线程:" << s << " 正在运行" << std::endl;sleep(1);return nullptr;
}int main()
{pthread_t t1;pthread_create(&t1, nullptr, routine, (void *)"pthread-1");pthread_detach(t1);sleep(1);std::cout << "主线程执行结束" << std::endl;sleep(2);return 0;
}
2. 线程ID及进程地址空间布局
以上我们了解了线程以及pthread库当中线程相关的接口之后,接下来就来来了解线程给用户提供的线程ID的本质以及在进程当中线程地址空间的布局具体是什么样的。
通过以上的学习我们了解到了在使用pthread_create的时候首先需要创建一个pthread_t的对象,那么问题就来了在此的pthread_t类型究竟是什么呢?
实际上线程 ID 类型是 pthread_t,
在pthreat_t 并不是内核中提出的,而是在 NPTL(Native POSIX Thread Library,Linux 的 POSIX 线程实现)里,本质上就是一个和 LWP(轻量级进程,Linux 下的内核线程)对应的标识。
实际上pthread_t是结构体struct pthread 的引用(或索引/指针/数值 ID,具体实现依赖 glibc)。
那么这时在用户的视角当中pthread_t就是线程的ID,而就不再需要再去了解不同系统当中线程具体的实现是什么样的,例如在Linux当中是底层是LWP等。
在此struct pthread 的结构体当中的数据如下所示:
struct pthread {/* ---- 基本线程管理 ---- */struct pthread *self; // 指向自己,快速获取 TCBpid_t tid; // 线程在内核中的 TIDpid_t pid; // 所属进程的 PID/* ---- 线程栈相关 ---- */void *stack_base; // 栈的基地址size_t stack_size; // 栈的大小void *stack_guard; // 栈保护区(防止栈溢出)/* ---- 线程状态 ---- */int detach_state; // 线程是 joinable 还是 detachedint canceled; // 是否被取消int exit_state; // 退出状态void *retval; // 线程返回值(用于 pthread_join)/* ---- 同步与等待 ---- */struct pthread *joinid; // 谁在等待我(join 的线程)pthread_cond_t join_cond; // join 等待的条件变量pthread_mutex_t lock; // 用于保护 TCB 的锁/* ---- 线程局部存储 TLS ---- */void **tls; // 线程局部存储指针size_t tls_size; // TLS 的大小/* ---- 信号相关 ---- */sigset_t sigmask; // 线程的信号屏蔽字void *signal_stack; // 信号处理栈/* ---- 调度与优先级 ---- */int sched_policy; // 调度策略(SCHED_FIFO, SCHED_RR, SCHED_OTHER)int sched_priority; // 调度优先级
};
在进程的虚拟地址空间当中就会存在对应的mmap当中存储不同线程对应的结构体,在结构体当中就会存储线程的信息。除此之外每个线程还会有自己独立的线程局部存储,该部分的作用就是在线程退出的时候将退出的信息存储,之后主线程就可以通过该空间得到,并且每个线程还有自己的线程栈;线程在运行的时候就会将局部变量等存储在该区域,实际上就可以理解为线程就是基于该空间运行的。
当多个线程同时运行的时候场景就如下所示,本质上就是和之前我们学习多个进程同时加载同一个动态库类似。
在pthread库当中提供了得到当前线程ID的函数pthread_self
#include <pthread.h>pthread_t pthread_self(void);
3.线程封装
以上我们了解了线程概念以及pthread线程库当中线程的接口之后那么接下来就试着来使用以上的函数来实现出面向对象式的线程类。
实现的代码如下所示:
#pragma once#include <iostream>
#include <functional>
#include <pthread.h>
#include <string.h>
#include <unistd.h>namespace ThreadModlue
{//线程ID,从1开始static int number = 1;//线程类class Thread{//线程运行执行的函数using func_t = std::function<void()>;//将线程分离void EnableDetach(){std::cout << "线程被分离" << std::endl;_isdetach = true;}//将线程的状态置为运行void EnableRunning(){_isrunning = true;}//线程运行函数,线程执行函数时参数只能包含一个args参数,因此需要将Routine函数置为静态成员函数static void *Routine(void *args){Thread *self = static_cast<Thread *>(args);self->EnableRunning();if (self->_isdetach)self->Detach();pthread_setname_np(self->_tid, self->_name.c_str());self->_func();return nullptr;}public://构造函数Thread(func_t func): _tid(0),_isdetach(false),_isrunning(false),res(nullptr),_func(func){_name = "thread-" + std::to_string(number++);}//线程分离void Detach(){if (_isdetach)return;if (_isrunning)pthread_detach(_tid);EnableDetach();}//启动线程bool Start(){if (_isrunning)return false;int n = pthread_create(&_tid, nullptr, Routine, this);if (n != 0){std::cerr << "create thread error!" << strerror(n) << std::endl;return false;}else{std::cout << "ctrate success!" << std::endl;return true;}}//终止线程bool Stop(){if (_isrunning){int n = pthread_cancel(_tid);if (n != 0){std::cerr << "cancel pthread error!" << strerror(n) << std::endl;return false;}else{_isrunning = false;std::cout << "cancel pthread success!" << std::endl;return true;}}return false;}//线程等待bool Join(){if (_isdetach){std::cout << "你的线程已经是分离的了,不能再进行join" << std::endl;return false;}int n = pthread_join(_tid, &res);if (n != 0){std::cerr << "join pthread error!" << strerror(n) << std::endl;return false;}else{std::cout << "join pthread success!" << std::endl;return true;}}~Thread(){}private://线程IDpthread_t _tid;//线程名std::string _name;//线程是否分离标志位bool _isdetach;//线程是否运行标志位bool _isrunning;void *res;//线程运行执行函数func_t _func;};
}
以上就是本篇的所有内容的,在此我们就将线程基本的概念了解完毕,接下来的篇章当中将开始线程控制相关的学习……