今天为大家讲一讲Linux中的线程。这部分的知识细节比较多,篇幅可能较长,但我们一步一步将每一个知识点搞清楚,Linux线程对我们来说就小菜一碟啦!
众所周知,linux中每创建出一个概念,都会使用先描述再组织的方法。例如进程我们使用PCB来描述的,文件我们用struct_file描述并用文件描述符表进行管理。但是Linux中其实并没有线程这个概念,而我们这里所说的线程本质是一个轻量级进程(后面我们详细讲)所以Linux就没有自己的系统调用来创建一个线程,而是有系统调用创建一个轻量级进程!因此想要在Linux环境下使用,就必须调用pthread库。
线程的本质:
当一个进程运行时并创建出一个线程,OS为该系统创建出和进程一摸一样的pcb,该pcb中的虚拟地址空间和进程中的一摸一样,因此线程是通过和主进程看到同一份资源来运行的。它没有自己的虚存,页表,和主进程共享一份,因此它和进程的区别就是它变轻了,因此线程本质就是一个轻量级进程。线程一旦被创建,线程和进程的大部分资源都是共享的。线程就是进程里面的一个执行流。
所以cpu在调度一个进程的时候,它看到的一个一个的pcb,而今天的pcb它可能是一个进程,也可能是一个线程,但是cpu不管是进程还是线程,只要调度就运行pcb里的代码和数据。因此线程是cpu调度的基本单位。
在今天我们对进程有了新的认识,以前我们知道进程=内核数据结构+数据代码。而今天进程是承担系统资源分配的基本实体。进程就相当于一个大家庭,而线程就是家庭成员,家庭成员共同努力是为了让这个家变得更好。线程也是共同努力合作完成一个任务。
线程的创建:
以下是创建一批线程的代码:
第一个参数是线程的pid,第二个参数我们默认填nullptr,第三个参数是线程调用的方法,是四个参数是调用方法的形参。
class ThreadData
{
public:
int number;
pthread_t tid;
char namebuffer[64];
};
void *start_routine(void *args)
{
// sleep(1);
// 一个线程如果出现了异常,会影响其他线程吗?会的(健壮性或者鲁棒性较差)
// 为什么?进程信号,信号是整体发给进程的!
ThreadData *td = static_cast(args); // 安全的进行强制类型转化
int cnt = 10;
while (cnt)
{
cout << "cnt: " << cnt << " &cnt: " << &cnt << endl; // bug
cnt--;
sleep(1);
cout << "new thread create success, name: " << td->namebuffer << " cnt: " << cnt-- << endl;
}
}
int main()
{
vector threads;
#define NUM 10
for(int i = 0; i < NUM; i++)
{
ThreadData *td = new ThreadData();
td->number = i+1;
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);
pthread_create(&td->tid, nullptr, start_routine, td);
threads.push_back(td);
// pthread_create(&tid, nullptr, start_routine, (void*)"thread one");
// pthread_create(&tid, nullptr, start_routine, namebuffer);
// sleep(1);
}
这时就会有人困惑,为什么不能将namebuffer在外面创建好,直接用下面的方式创建线程:
pthread_create(&tid, nullptr, start_routine, namebuffer);
因为创建一批线程,在for循环里面所有的资源都是共享的。所以当你把namebuffer当作参数传入的时候。当参数传给start_routine完毕时,线程才完全构建好。因为线程运行的优先顺序是随机的,速度非常快。所以可能前面进程还没有完全的讲自己的namebuffer完全构建好,就被后面的进程刷新覆盖了(namebuffer出了作用域被销毁,下一个线程被创建时,由于函数栈帧的原因,使用的nabuffer和上一个线程使用的位置一样,资源共享)。所以使用传入new出来的指针的方法,每一个线程看到的是独自的空间。
在任务中,局部变量cnt、td是独立的,因为每一个线程又拥有一个独立栈,当我们打印cnt的地址时,发现它们各自的地址是不一样的。
线程退出有两种方法:一种是返回值退出,另一种是使用系统调用。
那为什么不能使用exit?之前我们讲过exit退出是操作系统向进程发信号最后杀死进程。如果使用的话进程里的所有线程就会被杀死,就不能达到单个线程退出的目的。
返回值退出很简单,我们可以直接返回一个nullptr,如果你想传其他的值你也可以这样做:
但如果你想返回的不是一个数而是一组数,你也可以返回一个类的地址。但前提是类对象必须是new出来的,因为出了作用域战阵被销毁,返回的指针必将是一个野指针。
class ThreadReturn
{
public:
int exit_code;
int exit_result;
};
ThreadReturn * tr = new ThreadReturn();
tr->exit_code = 1;
tr->exit_result = 106;
return &tr;
另一种方法是使用pthread_exit():
参数就是你需要线程退出时返回的值。
线程也是可以被取消的,记住:取消线程前的其前提是这个线程已经开始跑起来了!!
线程和进程一样需要被等待,因为需要拿到线程退出的信息,以及回收对应线程的pcb资源:
首先我们学习一下pthread_join这个接口:
第二个参数是用来接收线程的返回值的,它是一个输出型参数便于我们获取返回值的结果,以下是具体的用法:
为什么要在用户栈上定义一个变量传到pthread_join里面呢?其背后的原理如下:
当线程执行一个任务并且结束时,它的返回值是要被保存到pthread库里面去的,当我们将上面的ret的地址传进去,并且调用pthread_join时,将返回值赋值给了*ret。这样就直接把返回值赋值给了ret,以下是实例图:
当一个线程进行分离以后,它就不需要被join了,如果join,就会报错:
现在我们来谈一谈线程id的作用:
进程需要被操作系统先描述再组织,那么线程需要也需要这样吗?答案是:是的。
在我们创建线程的时候,第二个参数是一个输出型参数,它就是一个结构体,它的作用就是描述一个线程并进行更好的管理。
我们都知道当我们使用线程的时候,需要引入线程库,线程库被加载到虚拟地址空间的共享区中,而线程库中就是我们创建的一个一个线程,并以结构体的方式管理起来:
所以线程id就是每个线程在动态库的起始位置,通过起始地址就可以找到每一个线程。线程独立的栈结构也是在共享区,并被每一个pthread_attr_t这个结构体管理着,所以线程各自的数据不会相互影响。而主线程的栈就是在虚存的栈区!!
那么上图中的线程局部存储是什么呢?我们用代码来演示:
int num =0;
void* task(void* args)
{
while(1)
{
cout<<"我是一个新线程,num:"<
以上代码的结果:
毋庸置疑,因为num是全局变量,一个线程对该值做修改,另一个线程也会看到改变后的值。所以两个线程看到的是同一个num,那么进行如下修改(thread前面加两个杠),结果会变成怎样呢?
以下是运行结果:
可以看到,两个线程看到的是不同地址的num,并且地址要比之前大了不少。原因就是加了__thread以后数据就变成了线程的局部存储,该值是存储在共享区的,已初始化代码区的地址比共享区的地址低,所以看到地址变大了。因为是局部存储,所以新线程对该值做修改不会影响其他进程。
到这里线程的控制就全部结束了,接下来我们封装以下线程库中的接口,使它和c++一样使用起来更加方便:
#include
#include
#include
#include
#include
using namespace std;
class Thread;
class Context
{
public:
Context()
: _this(nullptr), _args(nullptr)
{
}
~Context()
{
}
public:
Thread *_this;
void *_args;
};
class Thread
{
public:
typedef function func_t;
static void *task(void *args)
{
// 静态成员不能访问类内的非静态成员,所以必须将类的上下文传进来
Context *ctx = static_cast(args);
void *ret = ctx->_this->_func(ctx->_args);
delete ctx;
return ret;
}
Thread(func_t func, void *args = nullptr, int num = 0)
: _func(func), _args(args), _num(num)
{
char namebuffer[64];
snprintf(namebuffer, sizeof(namebuffer), "Pthread %d", _num);
_name = namebuffer;
Context *text = new Context();
text->_this = this;
text->_args = _args;
// 调用c式的接口识别不出来C++的东西,如_func。
// pthread_create(&_tid,nullptr,_func,_args);
int n = pthread_create(&_tid, nullptr, task, text);
assert(n == 0);
(void)n;
}
void join()
{
int n = pthread_join(_tid, nullptr);
assert(n == 0);
(void)n;
}
~Thread()
{
}
private:
pthread_t _tid;
int _num;
string _name;
func_t _func;
void *_args;
};
所以到后面的学习,我们就可以用我们写好的线程,这样使用起来更加方便: