文章目录
- 一、线程的优点
- 1. 创建的代价
- 2. 切换的代价
- 缓存和进程/线程切换
- 3. 占用的资源
- 4. 效率
- 二、线程的缺点
- 1. 性能损失
- 2. 健壮性降低
- 3. 缺乏访问控制
- 4. 编程难度高
- 三、线程分离
- 1. 线程分离
- 2. pthread_detach ()
- ① 函数细节
- ② 函数使用
- 四、线程自有和共享的数据
- 1. 线程自有的数据
- 2. 线程共享的资源
- 五、多个线程使用公共空间
- 1. 不要把公共空间传给多个线程
- 1.1 问题
- 1. 2 解决
- 2. 线程的传参和返回值,可以是各种对象
- 六、C++11 中的线程
- 1. C++11 的多线程
- 2. thread 类及使用
- 七、线程库对线程的管理
- 1. 线程的管理由谁来做?
- 2. 线程管理的细节
- ① 线程库首先要映射到当前进程的地址空间中
- ② 进程地址空间中的动态线程库
- ③ 整个系统的所有的线程都在这一个库中管理着
- 3. 执行流怎么找到栈的?
- 八、线程的封装
- 1. Pthread.hpp
- 2. test.cc
一、线程的优点
1. 创建的代价
创建一个新线程的代价要比创建一个新进程小的多。
创建线程只需要创建一个新的 PCB
,然后将曾经进程的资源指派给线程即可;而进程创建的时空成本是较高的,有各种数据结构的创建,数据的加载。
2. 切换的代价
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
缓存和进程/线程切换
- 在
CPU
内,每次读取当前进程的代码和数据时,都需要经过虚拟到物理的转换,然后得到内存中的数据放到CPU
中做处理,如果这样,那CPU
读取任何一条指令,都要访问内存,为了提高运行效率,CPU
中存在一种硬件cache
(高速缓冲存储器),在CPU
进行虚拟到物理的寻址时,本来是找到一行代码,较大概率会执行下一行(当然也可能会跳转到别处),所以会将周边数据全读到CPU
内部缓存起来,所以,之后CPU
再访问代码数据时,不用再去读取内存了,而是从cache
中再读取,大大提高CPU
寻址的效率。
- 进程切换和线程切换:
-
进程切换:
之前缓存的数据,就全都没有了,会重新加载新进程的数据 -
线程切换:
cache
中的数据在概率上依旧能用,不用重新清空和重新加载。
-
所以这才是线程成本低的主要原因。
3. 占用的资源
线程占用的资源要比进程少很多。
进程要占用多执行流,地址空间,页表,代码数据;而线程占用的都是一份的。
4. 效率
- 计算密集型应用:
排序、查找、加密、解密、压缩等动作都以计算为主,是计算密集型应用,主要应用的CPU
的资源。 - I/O 密集型应用:
下载、上传、拷贝等就是I/O
密集型应用。
应用要不就是计算密集型,要不就是 I/O
密集型,要不就是两者都是。
- 对于计算密集型应用:
- 为了能在多处理器系统上运行,将计算分解到多个线程中实现。一个线程处理一部分,效率会高。
- 当然也不是创建的线程越多越好,因为会有切换的成本,切换太多,说不定效率还不如一个进程从头算到尾。
- 所以,对于计算密集型,一般是
CPU
有多少个核就创建几个。
可以通过lscpu
查看一下核数:
- 对于I/O 密集型应用:
- 可以多创建进程,因为
I/O
过程大部分都在等,一个进程等10G
的数据,和10
个线程每个等1G
的数据,效率不一样。
- 可以多创建进程,因为
二、线程的缺点
1. 性能损失
多线程切换有成本,像上面的计算密集型应用,线程过多可能会有较大的性能损失(增加了额外的同步和调度开销,而可用资源不变)。
2. 健壮性降低
一个线程出问题,整个进程就挂掉了(这一点,线程 - 线程退出 中有讲到)。
在一个多线程程序里,因共享了不该共享的变量而造成不良影响的可能性很大。
3. 缺乏访问控制
线程间共享资源,对于共享资源,大家都可以任意时间访问,可能会有同时访问同一量的情况,可能会出错。
4. 编程难度高
编写与调试一个多线程程序比单线程程序要困难很多,往往需要全面深入的考虑以保证程序的健壮性。
三、线程分离
1. 线程分离
线程默认是 joinable
(可联接) 的,如果主线程不关心新线程的执行信息,可以将新线程设置为分离状态。
当线程处于分离状态后,退出时会自动释放线程资源。
- 分离和等待:
线程被分离后,就不能被pthread_join()
了,调用这个函数,函数就会返回一个错误码。 - 分离状态:
‘分离’仅仅是一种状态,一种不用主线程等的状态,而不是真正分离了。在这种状态下,线程出异常了,进程会受影响退出;主线程执行完了,进程退出,线程也会退出。
不管是分离还是等待,我们都希望主线程是最后退出的,所以分离的一个场景,就是主进程根本就不退出。
大部分程序都是一直在运行的,称为常驻进程。
2. pthread_detach ()
① 函数细节
thread
参数:要分离的线程的ID
。- 返回值:成功返回
0
,失败返回错误码
② 函数使用
- 主线程分离新线程:
#include <pthread.h>
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
using std::cout;
using std::endl;
void *thfunc(void *arg)
{
while (1)
{
sleep(1);
cout << "new" << endl;
}
return nullptr;
}
int main()
{
pthread_t rid;
pthread_create(&rid, nullptr, thfunc, nullptr);
pthread_detach(rid);
sleep(3);
cout << "man" << endl;
return 0;
}
运行结果:
- 新线程自己分离自己:
void *thfunc(void *arg)
{
pthread_detach(pthread_self());
while (1)
{
sleep(1);
cout << "new" << endl;
}
return nullptr;
}
int main()
{
pthread_t rid;
pthread_create(&rid, nullptr, thfunc, nullptr);
sleep(3);
cout << "man" << endl;
return 0;
}
运行结果:
- 在线程分离后,再通过
pthread_join()
等待线程会等待失败,函数会返回一个错误码:
四、线程自有和共享的数据
进程是资源分配的基本单位,线程是调度的基本单位。进程强调独立,线程强调共享。所以我们来看一下线程自有的和共享的部分。
1. 线程自有的数据
- 线程 ID: 是用户级别的,内核级的不使用线程
ID
,使用的是线程的LWD
。 - 硬件上下文: 每个线程都是被单独调度的执行流,所以要有自己的上下文数据,存放在一组寄存器中。
- 独立栈结构: 线程本质是在执行自己的函数,每个函数内都可以定义各种临时变量,临时变量都是存放在栈上的。每个线程都有独立的用户栈,不敢让它们一起用一块栈空间。
除了上面几个较为重要的,还有 errno
、信号屏蔽字、调度优先级等。
2. 线程共享的资源
- 地址空间
- 文件描述符表:因为文件描述符表表示的是进程和打开文件的关系,而不是线程。
- 每种信号共享的处理方式:这就是为什么一个线程出异常,整个进程就崩掉了。
- 当前工作目录:进程在哪里,线程就在哪里。
- 用户
id
和组id
。
五、多个线程使用公共空间
1. 不要把公共空间传给多个线程
我们来看下面这段代码:
我们想一次性创建 6
个线程,然后每一个线程都在自己的线程函数中打印一下自己的线程名字(从 1
到 6
号)。
#include <pthread.h>
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
using std::cout;
using std::endl;
const int threadsnum = 6; //要创建的线程个数
void *testfunc(void *arg) //线程函数
{
std::string threadsname = static_cast<char *>(arg);
cout << threadsname << endl;
return nullptr;
}
int main()
{
std::vector<pthread_t> ts; //将线程放到 vector 中管理一下
char threadsname[32];
for (int i = 0; i < threadsnum; i++)
{
snprintf(threadsname, 32, "线程 - %d", i + 1); //在 threadsname 这个数组中放线程的名字
pthread_t tid;
pthread_create(&tid, nullptr, testfunc, threadsname); //创建一个新线程,新线程去执行 testfunc 函数了
ts.push_back(tid); //将创建好的新线程放到 vecotr 中
}
for (auto &id : ts)
{
pthread_join(id, nullptr); //等待 vector 中的每一个线程
}
return 0;
}
我们期望的执行结果是这样的:
(调度顺序不同,可能 1-6
号的顺序不同,但 1- 6
号都要有)。
1.1 问题
- 我们来看实际的执行结果:
- 原因分析:
线程函数拿到的参数是一个指针,指向的外部的threadname
空间,一次循环完成之后,再次for
循环还会创建新线程,threadname[64]
中的内容就被覆盖掉了,而for
循环覆盖threadsname
内容的速度快于新线程取threadsname
的内容构成字符串的速度的,最后覆盖后,threadsname
里的内容是6
号的,所以新线程它们运行到构建字符串那里,取去threadsname
里拿到的就是6
号的内容了。 - 修改一下:
所以,只要我们将覆盖threadsname
中内容的操作变慢一些,让新线程在它被覆盖之前就拿到threadsname
里的数据,就不会有上面的情况出现了。
这样确实是符合预期了,但并不是符合要求,难道每个我们都这样卡时间吗?那也太麻烦了,而且不符合实际。
1. 2 解决
给每个线程单独开一块空间,最后再释放掉。
void *testfunc(void *arg)
{
std::string threadsname = static_cast<char *>(arg);
cout << threadsname << endl;
delete[] (char *)arg; //记得释放
return nullptr;
}
int main()
{
std::vector<pthread_t> ts;
for (int i = 0; i < threadsnum; i++)
{
char *threadsname = new char[32];
snprintf(threadsname, 32, "线程 - %d", i + 1);
pthread_t tid;
pthread_create(&tid, nullptr, testfunc, threadsname);
ts.push_back(tid);
}
for (auto &id : ts)
{
pthread_join(id, nullptr);
}
return 0;
}
结果:
完全符合预期。
2. 线程的传参和返回值,可以是各种对象
借由上面的框架,我们可以试一试将对象传给新线程,新线程也返回对象。
示例代码如下:
#include <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <unistd.h>
namespace zzz
{
class Task
{
public:
Task(int x, int y) : _x(x), _y(y)
{
}
int Excute()
{
return _x + _y;
}
private:
int _x;
int _y;
};
class ThreadData
{
public:
ThreadData(int x, int y, std::string name)
: _t(x, y), _thname(name)
{
}
int Excute()
{
return _t.Excute();
}
std::string GetName()
{
return _thname;
}
private:
std::string _thname;
Task _t;
};
class ThreadResult
{
public:
ThreadResult(int retval, std::string name)
: _ret(retval), _thname(name)
{
}
void Print()
{
std::cout << _thname << ":" << _ret << std::endl;
}
private:
int _ret;
std::string _thname;
};
}
using std::cout;
using std::endl;
const int threadsnum = 6;
using namespace zzz;
void *ThreadHandle(void *tdata)
{
ThreadData *td = static_cast<ThreadData *>(tdata);
int retval = td->Excute();
ThreadResult *ret = new ThreadResult(retval, td->GetName());
delete td;
return ret;
}
int main()
{
std::vector<pthread_t> threads;
std::vector<ThreadResult *> thread_rets;
for (int i = 0; i < threadsnum; i++)
{
// 有需要线程处理的数据
char threadsname[32];
snprintf(threadsname, 32, "线程 - %d", i + 1);
ThreadData *tdata = new ThreadData(i + 1, i + 1, (std::string)threadsname);
// 创建一个新线程
pthread_t tid;
pthread_create(&tid, nullptr, ThreadHandle, tdata);
threads.push_back(tid);
}
for (auto &tid : threads)
{
void *ret = nullptr;
pthread_join(tid, &ret);
thread_rets.push_back((ThreadResult *)ret);
}
for (auto ret : thread_rets)
{
ret->Print();
delete ret;
}
return 0;
}
运行结果:
六、C++11 中的线程
1. C++11 的多线程
C++11
的多线程其实是对原生线程库的封装。
封装是为了跨平台性,不同系统底层创建进程时使用的接口不同,如果直接使用系统提供的接口函数,代码换一个系统就识别不了接口函数了。不同平台提供了不同的 C++
标准库,所以在不同的平台,只需要链接不同的库就可以了。
2. thread 类及使用
C++11
的线程是 thread
类。
下面我们来简单使用一下:
#include <iostream>
#include <pthread.h>
#include <thread>
using std::cout;
using std::endl;
void thfunc(int num)
{
cout << "new:" << num << endl;
}
int main()
{
int num = 3;
std::thread t(thfunc, num);
t.join();
cout << "main" << endl;
return 0;
}
运行结果:
七、线程库对线程的管理
1. 线程的管理由谁来做?
系统只有轻量级进程的概念,它提供不了线程所需的数据:线程 id
,优先级等,也就管理不了线程。
库将轻量级进程封装成了线程TCB
,库给提供了创建、等待等线程的管理工作,所以库内管理了线程。
下面我们来看一些内部细节。
2. 线程管理的细节
① 线程库首先要映射到当前进程的地址空间中
② 进程地址空间中的动态线程库
这里有几个需要提及的点:线程ID
,线程栈、线程局部存储。下面我们分别来看一下:
-
线程ID: 从上图我们可以看到线程
ID
到底是什么,在线程 - 四、创建线程中我们反复提到了线程的ID
,其实线程的ID
就是一个地址,是当前线程对象在地址空间中对应位置的其实地址,所以通过线程ID
就可以知道线程的虚拟地址,知道虚拟地址就可以通过页表找到对应的物理内存地址了。 -
线程栈: 在上面的【四、2. 线程自有的数据】部分,我们提及了线程有独立栈结构,现在我们看到它了, 线程的栈结构是在库中维护的,每个线程都有自己的独立栈结构,主线程使用的是地址空间中的栈空间,叫主线程栈。通过下面的代码看一下:
让两个线程进同一个线程函数,打印同一个临时变量的地址,是不一样的,说明使用的是不同的栈结构。 -
线程的局部存储: 一个全局变量,线程之间是可以共享的,因为它们共享地址空间,这一点,在 线程 - 六、线程退出 中有代码看过,确实对于同一个全局变量,它们的地址都是相同。那么我现在想让每个线程都私有一个全局变量
g_val
,可以做到吗?可以的,我们来看一下:#include <iostream> #include <pthread.h> #include <unistd.h> #include <sys/types.h> using std::cout; using std::endl; __thread int g_val = 100; void *pfunc(void *arg) { cout << (char *)arg << " " << &g_val << endl; return nullptr; } int main() { pthread_t rid; pthread_create(&rid, nullptr, pfunc, (void *)"thread-1"); pthread_create(&rid, nullptr, pfunc, (void *)"thread-2"); sleep(2); cout << "core end" << endl; return 0; }
_thread
是gcc
的一种编译选项,会将它修饰的量,给各个线程都拷贝一份,放到了线程的局部存储空间中。这一点我们可以看一下:
- 所以,如果想记录一些线程独有的数据,但又不想哪个线程中都定义一次局部变量,那就可以用
__thread
来修饰一个全局变量,让所有线程独用。 - 需要强调一点的是:线程的局部存储只能存储内置类型!
③ 整个系统的所有的线程都在这一个库中管理着
- 这里我们可以思考一个问题:既然整个系统的所有的线程都在这一个库中管理着,那么一个线程会不会看到另一个进程的线程块?
- 其实不会出现这种情况,因为每个进程的线程都是自己的,在自己的地址空间的动态库中管理着,只有这个进程有它的线程的虚拟地址,通过该进程自己的页表才能得到物理地址,其他进程看不到其他进程的线程的虚拟地址,更看不见其他进程的页表,也就无法映射看到真是的物理内存中的线程。
3. 执行流怎么找到栈的?
既然主线程和其他线程的栈空间不同,那么执行流怎么找到栈的呢?
其实,底层是调用了 clone()
系统调用。
fn
:函数指针,可以指向线程函数。stack
:指向线程的栈空间的起始地址。
所以,是栈空间的地址是可以传递的,所以是可以找到的。
八、线程的封装
pthread
库对轻量级进程的封装我们并不清楚,所以这里我们所说的封装并不是这个,而是可以将pthread
库提供给我们的接口封装一下,成为类,就像 C++11
提供的线程类一样。这里提供给大家一份,供大家参考。
1. Pthread.hpp
#ifndef __THREAD_HPP
#define __THREAD_HPP
#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
namespace zzz
{
template <typename T>
class Thread
{
template <typename K>
using func_t = std::function<void(K &)>;
public:
Thread(func_t<T> func, T data, const std::string &name = "default_thread")
: _func(func), _data(data), _threadname(name), _stop(1)
{
}
static void *threadfunc(void *arg)
{
Thread<T> *_this = static_cast<Thread<T> *>(arg);
_this->_func(_this->_data);
return nullptr;
}
bool start()
{
_stop = 0;
int n = pthread_create(&_tid, nullptr, threadfunc, this);
if (!n)
{
_stop = false;
return true;
}
else
{
return false;
}
}
void join()
{
if (!_stop)
pthread_join(_tid, nullptr);
}
void detach()
{
if (!_stop)
pthread_detach(_tid);
}
void stop()
{
_stop = 1;
}
const std::string &name() const
{
return _threadname;
}
pthread_t id() const
{
return _tid;
}
private:
pthread_t _tid; // 线程 id
std::string _threadname; // 线程的名字
func_t<T> _func; // 线程函数
T _data; // 线程函数的参数
bool _stop; // 线程开始运行
};
}
#endif
2. test.cc
#include <iostream>
#include "Thread.hpp"
#include <vector>
#include <pthread.h>
using std::cout;
using std::endl;
using namespace zzz;
void testfun(int n)
{
cout << n << endl;
}
int main()
{
std::vector<Thread<int>> pths;
for (int i = 0; i < 5; i++)
{
std::string tname = "thread-" + std::to_string(i + 1);
pths.emplace_back(testfun, 100, tname);
}
for (auto &thread : pths)
{
thread.start();
}
for (auto &thread : pths)
{
thread.join();
}
cout << "main end" << endl;
return 0;
}
运行结果:
本文到这里就结束了,如果对您有帮助,希望得到您的一个赞!🌷
如有错漏,欢迎指正!😄