知识点#
17.1 基本概念和实现#
- @ 线程基础图 ![[白板/P52 线程基础.canvas|线程基础]]
17.1.1 并发、进程、线程的基本概念和综述#
[!tip] 并发
并发: ==两个或更多任务(独立的活动)同时发生(进行)==
- 一个程序同时执行多个独立的任务
- 单核
CPU通过任务切换实现并发假象(上下文切换)- 多核
CPU可真正实现并行执行(硬件并发)- 使用并发的主要原因:提高整体运行性能
[!tip] 可执行程序
可执行程序: ==磁盘上的一个文件(程序)==
Windows下扩展名为.exeLinux下有可执行权限(-rwxrw-r--中的x)
[!tip] 进程
进程: ==运行起来了的可执行程序==
- 运行一个可执行程序就创建了一个进程
- 多次运行可创建多个进程
[!tip] 线程
线程: ==一条代码的执行通路(道路)==
- 每个进程都有一个唯一的主线程,随进程自动启动
- 主线程从
main函数开始执行,到return 0;结束- 可通过代码创建其他线程(子线程),代表新的执行通路
- 线程不是越多越好:每个线程需独立堆栈空间(约
1MB),上下文切换也有开销- 创建线程数量一般不建议超过
200-300个
- ! 主线程执行完毕 = 整个进程结束,子线程会被操作系统强制终止
- ! 想保持子线程运行,必须让主线程保持运行
17.1.2 并发的实现方法#
[!tip] 并发实现方式
多进程并发: ==通过多个进程实现并发,每个进程做一件事==
- 进程间通信:管道、文件、消息队列、共享内存(同机);
socket(不同机)- 进程间通信较复杂(数据保护问题)
多线程并发: ==在单个进程中创建多个线程实现并发==
- 线程共享地址空间(共享内存)、全局变量、指针、引用
- 开销远小于多进程
- 带来数据一致性问题
- % 多线程并发优点:启动速度更快、更轻量级、系统资源开销更少、执行速度更快
- % 多线程并发缺点:使用难度大,需小心处理数据一致性问题
17.1.3 C++11 新标准线程库#
[!tip] C++11 线程库
C++11 线程库: ==C++语言本身增加的多线程支持,与具体操作系统平台无关==
- 极大增加程序可移植性(跨平台)
Windows下开发的代码可直接在Linux上编译- 以往需用
CreateThread(Windows)或pthread_create(Linux),不可跨平台
17.2 线程启动、结束与创建线程写法#
- @ 线程创建图 ![[白板/P53 线程创建.canvas|线程创建]]
17.2.1 范例演示线程运行的开始和结束#
[!tip] std::thread
std::thread: ==C++标准库中用来创建线程的类==
- 构造函数接受一个可调用对象作为参数
- 代码一执行,新线程创建并立即开始执行初始函数
#include <thread>
void myprint() {
cout << "我的线程开始执行了" << endl;
cout << "我的线程执行完毕了" << endl;
}
int main() {
std::thread mytobj(myprint); // 创建线程
mytobj.join(); // 等待线程执行完毕
cout << "main主函数执行结束!" << endl;
return 0;
}
[!tip] join
join(): ==阻塞主线程,等待子线程执行完毕==
- 主线程阻塞在
join行,直到子线程执行完毕- 书写良好的程序应确保主线程等待所有子线程执行完毕后再退出
[!tip] detach
detach(): ==分离主线程与子线程,主线程不必等待子线程==
- 主线程与子线程失去关联,子线程驻留后台运行
- 子线程被
C++运行时库接管,执行完后由运行时库清理资源- 分离后的线程称为守护线程
- 一旦调用
detach,不可再调用join,否则程序异常
- !
detach会导致程序员失去对线程的控制,多数实际项目中join更常用
[!tip] joinable
joinable(): ==判断是否可以成功使用join或detach==
- 创建线程后、未调用
join/detach前,返回true- 调用
join或detach后,返回false
if (mytobj.joinable()) {
mytobj.join(); // 安全调用
}
17.2.2 其他创建线程的写法#
[!tip] 用类创建线程
类对象创建线程: ==通过重载
operator()的类对象作为可调用对象==
class TA {
public:
void operator()() {
cout << "TA::operator()开始执行了" << endl;
cout << "TA::operator()执行结束了" << endl;
}
};
TA ta;
std::thread mytobj3(ta); // ta 不能是临时对象
mytobj3.join();
- ! 类与
detach结合使用时,类中不应包含引用或指针成员变量(主线程退出后引用/指针失效) - ! 类对象会被复制到子线程中(拷贝构造函数执行),但引用/指针成员仍指向主线程内存
[!tip] 用 lambda 表达式创建线程
auto mylambdathread = [] {
cout << "我的线程开始执行了" << endl;
cout << "我的线程执行完毕了" << endl;
};
std::thread mytobj4(mylambdathread);
mytobj4.join();
17.3 线程传参详解、detach 坑与成员函数作为线程函数#
17.3.1 传递临时对象作为线程参数#
[!tip] 线程传参要点
线程传参: ==传递简单类型参数建议值传递,传递类对象建议构建临时对象==
- ! 陷阱 1:
detach时不要传递引用、指针(主线程退出后内存不安全) - ! 陷阱 2:隐式类型转换可能在子线程中进行(主线程退出后才构造对象,不安全)
[!tip] 临时对象解决方案
安全传参: ==在创建线程时构造临时对象,确保形参在主线程退出前构造完毕==
- 临时对象在主线程中构造(通过线程
id可验证)- 线程入口函数的类类型形参应使用引用(避免多构造一次临时对象)
// 安全写法:显式构造临时对象
std::thread mytobj(myprint, mvar, string(mybuf));
// 不安全写法:依赖隐式转换
std::thread mytobj(myprint, mvar, mybuf); // mybuf 可能在线程中才转换
- ! 总结:传递
int等简单类型用值传递;传递类对象避免隐式转换,在创建线程行构建临时对象;建议不使用detach,只使用join
17.3.2 临时对象作为线程参数继续讲#
[!tip] 线程 id
线程
id: ==每个线程对应的唯一数字标识==
- 通过
std::this_thread::get_id()获取- 不同线程的
id必然不同
[!tip] 临时对象构造时机
关键结论: ==给线程入口函数传递类类型对象时,使用临时对象可确保形参在主线程退出前创建完毕==
- 不构造临时对象:对象可能在子线程中构造(不安全)
- 构造临时对象:对象在主线程中构造(安全)
- 形参使用引用可避免多一次拷贝构造
17.3.3 传递类对象与智能指针作为线程参数#
[!tip] std::ref
std::ref: ==明确告诉编译器传递一个能影响原始参数的引用==
- 使用
std::ref后不涉及产生临时对象- 线程入口函数形参可去掉
const修饰- 修改形参会影响到实参
A myobj(10);
std::thread mytobj(myprint2, std::ref(myobj)); // 真正传递引用
[!tip] 智能指针传参
unique_ptr传参: ==使用std::move将unique_ptr转移到线程==
unique_ptr<int> myp(new int(100));
std::thread mytobj(myprint3, std::move(myp)); // 所有权转移
mytobj.join(); // 必须用 join,不能用 detach
17.3.4 用成员函数作为线程入口函数#
class A {
public:
void thread_work(int num) {
cout << "子线程thread_work执行" << endl;
}
};
A myobj(10);
// 成员函数指针 + 对象 + 参数
std::thread mytobj(&A::thread_work, myobj, 15);
mytobj.join();
// 使用 std::ref 避免拷贝
std::thread mytobj(&A::thread_work, std::ref(myobj), 15);
// 或使用对象地址
std::thread mytobj(&A::thread_work, &myobj, 15);
17.4 创建多个线程、数据共享问题分析与案例代码#
17.4.1 创建和等待多个线程#
vector<std::thread> mythreads;
for (int i = 0; i < 5; i++) {
mythreads.push_back(std::thread(myprint, i)); // 创建并开始执行
}
for (auto iter = mythreads.begin(); iter != mythreads.end(); ++iter) {
iter->join(); // 等待所有线程返回
}
- ! 多个线程之间的执行顺序是乱的(与操作系统调度机制有关)
- $ 将
thread对象放到容器中管理,方便一次性创建和管理大量线程
17.4.2 数据共享问题分析#
- % 只读数据:多个线程同时读,安全
- % 有读有写:必须小心处理,否则数据不一致
- ! 写操作是多步骤的,必须保证原子性(中间不能被截断)
17.4.3 共享数据的保护实战范例#
[!tip] 共享数据保护
核心思路: ==当某线程操作共享数据时,用锁锁住,其他线程必须等待==
- 互斥量(
mutex)是最通用的保护共享数据的机制
17.5 互斥量的概念、用法、死锁演示与解决详解#
- @ 互斥量与死锁图 ![[白板/P54 互斥量与死锁.canvas|互斥量与死锁]]
17.5.1 互斥量的基本概念#
[!tip] 互斥量
互斥量(
mutex): ==一把锁,同一时间只有一个线程可以加锁成功==
- 多个线程调用
lock尝试加锁,只有一个成功- 未加锁成功的线程卡在
lock行不断尝试- 加锁成功线程执行完共享数据操作后必须
unlock- 保护范围不要多也不要少
17.5.2 互斥量的用法#
[!tip] lock 与 unlock
lock()/unlock(): ==成对使用,有lock必然要有unlock==
- 调用
1次lock必须调用1次unlock- 多个出口(
return)时每个出口都要unlock
my_mutex.lock();
// 操作共享数据
my_mutex.unlock();
[!tip] std::lock_guard
std::lock_guard: ==类模板,自动管理mutex的lock/unlock==
- 构造函数中调用
mutex的lock- 析构函数中调用
mutex的unlock- 超出作用域自动解锁,防止忘记
unlock- 不如单独使用
mutex灵活(无法随时unlock)
{
std::lock_guard<std::mutex> sbguard(my_mutex);
// 操作共享数据
} // 离开作用域,自动 unlock
17.5.3 死锁#
[!tip] 死锁
死锁: ==至少两个互斥量才能产生,线程互相等待对方解锁==
- 线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1
- 两个线程都卡住,程序无法继续执行
[!tip] 死锁的一般解决方案
解决死锁: ==确保多个互斥量上锁的先后顺序相同==
- 两个线程对锁的
lock顺序必须一致unlock顺序建议谁后lock谁先unlock
[!tip] std::lock 函数模板
std::lock: ==一次锁住两个或两个以上的互斥量,不存在死锁风险==
- 要么全部锁住,要么全部没锁住
- 锁住一个后若第二个失败,会释放第一个重新尝试
std::lock(my_mutex1, my_mutex2); // 同时锁住两个
// 操作共享数据
my_mutex2.unlock();
my_mutex1.unlock();
[!tip] std::adopt_lock
std::adopt_lock: ==标记互斥量已经被lock过了,lock_guard不需要再lock==
- 配合
std::lock使用,lock_guard只负责unlock
std::lock(my_mutex1, my_mutex2);
std::lock_guard<std::mutex> sbguard1(my_mutex1, std::adopt_lock);
std::lock_guard<std::mutex> sbguard2(my_mutex2, std::adopt_lock);
// 操作共享数据,离开作用域自动 unlock
17.6 unique_lock 详解#
17.6.1 unique_lock 取代 lock_guard#
[!tip] unique_lock
std::unique_lock: ==比lock_guard更灵活的互斥量管理类模板==
- 可完全取代
lock_guard- 更灵活但效率略差、内存占用稍多
- 构造函数中
lock,析构函数中unlock
std::unique_lock<std::mutex> sbguard1(my_mutex); // 替代 lock_guard
17.6.2 unique_lock 的第二个参数#
[!tip] std::adopt_lock
std::adopt_lock: ==表示互斥量已经被lock过了,不需要unique_lock构造函数再lock==
- 前提:开发者需要先把互斥量
lock上
my_mutex.lock();
std::unique_lock<std::mutex> sbguard1(my_mutex, std::adopt_lock);
[!tip] std::try_to_lock
std::try_to_lock: ==尝试lock,失败也立即返回,不会阻塞==
- 前提:不能自己先
lock互斥量- 通过
owns_lock()判断是否拿到锁
std::unique_lock<std::mutex> sbguard1(my_mutex, std::try_to_lock);
if (sbguard1.owns_lock()) {
// 拿到锁,操作共享数据
} else {
// 没拿到锁,做别的事
}
[!tip] std::defer_lock
std::defer_lock: ==初始化unique_lock但不加锁,后续手动lock==
- 前提:不能自己先
lock互斥量- 提供灵活性,可调用
unique_lock的成员函数
std::unique_lock<std::mutex> sbguard1(my_mutex, std::defer_lock);
sbguard1.lock(); // 手动加锁
17.6.3 unique_lock 的成员函数#
[!tip] unique_lock 成员函数
lock(): ==给互斥量加锁,无法加锁则阻塞等待==
unlock(): ==给互斥量解锁,可随时调用==
- 体现了
unique_lock比lock_guard灵活的地方
try_lock(): ==尝试加锁,成功返回true,失败返回false,不阻塞==
release(): ==返回所管理的mutex对象指针,并释放所有权==
- 不同于
unlock(只解锁不解除关联)- 调用
release后,程序员有责任负责解锁
- ! 锁的粒度:锁住的代码越少(粒度越细),效率越高;锁住的代码越多(粒度越粗),效率越低
- ! 选择合适的粒度是高级程序员能力的体现
17.6.4 unique_lock 所有权的传递#
[!tip] unique_lock 所有权
所有权: ==
unique_lock与mutex的绑定关系,可移动不可复制==
- 类似
unique_ptr的所有权传递- 通过
std::move转移所有权
std::unique_lock<std::mutex> sbguard1(my_mutex);
std::unique_lock<std::mutex> sbguard10(std::move(sbguard1)); // 移动所有权
- $ 返回
unique_lock类型也是一种用法(返回局部对象会触发移动构造)
17.7 单例设计模式共享数据分析、解决与 call_once#
17.7.1 设计模式简单谈#
[!tip] 设计模式
设计模式: ==开发程序的一些代码写法,使程序更灵活==
- 优点:增加或减少功能不会牵一发动全身
- 缺点:代码晦涩,管理复杂
- 应活学活用,不要生搬硬套
17.7.2 单例设计模式#
[!tip] 单例模式
单例模式: ==某个特殊的类只能创建一个对象==
- 构造函数用
private修饰- 通过静态成员函数
GetInstance()获取唯一对象- 使用内部类
CGarhuishou自动释放单例对象内存
class MyCAS {
private:
MyCAS() {} // 私有构造函数
static MyCAS* m_instance;
public:
static MyCAS* GetInstance() {
if (m_instance == NULL) {
m_instance = new MyCAS();
static CGarhuishou cl; // 程序退出时自动释放
}
return m_instance;
}
class CGarhuishou { // 内部类,用于释放对象
public:
~CGarhuishou() {
if (MyCAS::m_instance) {
delete MyCAS::m_instance;
MyCAS::m_instance = NULL;
}
}
};
};
MyCAS* MyCAS::m_instance = NULL; // 静态成员初始化
17.7.3 单例设计模式共享数据问题分析、解决#
[!tip] 双重检查锁定
双重检查锁定: ==外层
if提高效率,内层if加锁后确保安全==
static MyCAS* GetInstance() {
if (m_instance == NULL) { // 第一次检查(提高效率)
std::unique_lock<std::mutex> mymutex(resource_mutex);
if (m_instance == NULL) { // 第二次检查(确保安全)
m_instance = new MyCAS();
static CGarhuishou cl;
}
}
return m_instance;
}
- ! 外层
if成立不代表m_instance一定没被new过(可能另一个线程正在new)
17.7.4 std::call_once#
[!tip] std::call_once
std::call_once: ==保证函数只被调用一次(即使多线程环境下)==
- 配合
std::once_flag标记使用- 效率比互斥量消耗资源更少
- 可替代双重检查锁定解决单例初始化问题
std::once_flag g_flag;
static void CreateInstance() {
m_instance = new MyCAS();
static CGarhuishou cl;
}
static MyCAS* GetInstance() {
if (m_instance == NULL) { // 提高效率
std::call_once(g_flag, CreateInstance);
}
return m_instance;
}
- & 强烈建议在主线程中、子线程创建之前完成单例类对象的初始化
17.8 condition_variable、wait、notify_one 与 notify_all#
- @ 条件变量图 ![[白板/P55 条件变量.canvas|条件变量]]
17.8.1 条件变量 std::condition_variable、wait 与 notify_one#
[!tip] 条件变量
std::condition_variable: ==用于等待一个条件达成,需与互斥量配合工作==
- 线程 A 等待条件满足(如消息队列有数据)
- 线程 B 使条件满足后通知线程 A
[!tip] wait
wait(): ==阻塞等待条件满足==
- 有第二个参数(
lambda表达式):返回true则继续,返回false则解锁互斥量并阻塞- 无第二个参数:等同于
lambda返回false- 被唤醒后重新获取互斥量并加锁,再判断
lambda表达式
[!tip] notify_one
notify_one(): ==唤醒一个等待在wait的线程==
- 唤醒哪个线程不确定
- 必须把互斥量解锁,另一个线程的
wait才能继续正常工作
// 等待线程
std::unique_lock<std::mutex> sbguardl(my_mutex);
my_cond.wait(sbguardl, [this] {
if (!msgRecvQueue.empty()) return true;
return false;
});
// 流程走下来意味着队列必然有数据
// 通知线程
std::unique_lock<std::mutex> sbguardl(my_mutex);
msgRecvQueue.push_back(i);
my_cond.notify_one(); // 唤醒等待线程
17.8.2 上述代码深入思考#
- !
wait被唤醒后,队列中可能有多条数据 - !
notify_one唤醒时,如果线程不在wait行,则notify_one无效 - ! 必须透彻理解多线程函数的工作流程后再使用
17.8.3 notify_all#
[!tip] notify_all
notify_all(): ==通知所有处于wait状态的线程==
- 适用于多个不同线程做不同事情但等待同一条件变量的场景
- 被唤醒的线程仍需竞争获取锁
17.9 async、future、packaged_task 与 promise#
17.9.1 std::async 和 std::future 创建后台任务并返回值#
[!tip] std::async
std::async: ==启动一个异步任务,返回std::future对象==
- 可能创建新线程,也可能不创建(取决于参数)
future对象中包含线程入口函数的返回结果- 通过
get()获取结果(阻塞等待)
std::future<int> result = std::async(mythread);
cout << result.get() << endl; // 阻塞等待,只能 get 一次
[!tip] std::async 额外参数
std::launch::deferred: ==延迟执行,调用wait/get时才执行(不创建新线程)==
std::launch::async: ==强制创建新线程立即执行==
std::launch::async \| std::launch::deferred: ==系统自行决定是否创建新线程==(默认行为)
[!tip] std::async 与 std::thread 的区别
特性 std::threadstd::async创建线程 直接创建 可能创建也可能不创建 返回值 不易获取 通过 future获取资源紧张 创建失败报异常 不报异常,改为同步执行
17.9.2 std::packaged_task#
[!tip] std::packaged_task
std::packaged_task: ==把可调用对象包装起来,方便作为线程入口函数调用==
- 模板参数是可调用对象的类型签名
- 通过
get_future()获取future对象- 本身也是可调用对象,可直接调用
std::packaged_task<int(int)> mypt(mythread);
std::thread t1(std::ref(mypt), 1);
t1.join();
std::future<int> result = mypt.get_future();
cout << result.get() << endl;
17.9.3 std::promise#
[!tip] std::promise
std::promise: ==在某个线程中为其赋值,在其他线程中取出使用==
- 通过
set_value()保存值- 通过
get_future()绑定future获取值- 实现线程间数据传递
void mythread(std::promise<int>& tapp, int calc) {
calc++;
calc *= 10;
tapp.set_value(calc); // 保存结果
}
std::promise<int> myprom;
std::thread t1(mythread, std::ref(myprom), 180);
t1.join();
std::future<int> ful = myprom.get_future();
auto result = ful.get(); // 获取结果
17.10 future 其他成员函数、shared_future 与 atomic#
17.10.1 std::future 的其他成员函数#
[!tip] wait_for
wait_for(): ==等待一段时间,返回std::future_status枚举==
std::future_status::timeout:超时,线程还没执行完std::future_status::ready:线程成功返回std::future_status::deferred:线程被延迟执行
std::future_status status = result.wait_for(std::chrono::seconds(1));
if (status == std::future_status::timeout) { /* 超时 */ }
else if (status == std::future_status::ready) { /* 完成 */ }
else if (status == std::future_status::deferred) { /* 延迟 */ }
17.10.2 续谈 std::async 的不确定性问题#
[!tip] 判断异步任务是否被推迟
方法: ==使用
wait_for(std::chrono::seconds(0))判断==
- 返回
deferred:任务被推迟- 返回
ready:任务已完成- 返回
timeout:任务正在执行中
17.10.3 std::shared_future#
[!tip] std::shared_future
std::shared_future: ==共享式future,get()返回数据的复制而非移动==
future的get()是移动语义,只能调用一次shared_future的get()是复制语义,可调用多次- 适用于多个线程都需要获取结果的场景
std::shared_future<int> result_s(std::move(result)); // 从 future 转移
// 或 result_s = result.share();
auto val1 = result_s.get(); // 可多次 get
auto val2 = result_s.get();
17.10.4 原子操作 std::atomic#
[!tip] 原子操作
std::atomic: ==不可分割的操作,不会被打断,无需互斥量(无锁)==
- 针对一个变量而非代码段
- 效率比互斥量更高
++、--、+=、-=、&=、\|=、^=等简单运算是原子的atm = atm + 1;不是原子操作
std::atomic<int> g_mycout(0);
void mythread() {
for (int i = 0; i < 10000000; i++) {
g_mycout++; // 原子操作,不会被打断
}
}
[!tip] atomic 的限制
- 不允许拷贝构造(
auto atm2 = atm;编译报错)- 不允许复制赋值(
atm3 = atm;编译报错)load():以原子方式读atomic对象的值store():以原子方式写入内容
17.11 Windows 临界区与其他各种 mutex 互斥量#
17.11.1 Windows 临界区#
[!tip] Windows 临界区
临界区: ==
Windows平台下与mutex功能类似的同步机制==
EnterCriticalSection:进入临界区(加锁)LeaveCriticalSection:离开临界区(解锁)InitializeCriticalSection:初始化DeleteCriticalSection:释放
17.11.2 多次进入临界区试验#
- !
Windows临界区:同一线程可多次进入,但Enter和Leave次数必须相同 - !
C++11 mutex:同一线程连续两次lock会报异常
17.11.3 自动析构技术#
[!tip] RAII 技术
RAII(Resource Acquisition Is Initialization): ==在构造函数中初始化资源,析构函数中释放资源==
- 智能指针、容器等都用到了这种技术
- 可自定义类实现类似
lock_guard的功能
17.11.4 recursive_mutex 递归的独占互斥量#
[!tip] std::recursive_mutex
std::recursive_mutex: ==允许同一个线程多次lock同一个互斥量==
- 解决同一线程中函数嵌套调用导致多次
lock的问题- 比普通
mutex消耗更多、效率差一些lock多少次就要unlock多少次
17.11.5 带超时的互斥量#
[!tip] std::timed_mutex
std::timed_mutex: ==带超时功能的独占互斥量==
try_lock_for():等待一段时间获取锁,超时返回falsetry_lock_until():等待到某个时间点获取锁,超时返回false
std::recursive_timed_mutex: ==带超时功能的递归独占互斥量==
- 结合
recursive_mutex和timed_mutex的特性
std::timed_mutex my_mutex;
std::chrono::milliseconds timeout(100);
if (my_mutex.try_lock_for(timeout)) {
// 拿到锁
my_mutex.unlock();
} else {
// 超时未拿到锁
}
17.12 补充知识、线程池浅谈、数量谈与总结#
17.12.1 知识点补充#
[!tip] 虚假唤醒
虚假唤醒: ==
wait被唤醒但条件不满足(如消息队列无数据)==
- 产生原因:
push_back一条数据后调用多次notify_one,或notify_all唤醒多个线程- 解决方案:
wait的第二个参数(lambda表达式)中用if判断条件是否真正满足
[!tip] atomic 进一步理解
cout << atm << endl;读atm是原子操作,但整行语句不是atm = atm + 1;不是原子操作(atm.load()+atm.store())atm += 1;是原子操作
17.12.2 浅谈线程池#
[!tip] 线程池
线程池: ==预先创建一定数量的线程,统一管理调度,循环再利用==
- 程序启动时一次性创建好线程
- 任务到来时从池中取出空闲线程处理
- 处理完后线程不销毁,继续等待下次任务
- 优点:避免动态创建线程的资源消耗,提高效率,程序更健壮稳定
17.12.3 线程创建数量谈#
- ! 线程创建数量极限约
2000个 - ! 建议一个进程中的线程数量尽量不超过
500个,200个以内为佳 - & 采用
IOCP等技术时,建议线程数等于CPU数量或CPU数量 × 2 - & 根据业务需要确定线程数,实践是检验真理的最好标准
17.12.4 C++11 多线程总结#
Reply by Email[!tip] C++11 多线程总结
C++11引入多线程开发接口,实现跨平台统一开发- 降低学习成本,提高程序可移植性
- 可与具体操作系统平台线程开发接口结合使用,优势互补

