一. 线程的概念
1.什么是线程
线程是进程内部的一个执行流,是进程调度的基本单位。它具有轻量的特点,它的创建和销毁所消耗的资源更少,线程间切换比进程间切换消耗的资源更少;它与进程共享一张虚拟地址空间表,通过进程来给线程执行流分配资源;同时,每个线程都是独立执行的,拥有自己的程序计数器和上下文切换。
简单来说在Linux下,一个进程由多个 task_struct ,一张虚拟地址空间表和页表构成。而线程就是一个 task_struct ,进程内部的一个执行流,所有的线程都指向同一张虚拟地址空间表,让进程共同管理。这样我们就可以对所有线程的资源进行划分,划分为堆区栈区共享区等等。通过一张页表映射到物理内存当中。
总的来说,线程就是一个 task_struct ,通过共同指向同一张虚拟地址空间的方式实现了共同管理,降低了创建调度销毁成本。
2.深刻理解虚拟地址空间
在虚拟地址空间中,页表用于映射虚拟地址空间到实际的物理内存。我们在管理虚拟地址空间的时候,它的地址是连续的,而物理地址空间则是可以分散碎片的。在虚拟地址空间中,我们存储同一个资源的时候地址空间需要连续,但在物理地址当中,我们会将同类型的资源尽可能放到一处(这样可以节省空间),无论是哪个线程都可以将数据进行整合。
有了虚拟地址空间和物理地址空间,那么我们如何将他们连接起来呢?再加一层页表就好。
在 Linux 当中,页表是由,三级页表组成。一级页表是页表目录,其中存储着各个页表的地址;二级目录是各个页表,页表指向各个页帧的地址(4 KB);三级页表就是页帧,每个页帧由 4 KB 构成。在虚拟地址空间中,每个数据在虚拟地址空间下都有一个 32 字节的地址,这 32 个字节需要分为 10 + 10 + 12 来进行阅读,首先定位到页表目录当中,前10个字节,用于在页表目录当中找到对应的页表;中间的10个字节用于在当前页表当中找到对应的页帧;最后的12个字节用于对页帧的起始位置的偏移量,这样我们就能通过虚拟地址找到相对于的物理地址
下面是一个简化图
有了页帧,该如何管理呢? 先描述再组织,操作系统引入了 struct page 结构。对于每个页帧,都有一个 struct page 对它进行相应的管理。
下面我来介绍一下 struct page 的结构构成。
该结构主要用于管理记录,跟踪页帧的使用状态,页针的归属,管理页帧的映射关系,回收页帧等等。
首先是状态标识(flags),用于记录页帧的基本状态,是被锁定被修改还是内核保留;引用计数(_refcount)记录该页帧被引用的次数;映射关系(mapping + index)mapping 指向该文件的存储页,index 用于指向在该页下的偏移量。
总结:
1. 虚拟地址和物理地址管理,通过页表进行映射,使得其完成了解耦的操作。
2. 页表按需创建和分页机制有效的节省了空间消耗。
3.线程的优缺点
(1)优点
线程的创建相比于进程的创建代价要小很多且占用资源少,线程只需要创建 task_struct 挂接到虚拟地址空间上即可,而进程的创建就要涉及虚拟地址空间页表等等资源;线程切换比进程切换效率高,如果要进行进程间的切换,就需要连同虚拟地址空间等进行统一切换,而线程只需要切换 task_struct 和上下文资源即可;线程可以利用多处理器进行并发运行,提高 IO 效率和计算效率;
(2)缺点
线程共享进程的地址空间,因此可以访问到当前的共享资源,这就导致,若缺乏同步机制,线程会引发数据竞争,导致程序异常;当线程过多时,容易导致资源限制,首先每个线程都有自己的独立线程栈在内存当中,大小为 8 MB ,若线程过多就容易导致内存空间耗尽。其次,若线程过多,CPU的调度开销也对应的增加,CPU 将时间花在了不断调度线程中,导致 CPU 利用率下降。最后,每个线程创建都会在内核当中创建一个 TCB 资源,这也就导致了高频创建销毁会给内核增加负担。进程拥有较高的独立性,即使程序出错进程崩溃,这也不会影响其他的进程运行,但如果线程崩溃,可能会导致整个进程都退出。
二. 线程的控制
1.线程创建
pthread_create:
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void*),void *arg);
参数说明:
thread:获取创建成功的线程 ID ,该参数是一个输出型参数。
attr:用于设置进程属性,传入NULL 表示使用默认值。
start_routine:返回值和参数均为 void* 的函数指针。该参数表示线程例程,即后续线程需要执行的函数。
arg:传给线程实例的参数。
返回值:
成功返回0,失败返回错误码。
下面我们来看一个示例,让一个主线程创建一个新线程
当一个程序启动时,就有一个进程被操作系统创建,于此同时一个线程也立刻运行,这个线程就是主线程。
#include <iostream> #include <pthread.h> #include <unistd.h> using namespace std; void *startRoutine(void* args) { while(true) { cout<<"线程正在运行"<<endl; sleep(1); } } int main() { pthread_t tid; int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread-1"); cout<<"new thread id:"<<tid<<endl; while(true) { cout<<"main pthread 正在运行"<<endl; sleep(1); } return 0; }
运行结果:
当我们想获取线程 id 时,可以使用 pthread_self 函数,我们来看下面的代码
#include <iostream> #include <pthread.h> #include <unistd.h> using namespace std; static void Print(const char* name,pthread_t tid) { cout<<name<<" 正在运行"<<tid<<endl; } void *routine(void* argv) { const char* name = static_cast<const char*>(argv); while(true) { Print(name,pthread_self()); sleep(1); } } int main() { pthread_t tid; int n = pthread_create(&tid,nullptr,routine,(void*)"thread"); while(true) { cout<<"main thread run"<<endl; sleep(1); } return 0; }
下面是运行结果:
在线程运行中,调用了 pthread_self 函数将当前线程的tid传给了函数进行调用。
2.线程终止
终止一个线程有三种方法:
1.从线程函数 return
2.在线程中调用
3.在线程中调用 pthread_exit 终止其它进程中的另一个线程
方法一:(从线程return)
方法较简单不详细讲解
方法二:(pthread_exit)
pthread_exit 的功能就是终止线程
#include <pthread.h> void pthread_exit(void* retval);
参数说明:
retval:线程退出码
注意:
pthread_exit 和 return 返回的指针所指向的内存单元必须是全局的或者是 malloc 分配的,若是在线程内部创建的指针返回会导致访问结果不可控。因为随着 pthread_exit 线程栈上存储的数据也会被销毁。
下面看一下正确使用:
#include <iostream> #include <pthread.h> #include <unistd.h> using namespace std; static void printTid(const char* name,const pthread_t &tid) { cout<<name<<" 正在运行 "<<tid<<" "<<endl; } void* Routine(void* argv) { const char* name = static_cast<const char*>(argv); int cnt = 5; while(true) { printTid(name,pthread_self()); if(!(cnt--)) { break; } sleep(1); } cout<<"线程退出"<<endl; pthread_exit((void*)11111); } int main() { pthread_t tid; int n = pthread_create(&tid,nullptr,Routine,(void*)"thread"); void* ret = nullptr; pthread_join(tid,&ret); cout<<"main pthread success "<<(long long)ret<<endl; sleep(5); while(true) { printTid("main othread",pthread_self()); sleep(2); } return 0; }
运行结果:
3.线程等待
pthread_join:
类比于进程等待,线程创建也是需要被等待的,如果一个新线程被创建出来,主线程不进行等待,那么这个新线程的资源就无法被回收,就会导致资源泄露。在线程中等待的函数叫 pthread_join
#include <pthread.h> int pthread_join(pthread_t thread,void **retval);
参数说明:
thread:被等待的线程tid
retval:线程退出时的信息码
返回值:
成功返回0,失败返回信息码
下面是代码样例:
#include <iostream> #include <pthread.h> #include <unistd.h> using namespace std; void *thread_func(void *arg) { printf("子线程任务完成\n"); pthread_exit((void*)100); sleep(200); } int main() { pthread_t tid; void *ret; pthread_create(&tid, NULL, thread_func, NULL); pthread_join(tid, &ret); printf("子线程退出状态:%ld\n", (long)ret); return 0; }
运行结果:
主线程进行阻塞,等待子线程完成任务后,主线程才会继续运行
4.线程分离
pthread_detach:
线程分离与线程等待是一对互斥关系,当我们主线程不需要关心子线程的返回值时,我们可以将子线程进行分离(也可以是子线程自行分离),分离后的线程会继续执行自己的内容。一个线程被分离了,这个进程依旧需要管理这个线程的资源,若被分离的线程出现故障也有可能会影响其他的线程或者当前进程。分离的线程可以减轻 join 的负担,意味着主线程不需要再关注子线程了,而子线程执行完毕后也会自行释放资源。
#include <pthread.h> int pthread_detach(pthread_t thread);
参数说明:
thread:被分离的线程 ID
返回值:
成功返回0,失败返回错误码
5.POSIX线程库
在Linux当中,站在内核角度实际上并没有关于线程相关的接口,但是用户希望创建线程时可以调用接口,这样可以使编码更加便捷。于是,便有了第三方的线程库,基于这个第三方库,它为用户提供了线程相关的接口,构成了线程有关的完整系列。
这些接口大多数都是以 pthread_ 打头,在使用前需要包含头文件 <pthread.h> ,链接库时需要包含 -lpthread 选项。
6.线程栈和 pthread_t
线程是一个独立的执行流,在运行的过程中也会产生自己的数据,所以线程拥有自己的独立的栈,线程栈会随着线程的销毁被回收。
在 Linux 中,基于线程的接口都是通过外部库封装后进行调用的,pthread_t 是线程的身份证,用于识别和操作线程。在外部库中,pthread_t 是由 thread_info 结构体进行管理的。
struct thread_info { pthread_t tid; void *stack; }
与其一同管理的便是线程栈。每当用户创建一个线程时,就会在动态库中创建一个线程控制块 thread_info ,给用户返回一个 pthread_t ,也就是该结构体的起始虚拟地址。
主线程中的栈区使用的是地址空间中的栈区,而创建的子线程用的是库中提供的栈结构。
7.线程的局部存储
在线程中,全局变量是共享的,所有的线程可以共用一份全局变量,如果想让全局变量私有那么可以进行线程变量的局部存储
下面我们来验证一下,线程可以共用同一份全局变量
#include <iostream> #include <pthread.h> #include <unistd.h> using namespace std; int global_val = 100; void *Routine(void *argv) { const char* name = static_cast<const char*>(argv); while(true) { cout<<"thread: "<<name<<" global_value"<<global_val<<" new: "<<global_val++<<"address: "<<&global_val<<endl; sleep(1); } } int main() { pthread_t tid1; pthread_t tid2; pthread_t tid3; pthread_create(&tid1,nullptr,Routine,(void*)"thread1"); pthread_create(&tid2,nullptr,Routine,(void*)"thread2"); pthread_create(&tid3,nullptr,Routine,(void*)"thread3"); pthread_join(tid1,nullptr); pthread_join(tid2,nullptr); pthread_join(tid3,nullptr); return 0; }
运行结果:
我们发现,全局变量在主线程和子线程下该变量的地址都是一致的,它们所用的是同一个变量
若我们希望在每一个子线程下都创建一份变量我们可以这样操作
样例代码:
#include <iostream> #include <pthread.h> #include <unistd.h> using namespace std; __thread int global_val = 100; void *Routine(void *argv) { const char* name = static_cast<const char*>(argv); while(true) { cout<<"thread: "<<name<<" global_value"<<global_val<<" new: "<<global_val++<<"address: "<<&global_val<<endl; sleep(1); } } int main() { pthread_t tid1; pthread_t tid2; pthread_t tid3; pthread_create(&tid1,nullptr,Routine,(void*)"thread1"); pthread_create(&tid2,nullptr,Routine,(void*)"thread2"); pthread_create(&tid3,nullptr,Routine,(void*)"thread3"); pthread_join(tid1,nullptr); pthread_join(tid2,nullptr); pthread_join(tid3,nullptr); return 0; }
运行结果:
我们只要在全局变量前加上 __thread ,此时所有的线程都在自己的栈上拿到了一份数据,我们可以观察到,此时全局变量打印出的地址是不同的,且变量是肚子增加的。
三. 线程的封装
线程封装
我们简单的对线程进行封装,使其能进行创建分离等待终止等功能
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <functional>
#include <pthread.h>
using namespace std;
static uint32_t number = 1;
template <typename T>
class Thread
{
using func_t = function<void(T)>;
private:
void EnableDetach()
{
std::cout << "线程被分离了" << std::endl;
_isdetach = true;
}
void EnableRunning()
{
_isrunning = true;
}
static void *Routine(void *argv)
{
Thread<T> *self = static_cast<Thread<T> *>(argv);
self->EnableRunning();
if (self->_isdetach)
self->Detach();
self->_func(self->_data); // 回调处理
return nullptr;
}
public:
Thread(func_t func, T Data)
: _tid(0), _isrunning(false), _isdetach(false), _Data(Data), _func(func)
{
_name = "Thread - " + to_string(_number++);
}
void Detach()
{
if (_isdetach)
return;
int n = pthread_detach(_tid);
if (n != 0)
{
cerr << "fail to detach" << strerror(n) << endl;
}
else
{
cout << "success to detach" << endl;
_isdetach = true;
}
}
void Join()
{
if (_isdetach)
{
cout << "线程已经分离,无法进行等待" << endl;
return;
}
int n = pthread_join(_tid, &res);
if (n != 0)
{
cerr << "fail to join" << strerror(n) << endl;
}
else
{
cout << "success to join" << endl;
}
}
bool Start()
{
if (_isrunning)
return false;
int n = pthread_create(&tid, nullptr, Routine, this);
if (n != 0)
{
cerr << "fail to create pthread" << strerror(n) << endl;
return false;
}
else
{
cout << "success to create pthread" << strerror(n) << endl;
return true;
}
}
bool Stop()
{
if (!_isrunning)
return false;
int n = pthread_cancel(tid);
if (n != 0)
{
cerr << "fail to Stop" << strerror(n) << endl;
return false;
}
else
{
cout << "success to Stop" << endl;
_isrunning = false;
return true;
}
}
~Thread()
{
}
private:
string _name;
pthread_t _tid;
bool _isrunning;
bool _isdetach;
T _Data;
void *res;
func_t _func;
};
感谢各位观看,望多多支持!!!