怎么做网站知乎商旅100网页版
目录
线程等待
线程退出
线程的优缺点
线程独占和共享的数据
我们说Linux是用进程模拟的线程,所以Linux中只有轻量级进程的概念,但是,用户是只认线程的,所以我们有一个叫原生线程库的东西,它就负责把轻量级进程的系统调用进行封装,转成线程相关的接口提供给用户。
为什么叫原生呢?就是说只要你装Linux系统,就必须装这个库,就是默认装好的库。
有关线程的接口在3号手册,就证明它们不是系统调用,因为系统调用在2号手册。当然也有创建轻量级进程的系统调用
man clone,它是在二号手册
我们可以说,其实pthread_create其实就是封装了这个系统调用
pthread_create创建新线程时第一个参数是输出型参数,会带回来一个id值,那么这个值和LWP之间是什么关系呢?我们可以断定的认为它们是一对一的关系,用户层使用id值,内核只用LWP,我们可以打印一下这个id值,我们可以把id值改成十六进制
这个值很大,其实它就是一个地址,位于栈和堆之间的共享区的一个地址,我们后面会有讲解
这是主进程获取新线程的id值,那么一个线程如何获取它自己的id值呢?
man pthread_self
这个函数就是谁调用它就获取谁的id值
我们从主线程和新线程中获取的新线程的id肯定是一样的
新线程和主线程谁先运行呢?这个是不确定的,是由调度器来决定的
pthread_create的第四个参数是给新线程要执行的函数传的参数,是void*类型,我们之前都传的是nullptr,我们可以传一个名字
新主线程大部分资源是共享的,比如地址空间是共享的,我如果定义一个全局变量,那么新主进程都可以看见
我们可以看到,新线程对于全局数据的修改,主线程也是可以看到的
线程等待
主线程如果退出的话,不管新线程是否退出,整个进程都会退出,意味着资源会释放,所有线程都会退出。所以我们往往要求主线程是最后退出的
并且线程也是要被等待的,否则会像进程那样发生内存泄漏的问题,线程等待就要用到下面这个接口
man pthread_join
这个接口也是默认是阻塞式等待的
第一个参数是要等待哪个线程
第二个参数是等待到了什么东西,因为新线程的返回值是void*类型的,所以要用void*类型去接收,它是一个输出型参数,所以我要传的参数是void*的指针,所以是void**
我们简单来用一下:
void *newthreadrun(void *args) {char *str = (char *)args;int cnt = 5;while (cnt--){cout << str << " is running " << endl;sleep(1);}return (void *)12345; } int main() {pthread_t tid;pthread_create(&tid, nullptr, newthreadrun, (void *)"pthread-1");void *ret = nullptr;int n = pthread_join(tid, &ret);if (n == 0){cout << " return val is: " << (long long)ret << endl;//指针64位下是8个字节,强转成long long不会损失精度}return 0; }
如果说线程出异常(比如除零),那么整个进程都会退出,这是因为信号是给进程发送的,一个线程出了问题,那么就是这个进程出了问题,整个进程都要被杀死。
所以我们普遍认为多进程的代码往往健壮性不好,当然这是普遍来说的,如果一个多线程的代码写的很好,健壮性也是不成问题的
所以pthread_join是不考虑新线程的异常情况的,因为只要异常,整个进程都退出了,根本不存在等待的问题;它只考虑代码运行完,结果是否正确
线程退出
如果对一个线程使用exit会怎样呢?它会使整个进程退出,因为exit本身就是用来终止进程的,而不是用来终止线程的
所以我们有一个pthread_exit用来终止线程
man pthread_exit
参数就是像return后面的数据一样,就是想返回什么
上面这种方法是新线程主动退出,那如果是主线程想让新线程退出该怎么办呢?可以用下面的接口
man pthread_cancel
这里如果取消掉新线程并且等待新线程,那么n==0,ret==-1,这里的-1 其实就是一个宏,表示线程被取消了
所以进程退出有三种方式,return,pthread_exit和pthread_cancel
线程的优缺点
优点:
1.创建一个新线程的代价比创建一个新进程小很多,因为创建一个线程只需要创建一个PCB,而进程需要地址空间,页表,文件描述表等等
2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
要解释这个,我们就不得不提到CPU中的cache(缓存)了,CPU要执行某行代码,不是只把这行代码放到CPU中,而是要把执行的代码及周围的代码都放到cache中,这样CPU要执行的下一条代码就很有可能已经在缓存中了,对于不同的线程切换来说,这是有几率命中的,但是对于不同的进程切换,肯定要换掉缓存中的代码
我们可以用lscpu看一下云服务器的CPU配置
3.IO操作基本大部分时间都是在等,所以我们可以趁着等的时间多创建几个线程执行其他的任务
缺点:
1.一般计算密集型的进程的线程数量和CPU的数量相同,如果线程数量太多,也会增加调度的开销
2.健壮性降低,线程之间缺乏保护就容易导致产生不良影响
3.编程难度提高,编写与调试一个多线程程序比单线程程序困难得多
线程独占和共享的数据
线程独占:
1.线程的硬件上下文(CPU寄存器内部的值)(从调度角度而言)
2.线程的独立栈结构(从常规运行的角度)
3.线程的ID
4.信号屏蔽字
5.调度优先级
6.errno变量
线程共享
1.函数和全局变量
2.文件描述符表
3.各种信号的处理方式
4.当前工作目录
5.用户id和组id(这些id标识了线程的拥有者和所属组)
我们如果想创建多个线程可以这样
void *newthreadrun(void *args) {char *str = (char *)args;int cnt=5;while(cnt--){cout << str << " is running " << endl;sleep(1);}delete[] str;//new[]的空间要delete[]return nullptr; } int main() {pthread_t tid;vector<pthread_t>f;for (int i = 1; i <= 5; i++){char*buffer=new char[64];//这里必须new,否则五个线程看到的是一块空间snprintf(buffer, 64, "pthread-%d", i);//不能用sizeof,因为sizeof(buffer)==8pthread_create(&tid, nullptr, newthreadrun, buffer);f.push_back(tid);}for(auto e:f){pthread_join(e,nullptr);}return 0; }
我们也可以将线程和C++中的类和对象联系应用起来
template<class T> T add(T x, T y) {return x + y; } template <class T> class threaddata { public:threaddata(const char *str, function<T(T, T)> task): _name(str), _task(task){delete[] str;}T Dotask(T x, T y){return _task(x, y);}string _name;function<T(T, T)> _task;T _result; };void *newthreadrun(void *args) {threaddata<int> *p = (threaddata<int> *)args;p->_result = p->Dotask(10, 20);cout << p->_name << " get result: " << p->_result << endl;delete p;return nullptr; }int main() {pthread_t tid;vector<pthread_t> f;for (int i = 1; i <= 5; i++){char *buffer = new char[64];snprintf(buffer, 64, "pthread-%d", i);threaddata<int> *ptd = new threaddata<int>(buffer, add<int>);pthread_create(&tid, nullptr, newthreadrun, ptd);f.push_back(tid);}for (auto e : f){pthread_join(e, nullptr);}return 0; }