目录
我们说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
线程分离
默认新线程是joinable的,就是可等待的,我们也必须等待,否则会出现内存泄漏。但是有时候其实我们并不想管新进程的运行结果,不想让主进程进程阻塞式等待,我们只是想让新进程结束后就自己退出就行了,这时就用到了线程分离,要用到的接口是
man pthread_detach参数就是传新线程的id就可以了,我们可以在新线程用这个借口,也可以在主线程用这个借口
线程分离其实就是改变线程的一个状态(detached),分离后新线程肯定还是和主线程共享资源的,其他性质都是不变的,如果新线程分离后还等待的话就会出错
无论是线程分离还是之前的SIGCHLD信号,都是让父进程(主线程)不管子进程(新线程),其实一个软件一般就是一个死循环,只要我们不关掉,它就永远不会关,这时主线程其实是无暇去等待新线程的,线程分离也是为了这样的一种场景
线程的优缺点
优点:
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标识了线程的拥有者和所属组)
C++11中的多线程
c++11也推出了多线程模块,我们只需要包含头文件<thread>就可以创建新线程,我们先来简单的用一下
#include <iostream> #include <thread> #include <unistd.h> using namespace std; void threadrun(int a, int b) { int cnt = 5; while (cnt--) { cout << "a+b=: " << a + b << endl; sleep(1); } } int main() { thread t1(threadrun, 10, 20); cout << "I am main thread" << endl; t1.join(); return 0; }
我们如果不显示链接原生线程库的话就会出现如下的链接错误
这其实就证明C++11的多线程其实就是封装了原生线程库,这跟fopen在Linux下封装了open一样,语言只能封装系统调用才可以修改内核的数据,并且在不同的OS下封装各自OS的系统调用也是为了实现语言的跨平台性。
简单封装
那我们下面就用Linux提供的系统调用封装出类似C++11提供的库中的多线程
#pragma once #include <iostream> #include <string> #include <functional> #include <pthread.h> using namespace std; namespace MyThread { template<class T> using fun_t = function<void(const T &)>; template <class T> class Thread { private: // using fun_t = function<void(const T &)>;//如果写里边是这样的 // 等价于 typedef function<void(const T &)> fun_t; void excute() { _func(_data); } public: Thread(fun_t<T> func, const T &data, string &&name) : _func(func), _data(data), _stop(true), _name(forward<string>(name)) {} static void *threadrun(void *args)//如果不是静态,会有this指针 { Thread<T> *ptr = reinterpret_cast<Thread<T> *>(args); ptr->excute(); return nullptr; } bool start() { int n = pthread_create(&_id, nullptr, threadrun, this);//把this当参数传过去 if (n == 0) { _stop = false; return true; } else { return false; } } void join() { if (!_stop) { pthread_join(_id, nullptr); } } void detach() { if (!_stop) { pthread_detach(_id); } } void stop() { _stop = true; } const string &name() { return _name; } private: pthread_t _id; string _name; bool _stop; fun_t<T> _func; T _data; }; }
我们要用的话也是非常简单
#include<vector> #include"MyThread.hpp" using namespace MyThread; void print(const int& a) { cout<<"a= "<<a<<endl; } int main() { vector<Thread<int>>threads; for(int i=1;i<=10;i++)//创建一批线程 { string name="thread-"+to_string(i); threads.emplace_back(print,10,name); } for(auto&e:threads)//启动一批线程 { e.start(); } for(auto&e:threads)//等待一批线程 { e.join(); cout<<"wait thread done,thread is:"<<e.name()<<endl; } return 0; }
我们如果想创建多个线程可以这样
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)==8 pthread_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; }