跳过正文
P16 并发与多线程
  1. S/
  2. C/C++/
  3. C/
  4. C语言程序设计/

P16 并发与多线程

·6950 字·14 分钟· ·
Li
作者
Li
往前走,别回头!
目录
C++ 新经典笔记 - 这篇文章属于一个选集。

知识点
#

17.1 基本概念和实现
#

  • @ 线程基础图 ![[白板/P52 线程基础.canvas|线程基础]]

17.1.1 并发、进程、线程的基本概念和综述
#

[!tip] 并发

并发: ==两个或更多任务(独立的活动)同时发生(进行)==

  • 一个程序同时执行多个独立的任务
  • 单核 CPU 通过任务切换实现并发假象(上下文切换)
  • 多核 CPU 可真正实现并行执行(硬件并发)
  • 使用并发的主要原因:提高整体运行性能

[!tip] 可执行程序

可执行程序: ==磁盘上的一个文件(程序)==

  • Windows 下扩展名为 .exe
  • Linux 下有可执行权限(-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 上编译
  • 以往需用 CreateThreadWindows)或 pthread_createLinux),不可跨平台

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(): ==判断是否可以成功使用 joindetach==

  • 创建线程后、未调用 join/detach 前,返回 true
  • 调用 joindetach 后,返回 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::moveunique_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==

  • 调用 1lock 必须调用 1unlock
  • 多个出口(return)时每个出口都要 unlock
my_mutex.lock();
// 操作共享数据
my_mutex.unlock();

[!tip] std::lock_guard

std::lock_guard: ==类模板,自动管理 mutexlock/unlock==

  • 构造函数中调用 mutexlock
  • 析构函数中调用 mutexunlock
  • 超出作用域自动解锁,防止忘记 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_locklock_guard 灵活的地方

try_lock(): ==尝试加锁,成功返回 true,失败返回 false,不阻塞==

release(): ==返回所管理的 mutex 对象指针,并释放所有权==

  • 不同于 unlock(只解锁不解除关联)
  • 调用 release 后,程序员有责任负责解锁
  • ! 锁的粒度:锁住的代码越少(粒度越细),效率越高;锁住的代码越多(粒度越粗),效率越低
  • ! 选择合适的粒度是高级程序员能力的体现

17.6.4 unique_lock 所有权的传递
#

[!tip] unique_lock 所有权

所有权: ==unique_lockmutex 的绑定关系,可移动不可复制==

  • 类似 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: ==共享式 futureget() 返回数据的复制而非移动==

  • futureget() 是移动语义,只能调用一次
  • shared_futureget() 是复制语义,可调用多次
  • 适用于多个线程都需要获取结果的场景
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 临界区:同一线程可多次进入,但 EnterLeave 次数必须相同
  • ! 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():等待一段时间获取锁,超时返回 false
  • try_lock_until():等待到某个时间点获取锁,超时返回 false

std::recursive_timed_mutex: ==带超时功能的递归独占互斥量==

  • 结合 recursive_mutextimed_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 多线程总结
#

[!tip] C++11 多线程总结

  • C++11 引入多线程开发接口,实现跨平台统一开发
  • 降低学习成本,提高程序可移植性
  • 可与具体操作系统平台线程开发接口结合使用,优势互补
Reply by Email
C++ 新经典笔记 - 这篇文章属于一个选集。

相关文章