文章目录
- 线程概念
- Linux中线程是否存在的讨论
- 线程创建和线程控制
- 线程的终止和等待(三种终止方式 + pthread_join()的void**retval)
线程概念
线程就是进程内部的一个执行流,线程在进程内运行,线程在进程的地址空间内运行,拥有该进程的一部分资源。
这句话一说可能老铁们直接蒙蔽,线程就线程嘛,怎么还在进程里面运行呢?还在地址空间内运行?而且拥有进程的一部分资源,这都是什么鬼?
如何看待线程在地址空间内运行呢?实际进程就像一个封闭的屋子,线程就是在屋子里面的人,而地址空间就是一个个的窗户,屋子外面就是进程对应的代码和数据,一个屋子里面当然可以有多个人,而且每个人都可以挑选一个窗户看看外面的世界。
在上面的例子中,每个人挑选一个窗户实际就是将进程的资源分配给进程内部的多个执行流,以前fork创建子进程的时候,不就是将父进程的一部分代码块儿交给子进程运行吗?子进程不就是一个执行流吗?
而今天我们所谈到的线程道理也是类似,我们可以将进程的资源划分给不同的线程,让线程来执行某些代码块儿,而线程就是进程内部的一个执行流。那么此时我们就可以通过地址空间+页表的方式将进程的部分资源划分给每一个线程,那么线程的执行粒度一定比之前的进程更细!
Linux中线程是否存在的讨论
我们在思考一下,如果Linux在内核中真的创建出了我们上面所谈论到的线程,那么Linux就一定要管理内核中的这些线程,既然是管理,那就需要先描述,再组织,创建出真正的 TCB(Thread Create Block)结构体来描述线程,线程被创建的目的不就是被执行,被CPU调度吗?既然所有的线程都要被调度,那每个线程都应该有自己独立的thread_id,独立的上下文,状态,优先级,独立的栈(线程执行进程中的某一个代码块儿)等等,那么大家不觉得熟悉吗?单纯从CPU调度的角度来看,线程和进程有太多重叠的地方了!
所以Linux中就没有创建什么线程TCB结构体,直接复用进程的PCB当作线程的描述结构体,用PCB来当作Linux系统内部的"线程"。这么做的好处是什么呢?如果要创建真正的线程结构体,那就需要对其进行维护,需要和进程构建好关系,每个线程还需要和地址空间进行关联,CPU调度进程和调度线程还不一样,操作系统要对内核中大量的进程和线程做管理,这样维护的成本太高了!不利于系统的稳定性和健壮性,所以直接复用PCB是一个很好的选择,维护起来的成本很低,因为直接复用原来的数据结构就可以实现线程。所以这也是Linux系统既稳定又高效,成为世界上各大互联网公司服务器系统选择的原因。(而windows系统内是真正有对应的TCB结构体的,他确实创建出了真正的线程,所以维护起来的成本就会很高,这也是windows用的用的就卡起来,或者蓝屏的原因,因为不好维护啊,实现的结构太复杂!代码健壮性不高)
在知道linux的线程实现方案之后,我们又该如何理解线程这个概念呢?现在PCB都已经不表示进程了,而是代表线程。以前我们所学的进程概念是:进程的内核数据结构+进程对应的代码和数据,但今天站在内核视角来看,进程的概念实际可以被重构为:承担分配系统资源的基本实体!进程分配了哪些系统资源呢?PCB+虚存+页表+物存。所以进程到底是什么呢?
那在linux中什么是线程呢?线程是CPU调度的基本单位,也就是struct task_struct{},PCB就是线程,为进程中的执行流!
那我们以前学习的进程概念是否和今天学习的进程概念冲突了呢?当然没有,以前的进程也是承担分配系统资源的基本实体,只不过原来的进程内部只有一个PCB,也就是只有一个执行流,而今天我们所学的进程内部是有多个执行流,多个PCB!
所以: Linux内核中有没有真正意义上的线程, Linux用进程的PCB来模拟线程,是完全属于自己实现的一套方案!
站在CPU的角度来看,每一个PCB,都可以称之为轻量级进程,因为它只需要PCB即可,而进程承担分配的资源更多,量级更重!
Linux线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体!
进程用来整体向操作系统申请资源,线程负责向进程伸手要资源。如果线程向操作系统申请资源,实质上也是进程在向操作系统要资源,因为线程在进程内部运行,是进程内部的一部分!
Linux内核中虽然没有真正意义上的线程,但虽无进程之名,却有进程之实!
程序员只认线程,但Linux没有线程只有轻量级进程,所以Linux无法直接提供创建线程的系统调用接口,只能提供创建轻量级进程的接口!
线程创建和线程控制
#include <iostream>
#include <string>
#include<unistd.h>using namespace std;void *start_routine(void *arg)
{string name = static_cast<const char *>(arg);while (true){cout << "new thread: " << name << endl;sleep(1);}
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, start_routine, (void *)"thread-1");while (true){cout << "main thread" << endl;sleep(1);}return 0;
}
创建一个线程比较简单没什么含金量,所以在线程控制这里选择创建一批线程,来看看多个线程下的进程运行情况。
在线程的错误检查这里,并不会设置全部变量errno,道理也很简单,线程出错了,那其实就是进程出错了,错误码这件事不应该是我线程来搞,这是你进程的事情和我线程有什么关系?所以线程也没有理由去设置全局变量errno,他的返回值只表示成功或错误,具体的返回状态,其实是要通过pthread_join来获取的!
创建一批线程也并不困难,我们可以搞一个vector存放创建出来的每个线程的tid,但从打印出来的新线程的编号可以看出来,打印的非常乱,有的编号还没有显示,这是为什么呢?(我们主观认为应该是打印出来0-9编号的线程啊,这怎么打印的这么乱呢?)
其实这里就涉及到线程调度的话题了,创建出来的多个新线程以及主线程谁先运行,这是不确定的,这完全取决于调度器,我们事先无法预知哪个线程先运行,所以就有可能出现,新线程一直没有被调度,主线程一直被调度的情况,也有可能主线程的for循环执行到i等于6或9或8的时候,新线程又被调度起来了,此时新线程内部就会打印出创建成功的语句。所以打印的结果很乱,这也非常正常,因为哪个线程先被调度是不确定的!
线程的终止和等待(三种终止方式 + pthread_join()的void**retval)
再谈完线程的创建之后,那什么时候线程终止呢?所以接下来我们要谈论的就是线程终止的话题,线程终止总共有三种方式,分别为return,pthread_exit,pthread_cancel
我们知道线程在创建的时候会执行对应的start_routine函数指针指向的方法,所以最正常的线程终止方式就是等待线程执行完对应的方法之后,线程自动就会退出,如果你想要提前终止线程,可以通过最常见的return的方式来实现,线程函数的返回值为void*,一般情况下,如果不关心线程退出的情况,直接return nullptr即可。
和进程终止类似的是,除return这种方式外,原生线程库还提供了pthread_exit接口来终止线程,接口的使用方式也非常简单,只要传递一个指针即可,同样如果你不关心线程的退出结果,那么也只需要传递nullptr即可。
#include <iostream>
#include <string>
#include<unistd.h>
#include<vector>
#include <stdio.h>
#include<functional>
#include <time.h>
#include <pthread.h>#define NUM 10
using namespace std;
// using func_t = function<void()>;
typedef function<void()> func_t;class ThreadData
{
public:ThreadData(const std::string &name, const time_t &ctime, func_t f):_name(name), _createtime(ctime), _func_t(f){}public:string _name;time_t _createtime;func_t _func_t;
};void Print()
{std::cout << "我是线程执行的大任务的一部分" << std::endl;
}void* start_routine(void* arg)
{ThreadData* td = static_cast<ThreadData*> (arg);cout << "I am a new thread, my name is : "<< td->_name << " creatname is: " << td->_createtime << endl; td->_func_t();//return nullptr; //线程终止pthread_exit(nullptr);
}int main()
{//创建一批线程for(int i = 0; i < NUM; i++){pthread_t tid;char threadname[64];snprintf(threadname, sizeof(threadname), "%s: %d", "thread" , i+1);//创建一个线程数据对象string tdname = threadname;ThreadData* td = new ThreadData(tdname, (time_t)time(nullptr), Print);pthread_create(&tid, nullptr, start_routine, td);sleep(1);}return 0;
}
谈完上面两种线程终止的话题后,第三种终止方式我们先等会儿再说,与进程类似,进程退出之后要被等待,也就是回收进程的资源,否则会出现僵尸进程,僵尸的这种状态可以通过ps指令+axj选项看到,同时会产生内存泄露的问题。
线程终止同样也需要被等待,但线程这里没有僵尸线程这样的概念,如果不等待线程同样也会造成资源泄露,也就是PCB资源未被回收,线程退出的状态我们是无法看到的,我们只能看到进程的Z状态。
原生线程库给我们提供了对应的等待线程的接口,其中join的第二个参数是一个输出型参数,在join的内部会拿到线程函数的返回值,然后将返回值的内容写到这个输出型参数指向的变量里面,也就是写到我们用户定义的ret指针变量里,通过这样的方式来拿到线程函数的返回值。
通过bash的打印结果就可以看到,每个线程都正常的等待成功了。
#include <iostream>
#include <string>
#include<unistd.h>
#include<vector>
#include <stdio.h>
#include<functional>
#include <time.h>
#include <pthread.h>#define NUM 5
using namespace std;
// using func_t = function<void()>;
typedef function<void()> func_t;class ThreadData
{
public:ThreadData(const std::string &name, const time_t &ctime, func_t f):_name(name), _createtime(ctime), _func_t(f){}public:string _name;time_t _createtime;func_t _func_t;
};void Print()
{std::cout << "我是线程执行的大任务的一部分" << std::endl;
}void* start_routine(void* arg)
{ThreadData* td = static_cast<ThreadData*> (arg);cout << "I am a new thread, my name is : "<< td->_name << " creatname is: " << td->_createtime << endl; td->_func_t();//return nullptr; //线程终止//pthread_exit(nullptr);return (void*)110;
}int main()
{vector<pthread_t> tids;//保存线程的tid//创建一批线程for(int i = 0; i < NUM; i++){pthread_t tid;char threadname[64];snprintf(threadname, sizeof(threadname), "%s: %d", "thread" , i+1);//创建一个线程数据对象string tdname = threadname;ThreadData* td = new ThreadData(tdname, (time_t)time(nullptr), Print);pthread_create(&tid, nullptr, start_routine, td);tids.push_back(tid);sleep(1);}void* retval = nullptr;for(int i = 0; i < NUM; i++){//线程的等待pthread_join(tids[i], &retval);cout << "join sucess, retval is: ";cout << (long long)retval << endl;sleep(1);}return 0;
}
在了解join拿到线程函数的返回值之后,我们再来谈最后一个线程终止的方式pthread_cancel,叫做线程取消。首先线程要被取消,前提一定得是这个线程是跑起来的,跑起来的过程中,我们可以选择取消这个线程,换个说法就是中断这个线程的运行。
如果新线程是被别的线程取消的话,则新线程的返回值是一个宏PTHREAD_CANCELED,这个宏其实就是把-1强转成指针类型了,所以如果我们join被取消的线程,那join到的返回值就应该是-1,如果线程是正常运行结束退出的话,默认的返回值是0.
我们让创建出来的每个新线程跑10s,然后在第5s的时候,主线程取消前5个线程,那么这5个线程就会被中断,主线程阻塞式的join就会提前等待到这5个被取消的线程,并打印出线程函数的返回值,发现结果就是-1,再经过5s之后,其余的5个线程会正常的退出,主线程的join会相应的等待到这5个线程,并打印出默认为0的退出结果。
#include <iostream>
#include <string>
#include <unistd.h>
#include <vector>
#include <stdio.h>
#include <functional>
#include <time.h>
#include <pthread.h>#define NUM 10
using namespace std;
// using func_t = function<void()>;
typedef function<void()> func_t;class ThreadData
{
public:ThreadData(const std::string &name, const time_t &ctime, func_t f): _name(name), _createtime(ctime), _func_t(f){}public:string _name;time_t _createtime;func_t _func_t;
};void Print()
{std::cout << "我是线程执行的大任务的一部分" << std::endl;
}void *start_routine(void *arg)
{ThreadData *td = static_cast<ThreadData *>(arg);int cnt = 10;while (cnt--){cout << "I am a new thread, my name is : " << td->_name << " creatname is: " << td->_createtime << endl;td->_func_t();// return nullptr; //线程终止// pthread_exit(nullptr);// return (void*)110;sleep(1);}
}int main()
{vector<pthread_t> tids; // 保存线程的tid// 创建一批线程for (int i = 0; i < NUM; i++){pthread_t tid;char threadname[64];snprintf(threadname, sizeof(threadname), "%s: %d", "thread", i + 1);// 创建一个线程数据对象string tdname = threadname;ThreadData *td = new ThreadData(tdname, (time_t)time(nullptr), Print);pthread_create(&tid, nullptr, start_routine, td);tids.push_back(tid);//sleep(1);}sleep(5);for (int i = 0; i < NUM / 2; i++){pthread_cancel(tids[i]);cout << "cancel: " << tids[i] << "success" << endl;}void *retval = nullptr;for (int i = 0; i < NUM; i++){// 线程的等待pthread_join(tids[i], &retval);cout << "join sucess, retval is: ";cout << (long long)retval << endl;//sleep(1);}return 0;
}