1.理解用户级线程
我们前面用到的所有跟线程有关的接口全部都不是系统直接提供的接口,而是原生线程库pthread提供的接口。我们前面谈到了由于用户只认线程,而linux操作系统是通过用轻量级进程模拟线程,并不是真正的线程,所以linux的解决方案是中间添加一层软件层,向上提供用户所认为的线程接口,向下则调用对应的Linux提供的轻量级进程的接口与用户调用的线程接口与之对应,所以当我们有五个线程时我们都可以看到有5个不同的LWP。Linux天然的就知道进程管理的那一套方法,知道怎么创建,如何被调度,进程状态是运行还是等待,知道怎么回收,怎么终止等等这一系列对进程的管理。那么它又是怎么知道创建了几个线程,而线程的状态是什么呢?创建了几个线程其实我们的用户就知道,所以我们把这种线程叫做用户级线程,因为它是在用户层被实现的,并非在linux操作系统内,linux操作系统只有轻量级进程。本身那些用户级线程就要与linux操作系统下的轻量级进程一一对应,轻量级进程本身也很多被linux操作系统管理起来了,那么线程也相应的需要被管理起来,由谁管理,那么如何管理呢?线程是被pthread库管理起来的,所以要先描述在组织。所以就可以通过一个
struct tcb{
//lwp id
}
这样的结构体来进行维护的,然后里面的属性应该直接或间接的与lwp id产生相对应的关系,pthread库会把linux操作系统提供的跟轻量级进程相关的系统调用接口给封装起来,tcb中有些属性这样就可以做到与linux系统产生1:1的对应关系。如果用户层存在很多很多的线程,那么就需要某种数据结构将这些struct tcb对象一个一个的组织起来,所以在库里面我们就可以把线程管理起来。
2.系统调用问题
线程要有独立属性:
a.上下文
线程要有自己独立的上下文,上下文是以轻量级进程的形式维护在PCB当中的。
b.栈
栈在进程地址空间当中其实只有一个,而线程要有自己独立的栈,但是一般都会有多个线程的,那么这个栈该给哪个线程用呢? 其实栈只有一个是因为维护栈结构的寄存器只有一套,有edp,esp,所以每一个线程都有自己独立的栈我们该如何理解呢?
我们知道linux系统中只有轻量级进程的概念,创建轻量级进程的接口是clone,我们下面来认识一下这个接口:
其实这个库当中会在堆空间上申请一段空间充当我们的栈,然后把地址传入到clone接口里面,所以每个新线程的栈在库中维护。其实我们之前谈到的文件有对应的struct file,还有与之对应的文件缓冲区,这个缓冲区是在c语言库中维护的,所以库自己是可以对一段内存空间做维护,同样的道理pthread库也可以维护一段内存空间,所以说新线程的栈是在库中维护的,而我们的库一般是被放在到进程地址空间中的堆栈之间的共享区的,动态库是加载到这里,动态库一旦被加载到物理内存当中,然后通过页表映射到共享区当中,新线程创建的时候,pthread库会在堆中new出来一段空间然后用指针进行维护,给新创建的线程通过clone接口把该地址作为栈的起始地址传进去,这就给线程维护了独立的栈,那么当线程终止的时候只需要把这段空间释放掉就可以了,可是在有新线程之前一定是要先有主线程的,默认地址空间当中的栈是给主线程使用的。
3.理解phtread库管理线程
假设这是我们自己的多线程程序mythread,当我们的mythread允许时会将数据代码加载到内存当中,如果是单纯的打印不用库的程序直接就可以运行了,但是今天我们这个程序使用到了pthread动态库的,要想使用一系列的pthread_create创建线程,pthread_join线程等待,访问pthread_t等这些方法之类的需要先看到我们对应的库,所以动态库也是要被加载到内存中的,然后通过页表映射到进程地址空间的共享区当中,这个库就可以提供各种线程相关的方法属性,操作系统就可以创建相应的PCB了。当然这还只是单进程,考虑到操作系统中有多个进程,多个进程也会创建对应的多线程然后都会用到对应的pthread库,pthread是动态库可以共享的,所以该操作系统内的所有线程都要依赖于这个库,用这同一个pthread库,只需加载到内存中一次就可以一直被使用,所有线程被这个pthread库所管理,所以我们称这个库是共享库。也就是说线程库是共享的,所以内部要管理整个系统,多个用户启动的所有线程。
假设主线程是上图中tid2要通过pthread_join(pthread_t , &ret)获取tid的退出结果。这里的pthrea_t tid是线程属性集合在库中的地址。
4.站在语言角度理解pthread
因为我们知道c++11已经开始支持多线程了,下面我们用一段c++语言的代码来写一段多线程代码:
mythread.cc
#include<iostream>
#include<unistd.h>
#include<thread>
#include<cstdlib>using namespace std;void myrun()
{while(true){cout<<"I am a thread!"<<endl;sleep(1);}
}int main()
{thread t(myrun);t.join();return 0;
}
makefile:
mythread:mythread.ccg++ -o $@ $^ -std=c++11 .PHONY:clean
clean:rm -rf mythread
编译运行之后:
我们发现运行不了,然后我们编译时加上-lpthread
makefile:
mythread:mythread.ccg++ -o $@ $^ -std=c++11 -lpthread.PHONY:clean
clean:rm -rf mythread
然后再编译一次再运行:
发现没有报错了。
我们再打开监视窗口
发现确实是两个线程。
这说明了什么呢?如果不加上-lpthread那么这段代码就运行不了,说明c++11支持的多线程底层封装了pthread库,也是用到原生线程库pthread的。
5.理解线程局部存储
我们直接先用一段代码来进行现象说明:
#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <pthread.h>using namespace std;int g_val = 100; // 全局变量,本身就是被所有线程共享的void *threadRoutine(void *args)
{std::string name = static_cast<const char *>(args);while (true){sleep(1);std::cout << name << ", g_val: " << g_val << " ,&g_val: " << &g_val << "\n"<< std::endl;g_val++;}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");while (true){sleep(1);std::cout << "main thread, g_val: " << g_val << " ,&g_val: " << &g_val << "\n"<< std::endl;}pthread_join(tid, nullptr);return 0;
}
运行结果:
我们发现g_val的值一旦被新线程改变,主线程立马就能看到,全局变量,本身就是被所有线程共享的。
如果我们在g_val前面加上一个__thread
__thread int g_val = 100; // 全局变量,本身就是被所有线程共享的
其他代码不变的情况下的运行结果:
我们发现主线程的g_val与新线程的g_val的值和地址都不一样,这就是用到了线程的局部存储。
而获取线程的lwp我们可以通过这个接口获取syscall(SYS_gettid);
下面用一段代码来进行获取:
#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <pthread.h>using namespace std;// int g_val = 100; // 全局变量,本身就是被所有线程共享的
__thread int g_val = 100; // 线程的局部存储!有什么用?有什么坑?__thread pid_t lwp = 0;// __thread std::string threadname;pid_t gettid() {return syscall(SYS_gettid);
}void *threadRoutine(void *args)
{std::string name = static_cast<const char *>(args);lwp = gettid(); // 调用系统调用 SYS_gettid 获取当前线程的 TIDwhile (true){sleep(1);std::cout << name << ", g_val: " << g_val << " ,&g_val: " << &g_val << "\n"<< std::endl;std::cout <<"new thread: " << lwp << std::endl;g_val++;}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");lwp = gettid(); // 调用系统调用 SYS_gettid 获取当前线程的 TIDstd::cout <<"main thread: " << lwp << std::endl;while (true){sleep(1);std::cout << "main thread, g_val: " << g_val << " ,&g_val: " << &g_val << "\n"<< std::endl;}pthread_join(tid, nullptr);
}
运行结果:
这就获取到了线程的lwp.
6.用c++代码进行线程封装
//thread.hpp
#pragma once
#include<iostream>
#include<string>
#include<functional>
#include<pthread.h>
template<class T>
using func_t = std::function<void(T)>;template<class T>
class Thread
{
public:Thread(const std::string & threadname,func_t<T> func,T data):_tid(0),_isrunning(false),_threadname(threadname),_func(func),_data(data){}static void* ThreadRoutine(void * args){Thread* ts =static_cast<Thread*>(args);ts->_func(ts->_data);return nullptr;}bool Start(){int n = pthread_create(&_tid,nullptr,ThreadRoutine,this);if(n==0){_isrunning = true;return true;}return false;}bool Join(){if(!_isrunning) return true;int n = pthread_join(_tid,nullptr);if(n==0){_isrunning = false;return true;}return false;} std::string ThreadName(){return _threadname;}bool IsRunning(){return _isrunning;}~Thread(){}private:pthread_t _tid;bool _isrunning;std::string _threadname;func_t<T> _func;T _data;
};
//main.cc
#include<iostream>
#include<string>
#include<unistd.h>
#include"Thread.hpp"std::string GetThreadName()
{static int num = 1;char name[64];snprintf(name,sizeof(name),"Thread-%d",num++);return name;
}void Print(int num)
{while(num){std::cout<<"hello world: "<<num--<<std::endl;sleep(1);}
}int main()
{Thread<int> t(GetThreadName(),Print,10);std::cout<<"thread is running? "<<t.IsRunning()<<std::endl;t.Start();std::cout<<"thread is running? "<<t.IsRunning()<<std::endl;t.Join();return 0;
}
Makefile
thread_test:main.ccg++ -o $@ $^ -std=c++11 -lpthread.PHONY:clean
clean:rm -rf thread_test
运行结果: