├── homework ├── thread_pool │ └── .gitkeep └── README.md ├── md ├── 详细分析 │ ├── README.md │ ├── 02scoped_lock源码解析.md │ ├── 01thread的构造与源码解析.md │ ├── 04线程池.md │ └── 03async与future源码解析.md ├── 06协程.md ├── README.md ├── 01基本概念.md └── 05内存模型与原子操作.md ├── image ├── 赞助.jpg ├── 猫猫虫旋转.jpg ├── 捐赠 │ ├── 支付宝10.jpg │ ├── 支付宝20.jpg │ ├── 支付宝88.88.jpg │ └── README.md ├── 第四章 │ └── 进度条.png └── 现代C++并发编程教程.png ├── code ├── ModernCpp-ConcurrentProgramming-Tutorial │ ├── test │ │ ├── test_mutex.cpp │ │ └── AduioPlayer.h │ ├── test2.cpp │ ├── 27创建异步任务获取返回值.cpp │ ├── 01-HelloWorld.cpp │ ├── 07-thread对象转移所有权.cpp │ ├── 22线程存储期.cpp │ ├── 08-thread构造源码解析.cpp │ ├── 11-数据竞争.cpp │ ├── 30future的状态变化.cpp │ ├── 17-在不同作用域传递互斥量.cpp │ ├── 33限时等待-时间段.cpp │ ├── 21new和delete是线程安全的吗?.cpp │ ├── 05-传递参数.cpp │ ├── 20recursive_mutex.cpp │ ├── 32限时等待-时钟.cpp │ ├── 36C++20闩latch.cpp │ ├── 40线程池使用.cpp │ ├── 10-C++20jthread.cpp │ ├── 44atomic指针特化.cpp │ ├── 23局部、全局、线程、CPU变量的对比与使用.cpp │ ├── 03-thread_management.cpp │ ├── 15-死锁:问题与解决.cpp │ ├── 43atomic_bool.cpp │ ├── test.cpp │ ├── 34限时等待-时间点.cpp │ ├── 14-保护共享数据.cpp │ ├── 16-unique_lock.cpp │ ├── 42atomic_flag实现自旋锁.cpp │ ├── 13-try_lock.cpp │ ├── 24等待事件或条件.cpp │ ├── 38第四章总结-勘误初始化顺序.cpp │ ├── 06-this_thread命名空间.cpp │ ├── 37C++20屏障barrier.cpp │ ├── 04-RAII.cpp │ ├── 35C++20信号量.cpp │ ├── 18-保护共享数据的初始化过程.cpp │ ├── 12-使用互斥量.cpp │ ├── 19保护不常更新的数据结构.cpp │ ├── 31多个线程的等待shared_future.cpp │ ├── Log.h │ ├── 28future与 packaged_task.cpp │ ├── 39原子类型atomic.cpp │ ├── 29使用promise.cpp │ ├── 09-实现joining_thread.cpp │ ├── 02-hardware_concurrency.cpp │ ├── CMakeLists.txt │ ├── 25线程安全的队列.cpp │ ├── 45原子特化shared_ptr.cpp │ ├── CMakeSettings.json │ ├── 26使用条件变量实现后台提示音播放.cpp │ └── 41实现一个线程池.cpp ├── 04同步操作 │ └── async_progress_bar │ │ ├── xmake.lua │ │ ├── main.cpp │ │ ├── CMakeLists.txt │ │ ├── async_progress_bar.ui │ │ ├── async_progress_bar.cpp │ │ └── async_progress_bar.h └── 03共享数据 │ └── 保护不常更新的数据结构.cpp ├── .vuepress ├── public │ └── image │ │ ├── 赞助.jpg │ │ ├── 进度条.png │ │ ├── 猫猫虫旋转.jpg │ │ └── 现代C++并发编程教程.png ├── params.js └── config.js ├── .gitignore ├── package.json ├── .github └── workflows │ └── deploy.yml ├── README.md └── LICENSE /homework/thread_pool/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /md/详细分析/README.md: -------------------------------------------------------------------------------- 1 | # 详细分析 2 | 3 | 存放一些源码分析,以及一些其它非知识点,而是应用的造轮子(如线程池),或者其它扩展讲解。 4 | 5 | -------------------------------------------------------------------------------- /image/赞助.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/HEAD/image/赞助.jpg -------------------------------------------------------------------------------- /image/猫猫虫旋转.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/HEAD/image/猫猫虫旋转.jpg -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/test/test_mutex.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | std::mutex some_mutex; 4 | -------------------------------------------------------------------------------- /image/捐赠/支付宝10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/HEAD/image/捐赠/支付宝10.jpg -------------------------------------------------------------------------------- /image/捐赠/支付宝20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/HEAD/image/捐赠/支付宝20.jpg -------------------------------------------------------------------------------- /image/第四章/进度条.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/HEAD/image/第四章/进度条.png -------------------------------------------------------------------------------- /image/捐赠/支付宝88.88.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/HEAD/image/捐赠/支付宝88.88.jpg -------------------------------------------------------------------------------- /image/现代C++并发编程教程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/HEAD/image/现代C++并发编程教程.png -------------------------------------------------------------------------------- /.vuepress/public/image/赞助.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/HEAD/.vuepress/public/image/赞助.jpg -------------------------------------------------------------------------------- /.vuepress/public/image/进度条.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/HEAD/.vuepress/public/image/进度条.png -------------------------------------------------------------------------------- /.vuepress/public/image/猫猫虫旋转.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/HEAD/.vuepress/public/image/猫猫虫旋转.jpg -------------------------------------------------------------------------------- /.vuepress/public/image/现代C++并发编程教程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/HEAD/.vuepress/public/image/现代C++并发编程教程.png -------------------------------------------------------------------------------- /homework/README.md: -------------------------------------------------------------------------------- 1 | # 此处存放作业 2 | 3 | - **文件名**:`id` + 日期数字。 4 | 5 | `id` :随意。 6 | 7 | 日期数字:`8` 位数字,例如现在是 2024 年 7 月 31 日,那么日期数字则为:`20240731` 8 | 9 | - **后缀名**:根据作业情况,随意。如果是普通代码,自然是 `.cpp` 后缀。 10 | 11 | --- 12 | 13 | 1. 线程池作业存放在 [thread_pool](./thread_pool) 文件夹中。 14 | -------------------------------------------------------------------------------- /code/04同步操作/async_progress_bar/xmake.lua: -------------------------------------------------------------------------------- 1 | add_rules("mode.debug", "mode.release") 2 | 3 | set_runtimes("MD") 4 | 5 | target("async_progress_bar") 6 | add_rules("qt.widgetapp","qt.quickapp") 7 | set_languages("c++17") 8 | add_files("./*.cpp", "./*.ui", "./*.h") 9 | add_headerfiles("./*.h") 10 | target_end() -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/test2.cpp: -------------------------------------------------------------------------------- 1 | #include "Log.h" 2 | #include 3 | #include 4 | using namespace std::chrono_literals; 5 | 6 | int main() { 7 | LOG_WARN("😅"); 8 | std::jthread t{[]{ 9 | std::this_thread::sleep_for(100ms); 10 | LOG_ERROR("🤣"); 11 | }}; 12 | LOG_INFO("👉"); 13 | } -------------------------------------------------------------------------------- /.vuepress/params.js: -------------------------------------------------------------------------------- 1 | // 不同仓库需要修改的值 2 | const logoPath = '/image/现代C++并发编程教程.png'; 3 | const repoName = '现代C++并发编程教程'; 4 | const repoBase = '/ModernCpp-ConcurrentProgramming-Tutorial'; 5 | const repoUrl = `https://github.com/Mq-b${repoBase}/`; 6 | const sponsor = '/image/赞助.jpg'; 7 | const cat = '/image/猫猫虫旋转.jpg' 8 | 9 | export { repoUrl, repoBase, repoName, logoPath, sponsor, cat } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/27创建异步任务获取返回值.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include // 引入 future 头文件 4 | 5 | void f() { 6 | std::cout << std::this_thread::get_id() << '\n'; 7 | } 8 | 9 | int main() { 10 | auto t = std::async([] {}); 11 | std::future future{ std::move(t) }; 12 | future.wait(); // Error! 抛出异常 13 | } -------------------------------------------------------------------------------- /code/04同步操作/async_progress_bar/main.cpp: -------------------------------------------------------------------------------- 1 | #include "async_progress_bar.h" 2 | #include 3 | 4 | int main(int argc, char *argv[]){ 5 | QApplication a(argc, argv); 6 | 7 | auto s = std::to_string(_Thrd_id()); 8 | QMessageBox::information(nullptr, "主线程ID", s.c_str()); 9 | 10 | async_progress_bar w; 11 | w.show(); 12 | return a.exec(); 13 | } 14 | -------------------------------------------------------------------------------- /image/捐赠/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | cpp 4 | 5 | cpp 6 | 7 | cpp 8 | 9 |
10 | 11 | --- 12 | 13 |   我们会收集捐赠者进行感谢,所以请您捐赠了可以选择备注,或者联系我,或者直接在[**捐赠初始记录名单**](https://github.com/Mq-b/Modern-Cpp-templates-tutorial/discussions/5)中进行评论。 14 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/01-HelloWorld.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | void hello(){ 6 | std::cout << "Hello World" << std::endl; 7 | // std::this_thread::sleep_for(std::chrono::seconds(5)); 8 | } 9 | 10 | int main(){ 11 | std::thread t; // 默认构造?构造不关联线程的 thread 对象 12 | std::cout < 2 | #include 3 | 4 | // https://github.com/Mq-b/Loser-HomeWork/discussions/206 5 | 6 | // 反直觉 7 | // 形参、实参 8 | // 函数调用传参,实际上是初始化了(构造)形参的对象 9 | 10 | void f(std::thread t) { 11 | t.join(); 12 | } 13 | 14 | int main() { 15 | std::thread t{ [] {} }; 16 | f(std::move(t)); 17 | f(std::thread{ [] {} }); 18 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/22线程存储期.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int global_counter = 0; 5 | __declspec(thread) int thread_local_counter = 0; 6 | 7 | void print_counters() { 8 | std::cout << "global:" << global_counter++ << '\n'; 9 | std::cout << "thread_local:" << thread_local_counter++ << '\n'; 10 | } 11 | 12 | int main() { 13 | std::thread{ print_counters }.join(); 14 | std::thread{ print_counters }.join(); 15 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/08-thread构造源码解析.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | struct X { 6 | X(X&& x)noexcept {} 7 | template , X>, int> = 0> 8 | X(Fn&& f, Args&&...args) {} 9 | X(const X&) = delete; 10 | }; 11 | 12 | int main(){ 13 | std::thread 14 | X x{ [] {} }; 15 | X x2{ x }; // 选择到了有参构造函数,不导致编译错误 16 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/11-数据竞争.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | std::vector v; 6 | 7 | int n = 1; 8 | 9 | int main() { 10 | int cnt = 0; 11 | auto f = [&] { cnt++; }; 12 | std::thread t1{ f }, t2{ f }, t3{ f }; // ub 未定义行为 13 | t1.join(); 14 | t2.join(); 15 | t3.join(); 16 | std::cout << cnt << '\n'; 17 | } 18 | // 数据竞争它是未定义行为,但是 C++ 的编译器,它会假设你的程序(假设程序是对的,代码是对的)没有任何的未定义行为再去进行优化 19 | // 输出 n,优化,直接缓存这个值 -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/30future的状态变化.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main(){ 6 | std::futurefuture = std::async([] {}); 7 | std::cout << std::boolalpha << future.valid() << '\n'; // true 8 | future.get(); 9 | std::cout << std::boolalpha << future.valid() << '\n'; // false 10 | try { 11 | future.get(); // 抛出 future_errc::no_state 异常 12 | } 13 | catch (std::exception& e) { 14 | std::cerr << e.what() << '\n'; 15 | } 16 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/17-在不同作用域传递互斥量.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | std::unique_lock get_lock() { 8 | extern std::mutex some_mutex; 9 | std::unique_lock lk{ some_mutex }; 10 | return lk; // 选择到 unique_lock 的移动构造,转移所有权 11 | } 12 | void process_data() { 13 | std::unique_lock lk{ get_lock() }; // 转移到了主函数的 lk 中 14 | // 执行一些任务... 15 | }// 最后才会 unlock 解锁 16 | 17 | int main(){ 18 | process_data(); 19 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/33限时等待-时间段.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | using namespace std::chrono_literals; 6 | 7 | int main(){ 8 | using namespace std::chrono; 9 | auto future = std::async(std::launch::deferred, []{ 10 | std::cout << "deferred\n"; 11 | }); 12 | 13 | if (future.wait_for(35ms) == std::future_status::deferred) 14 | std::cout << "future_status::deferred " << "正在延迟执行\n"; 15 | 16 | future.wait(); // 在 wait() 或 get() 调用时执行,不创建线程 17 | } -------------------------------------------------------------------------------- /code/04同步操作/async_progress_bar/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(async_progress_bar VERSION 0.1 LANGUAGES CXX) 4 | 5 | set(CMAKE_AUTOUIC YES) 6 | set(CMAKE_AUTOMOC ON) 7 | set(CMAKE_AUTORCC ON) 8 | 9 | set(CMAKE_CXX_STANDARD 17) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | 12 | find_package(Qt6 REQUIRED Widgets) 13 | 14 | set(PROJECT_SOURCES 15 | main.cpp 16 | async_progress_bar.cpp 17 | ) 18 | 19 | add_executable(${PROJECT_NAME} ${PROJECT_SOURCES}) 20 | 21 | target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets) -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/21new和delete是线程安全的吗?.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | struct X { 6 | X(int v) { // 主要防止有人认为构造函数、析构函数啊,是线程安全的 7 | std::cout << v << " 🤣\n"; 8 | } 9 | }; 10 | 11 | void f() { 12 | X* p = new X{ 1 }; // 存在数据竞争 13 | delete p; 14 | } 15 | 16 | int main() 17 | { 18 | for (int i = 0; i < 10; ++i) { 19 | std::thread t{ f }; 20 | std::thread t2{ f }; 21 | t.join(); 22 | t2.join(); 23 | } 24 | 25 | // C++ 保证的是内存的申请和释放 这种全局状态 是线程安全的 26 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/05-传递参数.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | void f(const std::string&); 6 | 7 | void test() { 8 | char buffer[1024]{}; 9 | //todo.. code 10 | std::thread t{ f, std::string(buffer) }; // std::string(buffer) 构造对象,由 std::string 对象自行管理 11 | t.detach(); 12 | } 13 | 14 | int main(){ 15 | // A 的引用只能引用 A 类型,或者以任何形式 转换到 A 16 | double a = 1; 17 | const int& p = a; // a 隐式转换到了 int 类型,这个转换是纯右值表达式 18 | // 因为 const T& 可以接右值表达式,所以才能通过编译 19 | const std::string& s = "123"; // "123" 构造了 std::string 对象 20 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/20recursive_mutex.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | std::recursive_mutex mtx; 6 | 7 | void recursive_function(int count) { 8 | std::lock_guard lc{ mtx }; 9 | std::cout << "Locked by thread: " << std::this_thread::get_id() << ", count: " << count << std::endl; 10 | if (count > 0) { 11 | recursive_function(count - 1); 12 | } 13 | } 14 | 15 | int main() { 16 | std::thread t1(recursive_function, 3); 17 | std::thread t2(recursive_function, 2); 18 | 19 | t1.join(); 20 | t2.join(); 21 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/32限时等待-时钟.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | using namespace std::chrono_literals; 5 | 6 | int main(){ 7 | auto now = std::chrono::system_clock::now(); 8 | time_t now_time = std::chrono::system_clock::to_time_t(now); 9 | std::cout << "Current time:\t" << std::put_time(std::localtime(&now_time), "%H:%M:%S\n"); 10 | 11 | auto now2 = std::chrono::steady_clock::now(); 12 | now_time = std::chrono::system_clock::to_time_t(now); 13 | std::cout << "Current time:\t" << std::put_time(std::localtime(&now_time), "%H:%M:%S\n"); 14 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/36C++20闩latch.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | using namespace std::chrono_literals; 6 | 7 | std::latch latch{ 10 }; 8 | 9 | void f(int id) { 10 | //todo.. 脑补任务 11 | std::cout << std::format("线程 {} 执行完任务,开始等待其它线程执行到此处\n", id); 12 | latch.arrive_and_wait(); // 减少 并等待 count_down(1); wait(); 等待计数为 0 13 | std::cout << std::format("线程 {} 彻底退出函数\n", id); 14 | } 15 | 16 | int main() { 17 | std::vector threads; 18 | for (int i = 0; i < 10; ++i) { 19 | threads.emplace_back(f, i); 20 | } 21 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/40线程池使用.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main(int argc, char* argv[]) { 7 | QCoreApplication app(argc, argv); 8 | 9 | QThreadPool* threadPool = QThreadPool::globalInstance(); 10 | 11 | // 线程池最大线程数 12 | qDebug() << threadPool->maxThreadCount(); 13 | 14 | for (int i = 0; i < 20; ++i) { 15 | threadPool->start([i]{ 16 | qDebug() << QString("thread id %1").arg(i); 17 | }); 18 | } 19 | // 当前活跃线程数 10 20 | qDebug() << threadPool->activeThreadCount(); 21 | 22 | app.exec(); 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # vuepress 35 | node_modules/ 36 | .vuepress/.cache/ 37 | .vuepress/.temp/ 38 | .vuepress/dist/ 39 | 40 | .vscode/ 41 | .vs/ 42 | x64/ 43 | out/ 44 | bin/ 45 | *.ilk 46 | *.pdb 47 | 48 | CMakePresets.json 49 | CMakeUserPresets.json 50 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/10-C++20jthread.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace std::literals::chrono_literals; 5 | 6 | void f(std::stop_token stop_token, int value) { 7 | while (!stop_token.stop_requested()) { // 检查是否已经收到停止请求 8 | std::cout << value++ << ' ' << std::flush; 9 | std::this_thread::sleep_for(200ms); 10 | } 11 | std::cout << std::endl; 12 | } 13 | 14 | int main() { 15 | std::jthread thread{ f, 1 }; // 打印 1..15 大约 3 秒 16 | std::this_thread::sleep_for(3s); 17 | thread.request_stop(); // 发送信息,线程终止 18 | std::cout << "乐\n"; 19 | // jthread 的析构函数调用 request_stop() 和 join()。 20 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/44atomic指针特化.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct X{ 4 | int v{}; 5 | void f()const { 6 | std::cout << v << '\n'; 7 | } 8 | }; 9 | 10 | int main(){ 11 | int arr[10]{ 1,2 }; 12 | 13 | std::atomic p{ arr }; 14 | 15 | p.fetch_add(1); 16 | std::cout << *(p.load()) << '\n'; 17 | 18 | p.fetch_sub(1); 19 | std::cout << *(p.load()) << '\n'; 20 | 21 | p += 1; 22 | std::cout << *(p.load()) << '\n'; 23 | 24 | p -= 1; 25 | std::cout << *(p.load()) << '\n'; 26 | 27 | X xs[3]{ {10},{20},{30} }; 28 | std::atomic p2{ xs }; 29 | p2.load()->f(); 30 | p2 += 2; 31 | p2.load()->f(); 32 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/23局部、全局、线程、CPU变量的对比与使用.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | thread_local int n = (std::puts("thread_local init"), 0); 5 | 6 | void f(){ 7 | (void)n; // 防止 gcc 与 clang 优化 8 | std::puts("f"); 9 | } 10 | 11 | void f2(){ 12 | thread_local static int n = (std::puts("f2 init"), 0); 13 | } 14 | 15 | int main(){ 16 | (void)n; // 防止 gcc 与 clang 优化 17 | std::cout << "main\n"; 18 | std::thread{ f }.join(); 19 | f2(); 20 | f2(); 21 | f2(); 22 | } 23 | 24 | // gcc 与 clang 存在优化,会出现与 msvc 不同的结果,它们直接将线程变量优化掉了 25 | // 这应该视作 bug。 26 | // 视频中想到 std::async 是下一章的内容跳过了(想到的是 msvc 的一个问题),忘记了 gcc 与 clang 此处也存在问题。 27 | // https://godbolt.org/z/qa6YfMqP7 -------------------------------------------------------------------------------- /md/06协程.md: -------------------------------------------------------------------------------- 1 | # 协程 2 | 3 | ## 前言 4 | 5 | 既然是“**现代**” C++ 并发编程教程,怎么能不聊协程呢? 6 | 7 | C++20 引入了协程语法,新增了三个用作协程的关键字:`co_await`、`co_yield`、`co_return`。但并未给出标准**协程库**,协程库在 C++23 被引入。 8 | 9 | 希望您拥有 `gcc14`、`clang19`,`Visual Studio 2022 17.11`。 10 | 11 | 我们假设您对 C++20 的协程一无所知、假设您对协程这个概念一无所知、假设您不了解其它语言的协程实现(如 Python、java)。 12 | 13 | --- 14 | 15 | 绝大多数人对协程基本可以说是一无所知,但是应该都听过这个名字,大概是因为这些编程语言都在新版本中引入它作为核心语言特性。 16 | 17 | 这带来了许多的热度,不过这并不完全算是好事,许多的营销号一样的讲述,基本全部都是错误的。 18 | 19 | 据我所知,在我在 B站发布正经 C++20 协程的教学视频之前,几乎所有打着 C++ 旗号说什么协程的,都是胡言乱语。不过也有一些不错的,如:[**等疾风**](https://space.bilibili.com/35186937)、[**happyyang的百草园**](https://space.bilibili.com/312883756),都出过至少算作正经的 C++20 协程的教学视频。 20 | 21 | - **C++20 的协程是复杂的**。 22 | 23 | 不管是使用上还是概念上,引入了许多新颖的做法。 24 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/03-thread_management.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | struct func { 5 | int& m_i; 6 | func(int& i) :m_i{ i } {} 7 | void operator()(int n)const { 8 | for (int i = 0; i <= n; ++i) { 9 | m_i += i; // 可能悬空引用 10 | } 11 | } 12 | }; 13 | 14 | void f2() { throw std::runtime_error("test f2()"); } 15 | 16 | void f() { 17 | int n = 0; 18 | std::thread t{ func{n},10 }; 19 | try { 20 | // todo.. 一些当前线程可能抛出异常的代码 21 | f2(); 22 | t.join(); 23 | } 24 | catch (...) { 25 | t.join(); // 1 26 | // 如果此处不抛出 会掩盖错误 我们根本没有处理 没有解决 27 | } 28 | } 29 | 30 | int main() { 31 | f(); 32 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/15-死锁:问题与解决.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | using namespace std::chrono_literals; 6 | 7 | struct X { 8 | X(const std::string& str) :object{ str } {} 9 | 10 | friend void swap(X& lhs, X& rhs); 11 | private: 12 | std::string object; 13 | std::mutex m; 14 | }; 15 | 16 | void swap(X& lhs, X& rhs) { 17 | if (&lhs == &rhs) return; 18 | std::scoped_lock guard{ lhs.m,rhs.m }; 19 | swap(lhs.object, rhs.object); 20 | } 21 | 22 | int main(){ 23 | X a{ "🤣" }, b{ "😅" }; 24 | std::thread t{ [&] {swap(a, b); } }; // 1 25 | std::thread t2{ [&] {swap(b, a); } }; // 2 26 | t.join(); 27 | t2.join(); 28 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/43atomic_bool.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | std::atomic flag{ false }; 6 | bool expected = false; 7 | 8 | void try_set_flag() { 9 | // 尝试将 flag 设置为 true,如果当前值为 false 10 | if (flag.compare_exchange_strong(expected, true)) { 11 | std::cout << "flag 为 false,flag 设为 true。\n"; 12 | } 13 | else { 14 | std::cout << "flag 为 true, expected 设为 true。\n"; 15 | } 16 | } 17 | 18 | int main() { 19 | std::thread t1{ try_set_flag }; 20 | std::thread t2{ try_set_flag }; 21 | t1.join(); 22 | t2.join(); 23 | std::cout << "flag: " << std::boolalpha << flag << '\n'; 24 | std::cout << "expected: " << std::boolalpha << expected << '\n'; 25 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/test.cpp: -------------------------------------------------------------------------------- 1 | #include "test/AduioPlayer.h" 2 | 3 | AudioPlayer audioPlayer; 4 | 5 | int main(){ 6 | audioPlayer.addAudioPath(AudioPlayer::soundResources[4]); 7 | audioPlayer.addAudioPath(AudioPlayer::soundResources[5]); 8 | audioPlayer.addAudioPath(AudioPlayer::soundResources[6]); 9 | audioPlayer.addAudioPath(AudioPlayer::soundResources[7]); 10 | 11 | std::thread t{ []{ 12 | std::this_thread::sleep_for(1s); 13 | audioPlayer.addAudioPath(AudioPlayer::soundResources[1]); 14 | } }; 15 | std::thread t2{ []{ 16 | audioPlayer.addAudioPath(AudioPlayer::soundResources[0]); 17 | } }; 18 | 19 | std::cout << "乐\n"; 20 | 21 | t.join(); 22 | t2.join(); 23 | 24 | std::cout << "end\n"; 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "docs:build": "vuepress-vite build .", 4 | "docs:dev": "vuepress-vite dev .", 5 | "docs:preview": "vuepress-vite preview .", 6 | "docs:clean-dev": "vuepress-vite dev . --clean-cache", 7 | "docs:update-package": "npx vp-update" 8 | }, 9 | "devDependencies": { 10 | "@vuepress/bundler-vite": "2.0.0-rc.14", 11 | "@vuepress/plugin-search": "^2.0.0-rc.37", 12 | "vue": "^3.4.27", 13 | "vuepress": "2.0.0-rc.14", 14 | "vuepress-theme-hope": "2.0.0-rc.50" 15 | }, 16 | "dependencies": { 17 | "chart.js": "^4.4.3", 18 | "echarts": "^5.5.1", 19 | "flowchart": "^1.2.0", 20 | "flowchart.ts": "^3.0.0", 21 | "katex": "^0.16.10", 22 | "mermaid": "^10.9.1", 23 | "vuepress-plugin-search-pro": "^2.0.0-rc.50" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/34限时等待-时间点.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #pragma comment(lib,"winmm.lib") 8 | 9 | using namespace std::chrono_literals; 10 | 11 | std::condition_variable cv; 12 | bool done{}; 13 | std::mutex m; 14 | 15 | bool wait_loop() { 16 | const auto timeout = std::chrono::steady_clock::now() + 500ms; 17 | std::unique_lock lk{ m }; 18 | if (!cv.wait_until(lk, timeout, [] {return done; })) { 19 | std::cout << "超时 500ms\n"; 20 | return false; 21 | } 22 | return true; 23 | } 24 | 25 | int main() { 26 | std::thread t{ wait_loop }; 27 | std::this_thread::sleep_for(400ms); 28 | done = true; 29 | cv.notify_one(); 30 | t.join(); 31 | } 32 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/14-保护共享数据.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | class Data { 6 | int a{}; 7 | std::string b{}; 8 | public: 9 | void do_something() { 10 | // 修改数据成员等... 11 | } 12 | }; 13 | 14 | class Data_wrapper { 15 | Data data; 16 | std::mutex m; 17 | public: 18 | template 19 | void process_data(Func func) { 20 | std::lock_guard lc{ m }; 21 | func(data); // 受保护数据传递给函数 22 | } 23 | }; 24 | 25 | Data* p = nullptr; 26 | 27 | void malicious_function(Data& protected_data) { 28 | p = &protected_data; // 受保护的数据被传递到外部 29 | } 30 | 31 | Data_wrapper d; 32 | 33 | void foo() { 34 | d.process_data(malicious_function); // 传递了一个恶意的函数 35 | p->do_something(); // 在无保护的情况下访问保护数据 36 | } 37 | 38 | int main(){ 39 | 40 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/16-unique_lock.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | using namespace std::chrono_literals; 6 | 7 | struct X { 8 | X(const std::string& str) :object{ str } {} 9 | 10 | friend void swap(X& lhs, X& rhs); 11 | private: 12 | std::string object; 13 | std::mutex m; 14 | }; 15 | 16 | void swap(X& lhs, X& rhs) { 17 | if (&lhs == &rhs) return; 18 | std::lock(rhs.m, lhs.m); 19 | 20 | std::unique_lock lock1{ lhs.m, std::adopt_lock }; 21 | std::unique_lock lock2{ rhs.m, std::adopt_lock }; 22 | // std::lock(lock1, lock2); 23 | swap(lhs.object, rhs.object); 24 | } 25 | 26 | int main() { 27 | X a{ "🤣" }, b{ "😅" }; 28 | std::thread t{ [&] {swap(a, b); } }; // 1 29 | std::thread t2{ [&] {swap(b, a); } }; // 2 30 | t.join(); 31 | t2.join(); 32 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/42atomic_flag实现自旋锁.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | class spinlock_mutex { 7 | std::atomic_flag flag{}; 8 | public: 9 | spinlock_mutex()noexcept = default; 10 | void lock()noexcept { 11 | while (flag.test_and_set(std::memory_order_acquire)); 12 | } 13 | 14 | void unlock()noexcept { 15 | flag.clear(std::memory_order_release); 16 | } 17 | }; 18 | 19 | spinlock_mutex m; 20 | 21 | void f() { 22 | std::lock_guard lc{ m }; 23 | std::cout << "😅😅" << "❤️❤️\n"; 24 | } 25 | 26 | int main(){ 27 | std::thread t{ f }; 28 | std::thread t1{ f }; 29 | std::thread t2{ f }; 30 | std::thread t3{ f }; 31 | std::thread t4{ f }; 32 | std::thread t5{ f }; 33 | t.join(); 34 | t1.join(); 35 | t2.join(); 36 | t3.join(); 37 | t4.join(); 38 | t5.join(); 39 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/13-try_lock.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | using namespace std::string_literals; 7 | 8 | std::mutex mtx; 9 | 10 | void thread_function(int id) { 11 | // 尝试加锁 12 | if (mtx.try_lock()) { 13 | std::string s = "线程:"s + std::to_string(id) + " 获得锁"s + "\n"; 14 | std::string s2 = "线程:"s + std::to_string(id) + " 释放锁"s + "\n"; 15 | std::cout << s; 16 | // 临界区代码 17 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟临界区操作 18 | mtx.unlock(); // 解锁 19 | std::cout << s2; 20 | } 21 | else { 22 | std::string s = "线程:"s + std::to_string(id) + " 获取锁失败 处理步骤"s + "\n"; 23 | std::cout << s; 24 | } 25 | } 26 | 27 | int main(){ 28 | std::thread t1(thread_function, 1); 29 | std::thread t2(thread_function, 2); 30 | 31 | t1.join(); 32 | t2.join(); 33 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/24等待事件或条件.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | using namespace std::chrono_literals; 7 | 8 | std::mutex mtx; // 互斥量 9 | std::condition_variable_any cv; // 条件变量 10 | bool arrived = false; 11 | 12 | void wait_for_arrival() { 13 | std::unique_lock lck(mtx); // 上锁 14 | cv.wait(lck, [] { return arrived; }); // 等待 arrived 变为 true 会解锁的 再次上锁 15 | std::cout << "到达目的地,可以下车了!" << std::endl; 16 | } 17 | 18 | void simulate_arrival() { 19 | std::this_thread::sleep_for(std::chrono::seconds(5)); // 模拟地铁到站,假设5秒后到达目的地 20 | { 21 | std::lock_guard lck(mtx); 22 | arrived = true; // 设置条件变量为 true,表示到达目的地 23 | } 24 | cv.notify_one(); // 通知等待的线程 25 | } 26 | 27 | int main(){ 28 | std::thread t{ wait_for_arrival }; 29 | std::thread t2{ simulate_arrival }; 30 | t.join(); 31 | t2.join(); 32 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/38第四章总结-勘误初始化顺序.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | struct X { 6 | X() { 7 | // 假设 X 的初始化没那么快 8 | std::this_thread::sleep_for(std::chrono::seconds(1)); 9 | std::puts("X"); 10 | v.resize(10, 6); 11 | } 12 | std::vector v; 13 | }; 14 | 15 | struct Test { 16 | Test()/* : t{ &Test::f, this }*/ // 线程已经开始执行 17 | { 18 | // 严格意义来说 这里不算初始化 至少不算 C++ 标准的定义 19 | } 20 | void start() 21 | { 22 | t = std::thread{ &Test::f, this }; 23 | } 24 | ~Test() { 25 | if (t.joinable()) 26 | t.join(); 27 | } 28 | void f()const { // 如果在函数执行的线程 f 中使用 x 则会存在问题。使用了未初始化的数据成员 ub 29 | std::cout << "f\n"; 30 | std::cout << x.v[9] << '\n'; 31 | } 32 | 33 | 34 | std::thread t; // 声明顺序决定了初始化顺序,优先初始化 t 35 | X x; 36 | }; 37 | 38 | int main() { 39 | Test t; 40 | t.start(); 41 | } -------------------------------------------------------------------------------- /code/04同步操作/async_progress_bar/async_progress_bar.ui: -------------------------------------------------------------------------------- 1 | 2 | async_progress_barClass 3 | 4 | 5 | async_progress_barClass 6 | 7 | 8 | 9 | 0 10 | 0 11 | 600 12 | 400 13 | 14 | 15 | 16 | async_progress_bar 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/06-this_thread命名空间.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | using namespace std::chrono_literals; 5 | 6 | int main() { 7 | // 获取当前时间点 8 | auto now = std::chrono::system_clock::now(); 9 | 10 | // 设置要等待的时间点为当前时间点之后的5秒 11 | auto wakeup_time = now + 5s; 12 | 13 | // 输出当前时间 14 | auto now_time = std::chrono::system_clock::to_time_t(now); 15 | std::cout << "Current time:\t\t" << std::put_time(std::localtime(&now_time), "%H:%M:%S") << std::endl; 16 | 17 | // 输出等待的时间点 18 | auto wakeup_time_time = std::chrono::system_clock::to_time_t(wakeup_time); 19 | std::cout << "Waiting until:\t\t" << std::put_time(std::localtime(&wakeup_time_time), "%H:%M:%S") << std::endl; 20 | 21 | // 等待到指定的时间点 22 | std::this_thread::sleep_until(wakeup_time); 23 | 24 | // 输出等待结束后的时间 25 | now = std::chrono::system_clock::now(); 26 | now_time = std::chrono::system_clock::to_time_t(now); 27 | std::cout << "Time after waiting:\t" << std::put_time(std::localtime(&now_time), "%H:%M:%S") << std::endl; 28 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/37C++20屏障barrier.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | void f(int start, int end, int thread_id) { 7 | for (int i = start; i <= end; ++i) { 8 | // 输出当前线程的数字 9 | std::cout << std::to_string(i) + " "; 10 | 11 | // 等待所有线程同步到达 barrier 也就是等待都输出完数字 12 | #pragma omp barrier 13 | 14 | // 每个线程输出完一句后,主线程输出轮次信息 15 | #pragma omp master 16 | { 17 | static int round_number = 1; 18 | std::cout << "\t第" << round_number++ << "轮结束\n"; 19 | std::this_thread::sleep_for(std::chrono::seconds(1)); 20 | } 21 | 22 | // 再次同步 等待所有线程(包括主线程)到达此处、避免其它线程继续执行打断主线程的输出 23 | #pragma omp barrier 24 | } 25 | } 26 | 27 | int main() { 28 | constexpr int num_threads = 10; 29 | omp_set_num_threads(num_threads); 30 | 31 | #pragma omp parallel 32 | { 33 | const int thread_id = omp_get_thread_num(); 34 | f(thread_id * 10 + 1, (thread_id + 1) * 10, thread_id); 35 | } 36 | 37 | } 38 | 39 | // https://godbolt.org/z/fabqhbx3P -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/04-RAII.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | struct func { 6 | int& m_i; 7 | func(int& i) :m_i{ i } {} 8 | void operator()(int n)const { 9 | for (int i = 0; i <= n; ++i) { 10 | m_i += i; // 可能悬空引用 11 | } 12 | } 13 | }; 14 | 15 | void f2(){ 16 | // todo.. 17 | throw std::runtime_error("f2 error"); 18 | } 19 | 20 | class thread_guard{ 21 | public: 22 | explicit thread_guard(std::thread& t) :thread_{ t } 23 | {} 24 | ~thread_guard(){ 25 | std::puts("析构"); 26 | if(thread_.joinable()){ // 如果当前有活跃线程 则进行 join 27 | thread_.join(); 28 | } 29 | } 30 | thread_guard& operator=(const thread_guard&) = delete; 31 | thread_guard(const thread_guard&) = delete; 32 | 33 | std::thread& thread_; 34 | }; 35 | 36 | void f() { 37 | int n = 0; 38 | std::thread t{ func{n},10 }; 39 | thread_guard g(t); 40 | f2(); // 可能抛出异常 41 | } 42 | 43 | int main(){ 44 | // 栈回溯 45 | try{ 46 | f(); 47 | }catch (...){} 48 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/35C++20信号量.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | using namespace std::chrono_literals; 7 | 8 | // 定义一个信号量,最大并发数为 3 9 | std::counting_semaphore<3> semaphore{ 3 }; 10 | 11 | void handle_request(int request_id) { 12 | // 请求到达,尝试获取信号量 13 | std::cout << "进入 handle_request 尝试获取信号量\n"; 14 | 15 | semaphore.acquire(); 16 | 17 | std::cout << "成功获取信号量\n"; 18 | 19 | // 此处延时三秒可以方便测试,会看到先输出 3 个“成功获取信号量”,因为只有三个线程能成功调用 acquire,剩余的会被阻塞 20 | std::this_thread::sleep_for(3s); 21 | 22 | // 模拟处理时间 23 | std::random_device rd; 24 | std::mt19937 gen{ rd() }; 25 | std::uniform_int_distribution<> dis(1, 5); 26 | int processing_time = dis(gen); 27 | std::this_thread::sleep_for(std::chrono::seconds(processing_time)); 28 | 29 | std::cout << std::format("请求 {} 已被处理\n", request_id); 30 | 31 | semaphore.release(); 32 | } 33 | 34 | int main() { 35 | // 模拟 10 个并发请求 36 | std::vector threads; 37 | for (int i = 0; i < 10; ++i) { 38 | threads.emplace_back(handle_request, i); 39 | } 40 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/18-保护共享数据的初始化过程.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | struct some{ 8 | void do_something(){} 9 | }; 10 | 11 | std::shared_ptr ptr; 12 | std::once_flag resource_flag; 13 | 14 | void init_resource() { 15 | ptr.reset(new some); 16 | } 17 | 18 | void foo() { 19 | std::call_once(resource_flag, []{ptr.reset(new some); }); // 线程安全的一次初始化 20 | ptr->do_something(); 21 | } 22 | 23 | void test(){ 24 | std::call_once(resource_flag, [] {std::cout << "f init\n"; }); 25 | } 26 | 27 | std::once_flag flag; 28 | int n = 0; 29 | 30 | void f() { 31 | std::call_once(flag, [] { 32 | ++n; 33 | std::cout << "第 " << n << " 次调用\n"; 34 | throw std::runtime_error("异常"); 35 | }); 36 | } 37 | 38 | class my_class{}; 39 | 40 | inline my_class& get_my_class_instance() { 41 | static my_class instance; // 线程安全的初始化过程 初始化严格发生一次 42 | return instance; 43 | } 44 | 45 | int main() { 46 | get_my_class_instance(); 47 | get_my_class_instance(); 48 | get_my_class_instance(); 49 | get_my_class_instance(); 50 | get_my_class_instance(); 51 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/12-使用互斥量.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | std::mutex m; 10 | 11 | // 写 12 | void add_to_list(int n, std::list& list) { 13 | std::vector numbers(n + 1); 14 | std::iota(numbers.begin(), numbers.end(), 0); 15 | int sum = std::accumulate(numbers.begin(), numbers.end(), 0); 16 | 17 | { 18 | std::scoped_lock lc{ m }; 19 | list.push_back(sum); 20 | } 21 | } 22 | 23 | // 读 24 | void print_list(const std::list& list) { 25 | std::scoped_lock lc{ m }; 26 | for (const auto& i : list) { 27 | std::cout << i << ' '; 28 | } 29 | std::cout << '\n'; 30 | } 31 | 32 | int main(){ 33 | std::list list; 34 | std::thread t1{ add_to_list,10,std::ref(list) }; 35 | std::thread t2{ add_to_list,10,std::ref(list) }; 36 | std::thread t3{ print_list,std::cref(list) }; 37 | std::thread t4{ print_list,std::cref(list) }; 38 | t1.join(); 39 | t2.join(); 40 | t3.join(); 41 | t4.join(); 42 | std::cout << "---------------------\n"; 43 | print_list(list); 44 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/19保护不常更新的数据结构.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class Settings { 9 | private: 10 | std::map data_; 11 | mutable std::shared_timed_mutex mutex_; // “M&M 规则”:mutable 与 mutex 一起出现 12 | 13 | public: 14 | void set(const std::string& key, const std::string& value) { 15 | std::lock_guard lock{ mutex_ }; 16 | data_[key] = value; 17 | } 18 | 19 | std::string get(const std::string& key) const { 20 | std::shared_lock lock(mutex_); 21 | auto it = data_.find(key); 22 | return (it != data_.end()) ? it->second : ""; // 如果没有找到键返回空字符串 23 | } 24 | }; 25 | 26 | Settings set; 27 | 28 | void read(){ 29 | (void)set.get("1"); 30 | } 31 | 32 | void write(){ 33 | set.set("1", "a"); 34 | } 35 | 36 | int main(){ 37 | std::thread t{ read }; 38 | std::thread t2{ read }; 39 | std::thread t3{ read }; 40 | std::thread t4{ read }; 41 | std::thread t5{ write }; 42 | t.join(); 43 | t2.join(); 44 | t3.join(); 45 | t4.join(); 46 | t5.join(); 47 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/31多个线程的等待shared_future.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | std::string fetch_data() { 6 | std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作 7 | return "从网络获取的数据!"; 8 | } 9 | 10 | int main() { 11 | std::future future_data = std::async(std::launch::async, fetch_data); 12 | 13 | // // 转移共享状态,原来的 future 被清空 valid() == false 14 | std::shared_future shared_future_data = future_data.share(); 15 | 16 | // 多个线程持有一个 shared_future 对象并操作 17 | 18 | // 第一个线程等待结果并访问数据 19 | std::thread thread1([shared_future_data] { 20 | std::cout << "线程1:等待数据中..." << std::endl; 21 | shared_future_data.wait(); // 等待结果可用 22 | std::cout << "线程1:收到数据:" << shared_future_data.get() << std::endl; 23 | }); 24 | 25 | // 第二个线程等待结果并访问数据 26 | std::thread thread2([shared_future_data] { 27 | std::cout << "线程2:等待数据中..." << std::endl; 28 | shared_future_data.wait(); 29 | std::cout << "线程2:收到数据:" << shared_future_data.get() << std::endl; 30 | }); 31 | 32 | thread1.join(); 33 | thread2.join(); 34 | 35 | std::promise p; 36 | std::shared_future sf{ p.get_future() }; // 隐式转移所有权 37 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/Log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | inline void setupLogging() { 8 | auto file_sink = std::make_shared("logs.txt"); 9 | file_sink->set_level(spdlog::level::debug); 10 | file_sink->set_pattern("[%Y-%m-%d %H:%M:%S] [%@] [%!] [thread %t] [%oms] [%l] %v"); 11 | 12 | auto console_sink = std::make_shared(); 13 | console_sink->set_level(spdlog::level::debug); 14 | console_sink->set_pattern("%^[%Y-%m-%d %H:%M:%S] [thread %t] [%oms] [%l] %v%$"); 15 | 16 | auto logger = std::make_shared("multi_sink", spdlog::sinks_init_list{ file_sink, console_sink }); 17 | spdlog::register_logger(logger); 18 | 19 | spdlog::set_default_logger(logger); 20 | } 21 | 22 | // spdlog 要想输出文件、路径、函数、行号,只能借助此宏,才会显示。 23 | // 其实使用 C++20 std::source_location 也能获取这些信息,后面再考虑单独封装吧,目前这样做导致没办法做格式字符串。 24 | 25 | #define LOG_INFO(msg, ...) SPDLOG_LOGGER_INFO(spdlog::get("multi_sink"), msg) 26 | #define LOG_WARN(msg, ...) SPDLOG_LOGGER_WARN(spdlog::get("multi_sink"), msg) 27 | #define LOG_ERROR(msg, ...) SPDLOG_LOGGER_ERROR(spdlog::get("multi_sink"), msg) 28 | 29 | const auto init_log = (setupLogging(), 0); 30 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/28future与 packaged_task.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | template 6 | void async_task(std::packaged_task& task, Args&&...args) { 7 | // todo.. 8 | task(std::forward(args)...); 9 | } 10 | 11 | int main() { 12 | std::packaged_task task([](int a, int b) { 13 | return a + b; 14 | }); 15 | 16 | int value = 50; 17 | 18 | std::future future = task.get_future(); 19 | 20 | // 创建一个线程来执行异步任务 21 | std::thread t{ [&] { async_task(task, value, value); } }; 22 | std::cout << future.get() << '\n'; 23 | t.join(); 24 | } 25 | 26 | //int main(){ 27 | // std::cout << "main: " << std::this_thread::get_id() << '\n'; 28 | // 29 | // // 只能移动不能复制 30 | // std::packaged_task task{ [](int a, int b) { 31 | // std::cout << "packaged_task: " << std::this_thread::get_id() << '\n'; 32 | // return std::pow(a, b); 33 | // } }; 34 | // 35 | // std::future future = task.get_future(); 36 | // 37 | // // task(10, 2); // 调用 此处执行任务 38 | // 39 | // std::thread t{ std::move(task) ,10,2 }; 40 | // 41 | // std::cout << "------\n"; 42 | // 43 | // std::cout << future.get() << '\n'; // 会阻塞,直到任务执行完毕 44 | // 45 | // t.join(); 46 | //} -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/39原子类型atomic.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | using namespace std::chrono_literals; 6 | 7 | // 不要这样使用 不要在多线程并发中使用 volatile 8 | // 它的行为是不保证的 9 | std::atomic n = 0; 10 | 11 | void read(){ 12 | while(true){ 13 | std::this_thread::sleep_for(500ms); 14 | std::cout << n.load() << '\n'; 15 | } 16 | } 17 | 18 | void write(){ 19 | while (true){ 20 | ++n; 21 | } 22 | } 23 | 24 | // 数据竞争 数据竞争未定义行为 25 | // 优化会假设你的程序中没有未定义行为 26 | 27 | // C 语言的平凡的结构体 28 | struct trivial_type { 29 | int x; 30 | float y; 31 | }; 32 | 33 | int main(){ 34 | // 创建一个 std::atomic 对象 35 | std::atomic atomic_my_type{ { 10, 20.5f } }; 36 | 37 | // 使用 store 和 load 操作来设置和获取值 38 | trivial_type new_value{ 30, 40.5f }; 39 | atomic_my_type.store(new_value); 40 | 41 | std::cout << "x: " << atomic_my_type.load().x << ", y: " << atomic_my_type.load().y << std::endl; 42 | 43 | // 使用 exchange 操作 44 | trivial_type exchanged_value = atomic_my_type.exchange({ 50, 60.5f }); 45 | std::cout << "交换前的 x: " << exchanged_value.x 46 | << ", 交换前的 y: " << exchanged_value.y << std::endl; 47 | std::cout << "交换后的 x: " << atomic_my_type.load().x 48 | << ", 交换后的 y: " << atomic_my_type.load().y << std::endl; 49 | } -------------------------------------------------------------------------------- /code/04同步操作/async_progress_bar/async_progress_bar.cpp: -------------------------------------------------------------------------------- 1 | #include "async_progress_bar.h" 2 | 3 | async_progress_bar::async_progress_bar(QWidget *parent) 4 | : QMainWindow{ parent }, progress_bar{ new QProgressBar(this) }, 5 | button{ new QPushButton("start",this) },button2{ new QPushButton("测试",this) } { 6 | ui.setupUi(this); 7 | 8 | progress_bar->setStyleSheet(progress_bar_style); 9 | progress_bar->setRange(0, 1000); 10 | 11 | button->setMinimumSize(100, 50); 12 | button->setMaximumWidth(100); 13 | button->setStyleSheet(button_style); 14 | button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); 15 | 16 | button2->setMinimumSize(100, 50); 17 | button2->setMaximumWidth(100); 18 | button2->setStyleSheet(button_style); 19 | button2->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); 20 | 21 | QVBoxLayout* layout = new QVBoxLayout; 22 | layout->addWidget(progress_bar); 23 | layout->addWidget(button, 0, Qt::AlignHCenter); 24 | layout->addWidget(button2, 0, Qt::AlignHCenter); 25 | // 设置窗口布局为垂直布局管理器 26 | centralWidget()->setLayout(layout); 27 | 28 | connect(button, &QPushButton::clicked, this, &async_progress_bar::task); 29 | connect(button2, &QPushButton::clicked, []{ 30 | QMessageBox::information(nullptr, "测试", "没有卡界面!"); 31 | }); 32 | } 33 | 34 | async_progress_bar::~async_progress_bar() 35 | {} 36 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | # 每当 push 到 main 分支时触发部署 5 | push: 6 | branches: [main] 7 | # 手动触发部署 8 | workflow_dispatch: 9 | 10 | jobs: 11 | docs: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | # “最近更新时间” 等 git 日志相关信息,需要拉取全部提交记录 18 | fetch-depth: 0 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v4 22 | with: 23 | # 选择要使用的 pnpm 版本 24 | version: 8 25 | # 使用 pnpm 安装依赖 26 | run_install: true 27 | 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v4 30 | with: 31 | # 选择要使用的 node 版本 32 | node-version: 20 33 | # 缓存 pnpm 依赖 34 | cache: pnpm 35 | 36 | # 运行构建脚本 37 | - name: Build VuePress site 38 | run: pnpm docs:build 39 | 40 | # 查看 workflow 的文档来获取更多信息 41 | # @see https://github.com/crazy-max/ghaction-github-pages 42 | - name: Deploy to GitHub Pages 43 | uses: crazy-max/ghaction-github-pages@v4 44 | with: 45 | # 部署到 gh-pages 分支 46 | target_branch: gh-pages 47 | # 部署目录为 VuePress 的默认输出目录 48 | build_dir: .vuepress/dist 49 | env: 50 | # @see https://docs.github.com/cn/actions/reference/authentication-in-a-workflow#about-the-github_token-secret 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | cpp 5 | 6 | 7 | # 现代C++并发编程教程 8 | 9 | 本仓库用来存放 B 站课程[《现代 C++ 并发编程教程》](https://www.bilibili.com/cheese/play/ss34184)的教案、代码。 10 | 11 | 不管是否购买课程,任何组织和个人遵守 [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh-hans) 协议均可随意使用学习。 12 | 13 | [捐赠](https://github.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/tree/main/image/%E6%8D%90%E8%B5%A0)、[issues](https://github.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/issues)、[pr](https://github.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/pulls) 均会在致谢列表中**铭记您的贡献**。 14 | 15 |
16 | 17 | --- 18 | 19 |   国内的 C++ 并发编程的教程并不稀少,不管是书籍、博客、视频。然而大多数是粗糙的、不够准确、复杂的。而我们想以更加**现代**、**简单**、**准确**的方式进行教学。 20 | 21 |   我们在教学中可能常常为您展示部分标准库源码,自己手动实现一些库,这是必须的,希望您是已经较为熟练使用模板(如果没有,可以先学习 [**现代C++模板教程**](https://github.com/Mq-b/Modern-Cpp-templates-tutorial))。阅读源码可以帮助我们更轻松的理解标准库设施的使用与原理。 22 | 23 |   本教程假设开发者的最低水平为:**`C++11 + STL + template`**。 24 | 25 |   虽强调现代,但不用担心,我们几乎是从头教学,即使你从来没使用过 C++ 进行多线程编程,也不成问题。 26 | 27 |   我们希望您的编译器版本和标准尽可能的高,我们的代码均会测试三大编译器 gcc、clang、msvc。需要更高的标准会进行强调。 28 | 29 |
30 | 31 | ![猫猫虫](./image/猫猫虫旋转.jpg) 32 | 33 | 如果你觉得本仓库对你有所帮助,可以通过支付宝赞助白老师,激励白老师有更多的精力和信心维护本仓库。 34 | 35 |
36 | 37 | > [!TIP] 38 | > 每一位开发者赞助 `30`,白老师一天的食品安全就有了着落。 39 | 40 | cpp 41 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/29使用promise.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | using namespace std::chrono_literals; 6 | 7 | void f(std::promise obj ,int num){ 8 | // todo.. 9 | obj.set_value(num * num); // 调用了 set_value 10 | // todo.. 11 | std::this_thread::sleep_for(5s); // 模拟一些计算 12 | } 13 | 14 | void throw_function(std::promise prom) { 15 | prom.set_value(100); 16 | try { 17 | // todo.. 18 | throw std::runtime_error("一个异常"); 19 | } 20 | catch (...) { 21 | try { 22 | // 共享状态的 promise 已存储值,调用 set_exception 产生异常 23 | prom.set_exception(std::current_exception()); 24 | } 25 | catch (std::exception& e) { 26 | std::cerr << "来自 set_exception 的异常: " << e.what() << '\n'; 27 | } 28 | } 29 | } 30 | 31 | int main() { 32 | std::promise prom; 33 | std::future fut = prom.get_future(); 34 | 35 | std::thread t(throw_function, std::move(prom)); 36 | 37 | std::cout << "等待线程执行,抛出异常并设置\n"; 38 | std::cout << "值:" << fut.get() << '\n'; // 100 39 | 40 | t.join(); 41 | } 42 | 43 | 44 | //int main(){ 45 | // std::promise promise; 46 | // 47 | // auto future = promise.get_future(); // 关联了 48 | // 49 | // std::thread t{ f,std::move(promise), 10 }; 50 | // // f(std::move(promise), 10); 51 | // 52 | // std::cout << future.get() << '\n'; // 阻塞,直至结果可用 53 | // std::cout << "end\n"; 54 | // t.join(); 55 | //} -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/09-实现joining_thread.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | class joining_thread { 8 | std::thread t; 9 | public: 10 | joining_thread()noexcept = default; 11 | 12 | template 13 | explicit joining_thread(Callable&& func, Args&&...args) : 14 | t{ std::forward(func), std::forward(args)... } {} 15 | 16 | explicit joining_thread(std::thread t_)noexcept : t{ std::move(t_) } {} 17 | 18 | joining_thread(joining_thread&& other)noexcept : t{ std::move(other.t) } {} 19 | 20 | joining_thread& operator=(std::thread&& other)noexcept { 21 | if (joinable()) { // 如果当前有活跃线程(判断当前对象是否持有资源),那就先执行完(先释放) 22 | join(); // 就相当于释放资源一样的意思 23 | } 24 | t = std::move(other); 25 | return *this; 26 | } 27 | ~joining_thread() { 28 | if (joinable()) { 29 | join(); 30 | } 31 | } 32 | void swap(joining_thread& other)noexcept { 33 | t.swap(other.t); 34 | } 35 | std::thread::id get_id()const noexcept { 36 | return t.get_id(); 37 | } 38 | bool joinable()const noexcept { 39 | return t.joinable(); 40 | } 41 | void join() { 42 | t.join(); 43 | } 44 | void detach() { 45 | t.detach(); 46 | } 47 | std::thread& data()noexcept { 48 | return t; 49 | } 50 | const std::thread& data()const noexcept { 51 | return t; 52 | } 53 | }; 54 | 55 | int main(){ 56 | auto func = []{ 57 | std::cout << std::this_thread::get_id() << '\n'; 58 | }; 59 | 60 | std::vector vec; 61 | 62 | for (int i = 0; i < 10; ++i){ 63 | vec.emplace_back(func); 64 | } 65 | 66 | for(auto& thread : vec){ 67 | thread.join(); 68 | } 69 | } -------------------------------------------------------------------------------- /md/README.md: -------------------------------------------------------------------------------- 1 | # 阅读须知 2 | 3 |   本套教程侧重点在于使用 C++ 并发支持库进行多线程编程。我们假设读者最低水平为:C++11 + STL + template,可能没有接触过 C++ 标准并发库,假设略微了解操作系统基本知识。 4 | 5 |   我们强调了模板,因为并发支持库的很多设施其实现是较为简单的,概念与使用,再结合源码讲解会更加简单直观,然而要想阅读学习源码,模板的知识必不可少。不需要模板的水平有多高,也不需要会什么元编程,但是基本的需求得能做到,得会,这里推荐一下:[**《现代C++模板教程》**](https://github.com/Mq-b/Modern-Cpp-templates-tutorial)。 6 | 7 |   本教程不保证你学习之后的成果,不过依然可以自信地说:**本教程在中文社区的同类型教程中是绝对的第一**。事实上只需要一句话就可以表达了——**伟大无需多言**。 8 | 9 | ## 学习注意事项 10 | 11 |   我们的教程中常包含许多外部链接,这并非当前描述不足或者不够严谨,而是为了考虑读者的水平和可能的扩展学习需求。同时,也希望能让读者避免获取二手知识与理解,我们提供的链接基本都是较为专业的文档或官方网站。 12 | 13 |   虽然教程名为《现代 C++ 并发编程教程》,但我们也扩展涉及了许多其他知识,包括但不限于:Win32、POSIX API;MSVC STL、libstdc++、libc++ 对标准库的实现;GCC 与 MSVC 的编译器扩展,以及 Clang 对它们的兼容;使用 CMake + Qt 构建带 UI 的程序,展示多线程异步的必要性;不同架构的内存模型(例如 x86 架构内存模型:Total Store Order (TSO),较为严格的内存模型)。 14 | 15 |   既然强调了“**现代**”,那自然是全方面的,具体的读者会在学习中感受到的。 16 | 17 |   另外我们的代码都会测试三大编译器 `Clang`、`GCC`、`MSVC`。通常都会是最新的,`Clang18`、`GCC14`。我们的教程中常常会提供 [Complier Explorer](https://godbolt.org/) 的运行测试链接以确保正确性,以及方便读者的测试与学习。如果你对此网站的使用不熟悉,可以阅读[使用文档](https://mq-b.github.io/Loser-HomeWork/src/%E5%8D%A2%E7%91%9F%E6%97%A5%E7%BB%8F/godbolt%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3)。 18 | 19 | ## 代码风格 20 | 21 |   我们的代码风格较为简洁明了,命名全部使用下划线连接,而不是驼峰命名法。花括号通常只占一行,简短的代码可以不额外占行。一般初始化时使用 `{}`,而非 `()` 或者 `=` 。这样简单直观,避免歧义和许多问题。`#include` 引入头文件时需要在尖括号或引号前后加空格。 22 | 23 | ```cpp 24 | #include 25 | 26 | struct move_only{ 27 | move_only() { std::puts("默认构造"); } 28 | move_only(move_only&&)noexcept { std::puts("移动构造"); } 29 | move_only& operator=(move_only&&) noexcept { 30 | std::puts("移动赋值"); 31 | return *this; 32 | } 33 | move_only(const move_only&) = delete; 34 | }; 35 | 36 | int main(){ 37 | move_only m{}; 38 | char buffer[1024]{} // 全部初始化为 0 39 | } 40 | ``` 41 | 42 | 如果是标量类型,可能考虑使用复制初始化,而非 `{}`,如:`int n = 0;`。 43 | 44 | ## 总结 45 | 46 |   本教程长期维护,接受 pr 与 issue。 47 | 48 |   好了,稍微了解了一下,我们可以开始进入正式的学习内容了。 49 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/02-hardware_concurrency.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | template 9 | auto sum(ForwardIt first, ForwardIt last) { 10 | using value_type = std::iter_value_t; 11 | std::size_t num_threads = std::thread::hardware_concurrency(); 12 | std::ptrdiff_t distance = std::distance(first, last); 13 | 14 | if (distance > 1024000) { 15 | // 计算每个线程处理的元素数量 16 | std::size_t chunk_size = distance / num_threads; 17 | std::size_t remainder = distance % num_threads; 18 | 19 | // 存储每个线程的结果 20 | std::vector results{ num_threads }; 21 | 22 | // 存储关联线程的线程对象 23 | std::vector threads; 24 | 25 | // 创建并启动线程 26 | auto start = first; 27 | for (std::size_t i = 0; i < num_threads; ++i) { 28 | auto end = std::next(start, chunk_size + (i < remainder ? 1 : 0)); 29 | threads.emplace_back([start, end, &results, i] { 30 | results[i] = std::accumulate(start, end, value_type{}); 31 | }); 32 | start = end; // 开始迭代器不断向前 33 | } 34 | 35 | // 等待所有线程执行完毕 36 | for (auto& thread : threads) 37 | thread.join(); 38 | 39 | // 汇总线程的计算结果 40 | value_type total_sum = std::accumulate(results.begin(), results.end(), value_type{}); 41 | return total_sum; 42 | } 43 | 44 | value_type total_sum = std::accumulate(first, last, value_type{}); 45 | return total_sum; 46 | } 47 | 48 | int main() { 49 | std::vector vecs{ "1","2","3","4" }; 50 | auto result = sum(vecs.begin(), vecs.end()); 51 | std::cout << result << '\n'; 52 | 53 | vecs.clear(); 54 | for (std::size_t i = 0; i <= 1024001u; ++i) { 55 | vecs.push_back(std::to_string(i)); 56 | } 57 | result = sum(vecs.begin(), vecs.end()); 58 | std::cout << result << '\n'; 59 | } -------------------------------------------------------------------------------- /md/01基本概念.md: -------------------------------------------------------------------------------- 1 | # 基本概念 2 | 3 | ## 前言 4 | 5 |   在我们谈起“*并发编程*”,其实可以直接简单理解为“**多线程编程**”,我知道你或许有疑问:“那多进程呢?” C++ 语言层面没有进程的概念,并发支持库也不涉及多进程,所以在本教程中,不用在意。 6 | 7 |   我们主要使用标准 C++ 进行教学,也会稍微涉及一些其它库。 8 | 9 | ## 并发 10 | 11 | 并发,指两个或两个以上的独立活动同时发生。 12 | 13 | 并发在生活中随处可见,我们可以一边走路一边说话,也可以两只手同时做不同的动作,又或者一边看电视一边吃零食。 14 | 15 | ## 在计算机中的并发 16 | 17 | 计算机中的并发有两种方式: 18 | 19 | 1. 多核机器的真正**并行**。 20 | 21 | 2. 单核机器的**任务切换**。 22 | 23 |   在早期,一些单核机器,它要想并发,执行多个任务,那就只能是任务切换,任务切换会给你一种“**好像**这些任务都在同时执行”的假象。只有硬件上是多核的,才能进行真正的并行,也就是真正的”**同时执行任务**“。 24 | 25 |   在现在,我们日常使用的机器,基本上是二者都有。我们现在的 CPU 基本都是多核,而操作系统调度基本也一样有任务切换,因为要执行的任务非常之多,CPU 是很快的,但是核心却没有那么多,不可能每一个任务都单独给一个核心。大家可以打开自己电脑的任务管理器看一眼,进程至少上百个,线程更是上千。这基本不可能每一个任务分配一个核心,都并行,而且也没必要。正是任务切换使得这些后台任务可以运行,这样系统使用者就可以同时运行文字处理器、编译器、编辑器和 Web 浏览器。 26 | 27 | ```mermaid 28 | graph TD; 29 | subgraph "多核机器的真正并行" 30 | MultiCore[多核机器] 31 | CPU1[CPU1] 32 | CPU2[CPU2] 33 | CPU3[CPU3] 34 | 35 | MultiCore --> CPU1 36 | MultiCore --> CPU2 37 | MultiCore --> CPU3 38 | 39 | CPU1 --> Task1[任务1] 40 | CPU2 --> Task2[任务2] 41 | CPU3 --> Task3[任务3] 42 | end 43 | ``` 44 | 45 | ```mermaid 46 | graph TD; 47 | subgraph "单核机器的任务切换" 48 | SingleCore[单核机器] 49 | SingleCore --> OS[操作系统调度] 50 | OS --> Task1[任务1执行片段] 51 | Task1 --> OS2[操作系统调度] 52 | OS2 --> Task2[任务2执行片段] 53 | Task2 --> OS3[操作系统调度] 54 | OS3 --> Task3[任务3执行片段] 55 | end 56 | 57 | ``` 58 | 59 | ## 并发与并行 60 | 61 | 事实上,对于这两个术语,并没有非常公认的说法。 62 | 63 | 1. 有些人认为二者毫无关系,指代的东西完全不同。 64 | 65 | 2. 有些人认为二者大多数时候是相同的,只是用于描述一些东西的时候关注点不同。 66 | 67 | 我喜欢第二种,那我们就讲第二种。 68 | 69 | 对多线程来说,这两个概念大部分是重叠的。对于很多人来说,它们没有什么区别。 70 | 这两个词是用来描述硬件同时执行多个任务的方式: 71 | 72 | - “并行”更加注重性能。使用硬件提高数据处理速度时,会讨论程序的并行性。 73 | 74 | - 当关注重点在于任务分离或任务响应时,会讨论程序的并发性。 75 | 76 | 这两个术语存在的目的,就是为了区别多线程中不同的关注点。 77 | 78 | ## 总结 79 | 80 |   概念从来不是我们的重点,尤其是某些说法准确性也一般,假设开发者对操作系统等知识有基本了解。 81 | 82 |   我们也不打算**特别介绍**什么 C++ 并发库的历史发展、什么时候你该使用多线程、什么时候不该使用多线程... 类似问题应该是看你自己的,而我们回到代码上即可。 83 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.8) 2 | 3 | project ("ModernCpp-ConcurrentProgramming-Tutorial") 4 | 5 | set(CMAKE_CXX_STANDARD 20) 6 | 7 | SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) 8 | 9 | if(MSVC) 10 | add_compile_options("/utf-8" "/permissive-" "/Zc:nrvo" "/openmp") 11 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 12 | add_compile_options("-finput-charset=UTF-8" "-fexec-charset=UTF-8" "-fopenmp") 13 | endif() 14 | 15 | add_executable(${PROJECT_NAME} "test2.cpp") 16 | 17 | set(CMAKE_PREFIX_PATH "D:/lib" CACHE STRING "自定义查找包的安装路径" FORCE) 18 | 19 | find_package(SFML 2.6.1 COMPONENTS system window graphics audio network REQUIRED) 20 | target_link_libraries(${PROJECT_NAME} PRIVATE sfml-system sfml-window sfml-graphics sfml-audio sfml-network) 21 | 22 | find_package(fmt CONFIG REQUIRED) 23 | target_link_libraries(${PROJECT_NAME} PRIVATE fmt::fmt-header-only) 24 | 25 | find_package(Qt6 REQUIRED Widgets) 26 | target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets) 27 | 28 | # 当前环境可以直接查找到 vcpkg 的 Boost_DIR 但是却无法查找到 include 路径,手动设置 29 | set(Boost_INCLUDE_DIR "D:/vcpkg-master/installed/x64-windows/include") 30 | include_directories(${Boost_INCLUDE_DIR}) 31 | find_package(Boost REQUIRED COMPONENTS system) 32 | target_link_libraries(${PROJECT_NAME} PRIVATE Boost::system) 33 | 34 | target_include_directories(${PROJECT_NAME} PRIVATE "D:/project/cpp-terminal/include") 35 | if(CMAKE_BUILD_TYPE STREQUAL "Release") 36 | target_link_libraries(${PROJECT_NAME} PRIVATE 37 | "D:/project/cpp-terminal/lib/cpp-terminal-private.lib" 38 | "D:/project/cpp-terminal/lib/cpp-terminal.lib" 39 | ) 40 | else() 41 | target_link_libraries(${PROJECT_NAME} PRIVATE 42 | "D:/project/cpp-terminal/lib/private/debug/cpp-terminal-private.lib" 43 | "D:/project/cpp-terminal/lib/debug/cpp-terminal.lib" 44 | ) 45 | endif() 46 | 47 | find_package(spdlog REQUIRED) 48 | target_link_libraries(${PROJECT_NAME} PRIVATE spdlog::spdlog_header_only) 49 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/25线程安全的队列.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | using namespace std::chrono_literals; 8 | 9 | template 10 | class threadsafe_queue { 11 | mutable std::mutex m; // M&M 原则 互斥量,用于保护队列操作的独占访问 12 | std::condition_variable data_cond; // 条件变量,用于在队列为空时等待 13 | std::queue data_queue; // 实际存储数据的队列 14 | public: 15 | threadsafe_queue() {} 16 | 17 | void push(T new_value) { 18 | { 19 | std::lock_guard lk{ m }; 20 | std::cout << "push:" << new_value << std::endl; 21 | data_queue.push(new_value); 22 | } 23 | data_cond.notify_one(); 24 | } 25 | // 从队列中弹出元素(阻塞直到队列不为空) 26 | void pop(T& value) { 27 | std::unique_lock lk{ m }; 28 | data_cond.wait(lk, [this] {return !data_queue.empty(); }); // 解除阻塞 重新获取锁 lock 29 | value = data_queue.front(); 30 | std::cout << "pop:" << value << std::endl; 31 | data_queue.pop(); 32 | } 33 | // 从队列中弹出元素(阻塞直到队列不为空),并返回一个指向弹出元素的 shared_ptr 34 | std::shared_ptr pop() { 35 | std::unique_lock lk{ m }; 36 | data_cond.wait(lk, [this] {return !data_queue.empty(); }); 37 | std::shared_ptrres{ std::make_shared(data_queue.front()) }; 38 | data_queue.pop(); 39 | return res; 40 | } 41 | bool empty()const { 42 | std::lock_guard lk(m); 43 | return data_queue.empty(); 44 | } 45 | }; 46 | 47 | void producer(threadsafe_queue& q) { 48 | for (int i = 0; i < 5; ++i) { 49 | q.push(i); 50 | } 51 | } 52 | void consumer(threadsafe_queue& q) { 53 | for (int i = 0; i < 5; ++i) { 54 | int value{}; 55 | q.pop(value); 56 | } 57 | } 58 | 59 | int main() { 60 | threadsafe_queue q; 61 | 62 | std::thread producer_thread(producer, std::ref(q)); 63 | std::thread consumer_thread(consumer, std::ref(q)); 64 | 65 | producer_thread.join(); 66 | consumer_thread.join(); 67 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/45原子特化shared_ptr.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | using namespace std::chrono_literals; 7 | 8 | class Data { 9 | public: 10 | Data(int value = 0) : value_(value) {} 11 | int get_value() const { return value_; } 12 | void set_value(int new_value) { value_ = new_value; } 13 | private: 14 | int value_; 15 | }; 16 | 17 | std::atomic> data = std::make_shared(); 18 | 19 | void writer() { 20 | for (int i = 0; i < 10; ++i) { 21 | std::shared_ptr new_data = std::make_shared(i); 22 | data.store(new_data); 23 | std::this_thread::sleep_for(100ms); 24 | } 25 | } 26 | 27 | void reader() { 28 | for (int i = 0; i < 10; ++i) { 29 | if (auto sp = data.load()) { 30 | std::cout << "读取线程值: " << sp->get_value() << std::endl; 31 | } 32 | else { 33 | std::cout << "没有读取到数据" << std::endl; 34 | } 35 | std::this_thread::sleep_for(100ms); 36 | } 37 | } 38 | 39 | std::atomic> ptr = std::make_shared(); 40 | 41 | void wait_for_wake_up() { 42 | std::osyncstream{ std::cout } 43 | << "线程 " 44 | << std::this_thread::get_id() 45 | << " 阻塞,等待更新唤醒\n"; 46 | 47 | // 等待 ptr 变为其它值 48 | ptr.wait(ptr.load()); 49 | 50 | std::osyncstream{ std::cout } 51 | << "线程 " 52 | << std::this_thread::get_id() 53 | << " 已被唤醒\n"; 54 | } 55 | 56 | void wake_up() { 57 | std::this_thread::sleep_for(5s); 58 | 59 | // 更新值并唤醒 60 | ptr.store(std::make_shared(10)); 61 | ptr.notify_one(); 62 | } 63 | 64 | int main() { 65 | //std::thread writer_thread{ writer }; 66 | //std::thread reader_thread{ reader }; 67 | 68 | //writer_thread.join(); 69 | //reader_thread.join(); 70 | 71 | //std::atomic> ptr = std::make_shared(10); 72 | //std::atomic_ref ref{ *ptr.load() }; 73 | //ref = 100; // 原子地赋 100 给被引用的对象 74 | //std::cout << *ptr.load() << '\n'; 75 | std::thread t1{ wait_for_wake_up }; 76 | wake_up(); 77 | t1.join(); 78 | } -------------------------------------------------------------------------------- /code/03共享数据/保护不常更新的数据结构.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | using namespace std::chrono_literals; 8 | 9 | class Settings { 10 | std::map data_; 11 | mutable std::shared_mutex mutex_; // “M&M 规则”:mutable 与 mutex 一起出现 12 | 13 | public: 14 | void set(const std::string& key, const std::string& value) { 15 | std::lock_guard lock(mutex_); 16 | data_[key] = value; 17 | } 18 | 19 | std::string get(const std::string& key) const { 20 | std::shared_lock lock{ mutex_ }; 21 | const auto it = data_.find(key); 22 | return (it != data_.end()) ? it->second : ""; // 如果没有找到键返回空字符串 23 | } 24 | }; 25 | 26 | void readSettings(const Settings& settings, const std::string& key, int threadId) { 27 | std::cout << "线程ID " << threadId << " 读设置: " << key << ", value: " << settings.get(key) << std::endl; 28 | } 29 | 30 | void writeSettings(Settings& settings, const std::string& key, const std::string& value, int threadId) { 31 | settings.set(key, value); 32 | std::cout << "线程ID " << threadId << " 写设置 " << key << " to " << value << std::endl; 33 | } 34 | 35 | int main() { 36 | Settings settings; 37 | 38 | // 写入设置 (with exclusive lock) 39 | std::thread writer{ [&settings] { 40 | for (int i = 0; i < 5; ++i) { 41 | writeSettings(settings, "key" + std::to_string(i), "value" + std::to_string(i), 1); 42 | std::this_thread::sleep_for(100ms); // 模拟一些工作 43 | } 44 | } }; 45 | 46 | // 读取设置 (with shared lock) 47 | std::thread reader1{ [&settings] { 48 | for (int i = 0; i < 5; ++i) { 49 | readSettings(settings, "key" + std::to_string(i), 2); 50 | std::this_thread::sleep_for(100ms); // 模拟一些工作 51 | } 52 | } }; 53 | 54 | std::thread reader2{ [&settings] { 55 | for (int i = 0; i < 5; ++i) { 56 | readSettings(settings, "key" + std::to_string(i), 3); 57 | std::this_thread::sleep_for(100ms); // 模拟一些工作 58 | } 59 | } }; 60 | 61 | writer.join(); 62 | reader1.join(); 63 | reader2.join(); 64 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/CMakeSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "x64-Debug", 5 | "generator": "Ninja", 6 | "configurationType": "Debug", 7 | "inheritEnvironments": [ "msvc_x64_x64" ], 8 | "buildRoot": "${projectDir}\\out\\build\\${name}", 9 | "installRoot": "${projectDir}\\out\\install\\${name}", 10 | "cmakeCommandArgs": "", 11 | "buildCommandArgs": "", 12 | "ctestCommandArgs": "" 13 | }, 14 | { 15 | "name": "x64-Release", 16 | "generator": "Ninja", 17 | "configurationType": "RelWithDebInfo", 18 | "buildRoot": "${projectDir}\\out\\build\\${name}", 19 | "installRoot": "${projectDir}\\out\\install\\${name}", 20 | "cmakeCommandArgs": "", 21 | "buildCommandArgs": "", 22 | "ctestCommandArgs": "", 23 | "inheritEnvironments": [ "msvc_x64_x64" ], 24 | "variables": [] 25 | }, 26 | { 27 | "name": "x86-Debug", 28 | "generator": "Ninja", 29 | "configurationType": "Debug", 30 | "buildRoot": "${projectDir}\\out\\build\\${name}", 31 | "installRoot": "${projectDir}\\out\\install\\${name}", 32 | "cmakeCommandArgs": "", 33 | "buildCommandArgs": "", 34 | "ctestCommandArgs": "", 35 | "inheritEnvironments": [ "msvc_x86" ], 36 | "variables": [] 37 | }, 38 | { 39 | "name": "x86-Release", 40 | "generator": "Ninja", 41 | "configurationType": "RelWithDebInfo", 42 | "buildRoot": "${projectDir}\\out\\build\\${name}", 43 | "installRoot": "${projectDir}\\out\\install\\${name}", 44 | "cmakeCommandArgs": "", 45 | "buildCommandArgs": "", 46 | "ctestCommandArgs": "", 47 | "inheritEnvironments": [ "msvc_x86" ], 48 | "variables": [] 49 | }, 50 | { 51 | "name": "x64-Clang-Debug", 52 | "generator": "Ninja", 53 | "configurationType": "Debug", 54 | "buildRoot": "${projectDir}\\out\\build\\${name}", 55 | "installRoot": "${projectDir}\\out\\install\\${name}", 56 | "cmakeCommandArgs": "", 57 | "buildCommandArgs": "", 58 | "ctestCommandArgs": "", 59 | "inheritEnvironments": [ "clang_cl_x64_x64" ], 60 | "variables": [] 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /code/04同步操作/async_progress_bar/async_progress_bar.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "ui_async_progress_bar.h" 12 | 13 | using namespace std::chrono_literals; 14 | 15 | class async_progress_bar : public QMainWindow{ 16 | Q_OBJECT 17 | 18 | public: 19 | async_progress_bar(QWidget *parent = nullptr); 20 | ~async_progress_bar(); 21 | 22 | void task(){ 23 | future = std::async(std::launch::async, [=] { 24 | QMetaObject::invokeMethod(this, [this] { 25 | // 这里显示的线程 ID 就是主线程,代表这些任务就是在主线程,即 UI 线程执行 26 | QMessageBox::information(nullptr, "线程ID", std::to_string(_Thrd_id()).c_str()); 27 | button->setEnabled(false); 28 | progress_bar->setRange(0, 1000); 29 | button->setText("正在执行..."); 30 | }); 31 | for (int i = 0; i <= 1000; ++i) { 32 | std::this_thread::sleep_for(10ms); 33 | QMetaObject::invokeMethod(this, [this, i] { 34 | progress_bar->setValue(i); 35 | }); 36 | } 37 | QMetaObject::invokeMethod(this, [this] { 38 | button->setText("start"); 39 | button->setEnabled(true); 40 | }); 41 | // 不在 invokeMethod 中获取线程 ID,这里显示的是子线程的ID 42 | auto s = std::to_string(_Thrd_id()); 43 | QMetaObject::invokeMethod(this, [=] { 44 | QMessageBox::information(nullptr, "线程ID", s.c_str()); 45 | }); 46 | }); 47 | } 48 | private: 49 | QString progress_bar_style = 50 | "QProgressBar {" 51 | " border: 2px solid grey;" 52 | " border-radius: 5px;" 53 | " background-color: lightgrey;" 54 | " text-align: center;" // 文本居中 55 | " color: #000000;" // 文本颜色 56 | "}" 57 | "QProgressBar::chunk {" 58 | " background-color: #7FFF00;" 59 | " width: 10px;" // 设置每个进度块的宽度 60 | " font: bold 14px;" // 设置进度条文本字体 61 | "}"; 62 | QString button_style = 63 | "QPushButton {" 64 | " text-align: center;" // 文本居中 65 | "}"; 66 | QProgressBar* progress_bar{}; 67 | QPushButton* button{}; 68 | QPushButton* button2{}; 69 | Ui::async_progress_barClass ui{}; 70 | std::future future; 71 | }; 72 | -------------------------------------------------------------------------------- /.vuepress/config.js: -------------------------------------------------------------------------------- 1 | import { viteBundler } from '@vuepress/bundler-vite'; 2 | import { defineUserConfig } from 'vuepress'; 3 | import { logoPath, repoBase, repoName, repoUrl } from './params'; 4 | import { hopeTheme } from 'vuepress-theme-hope'; 5 | 6 | const tutorialPath = '/md/'; 7 | const srcCodePath = tutorialPath + '详细分析/'; 8 | 9 | export default defineUserConfig({ 10 | base: repoBase + '/', 11 | title: repoName, 12 | description: repoName, 13 | lang: 'zh-CN', 14 | theme: hopeTheme({ 15 | sidebar: [ 16 | { text: '首页', link: '/', }, 17 | { text: '阅读须知', link: tutorialPath }, 18 | { text: '基本概念', link: tutorialPath + '01基本概念', }, 19 | { text: '使用线程', link: tutorialPath + '02使用线程', }, 20 | { text: '共享数据', link: tutorialPath + '03共享数据', }, 21 | { text: '同步操作', link: tutorialPath + '04同步操作', }, 22 | { text: '内存模型与原子操作', link: tutorialPath + '05内存模型与原子操作', }, 23 | { text: '协程', link: tutorialPath + '06协程', }, 24 | { 25 | text: '详细分析', link: srcCodePath, 26 | collapsible: true, 27 | children: [ 28 | { text: 'std::thread 的构造-源码解析', link: srcCodePath + '01thread的构造与源码解析', }, 29 | { text: 'std::scoped_lock 的源码实现与解析', link: srcCodePath + '02scoped_lock源码解析', }, 30 | { text: 'std::async 与 std::future 源码解析', link: srcCodePath + '03async与future源码解析', }, 31 | { text: '线程池', link: srcCodePath + "04线程池", }, 32 | ] 33 | }, 34 | ], 35 | favicon: logoPath, 36 | logo: logoPath, 37 | navTitle: repoName, 38 | repo: repoUrl, 39 | editLinkPattern: repoUrl + 'blob/main/:path', 40 | contributors: false, 41 | darkmode: 'toggle', 42 | pageInfo: ['ReadingTime'], 43 | plugins: { 44 | mdEnhance: { 45 | gfm: true, 46 | hint: true, 47 | vPre: true, 48 | alert: true, 49 | tabs: true, 50 | codetabs: true, 51 | align: true, 52 | attrs: true, 53 | sup: true, 54 | sub: true, 55 | footnote: true, 56 | mark: true, 57 | figure: true, 58 | imgLazyload: true, 59 | imgMark: true, 60 | imgSize: true, 61 | obsidianImgSize: true, 62 | tasklist: true, 63 | include: true, 64 | katex: true, 65 | component: true, 66 | chart: true, 67 | echarts: true, 68 | flowchart: true, 69 | mermaid: true, 70 | plantuml: true, 71 | }, 72 | searchPro: true 73 | } 74 | }), 75 | bundler: viteBundler({}), 76 | }) 77 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/test/AduioPlayer.h: -------------------------------------------------------------------------------- 1 | #ifndef AUDIOPLAYER_H 2 | #define AUDIOPLAYER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | using namespace std::chrono_literals; 14 | 15 | class AudioPlayer { 16 | public: 17 | AudioPlayer() : stop{ false }, player_thread{ &AudioPlayer::playMusic, this } 18 | {} 19 | 20 | ~AudioPlayer() { 21 | // 等待队列中所有音乐播放完毕 22 | while (!audio_queue.empty()) { 23 | std::this_thread::sleep_for(50ms); 24 | } 25 | stop = true; 26 | cond.notify_all(); 27 | if (player_thread.joinable()) { 28 | player_thread.join(); 29 | } 30 | } 31 | 32 | void addAudioPath(const std::string& path) { 33 | std::lock_guard lock{ mtx }; // 互斥量确保了同一时间不会有其它地方在操作共享资源(队列) 34 | audio_queue.push(path); // 为队列添加元素 表示有新的提示音需要播放 35 | cond.notify_one(); // 通知线程新的音频 36 | } 37 | 38 | private: 39 | void playMusic() { 40 | while (!stop) { 41 | std::string path; 42 | { 43 | std::unique_lock lock{ mtx }; 44 | cond.wait(lock, [this] { return !audio_queue.empty() || stop; }); 45 | 46 | if (audio_queue.empty()) return; // 防止在对象为空时析构出错 47 | 48 | path = audio_queue.front(); // 从队列中取出元素 49 | audio_queue.pop(); // 取出后就删除元素,表示此元素已被使用 50 | } 51 | 52 | if (!music.openFromFile(path)) { 53 | std::cerr << "无法加载音频文件: " << path << std::endl; 54 | continue; // 继续播放下一个音频 55 | } 56 | 57 | music.play(); 58 | 59 | // 等待音频播放完毕 60 | while (music.getStatus() == sf::SoundSource::Playing) { 61 | sf::sleep(sf::seconds(0.1f)); // sleep 避免忙等占用 CPU 62 | } 63 | } 64 | } 65 | 66 | std::atomic stop; // 控制线程的停止与退出, 67 | std::thread player_thread; // 后台执行音频任务的专用线程 68 | std::mutex mtx; // 保护共享资源 69 | std::condition_variable cond; // 控制线程等待和唤醒,当有新任务时通知音频线程 70 | std::queue audio_queue; // 音频任务队列,存储待播放的音频文件路径 71 | sf::Music music; // SFML 音频播放器,用于加载和播放音频文件 72 | 73 | public: 74 | static constexpr std::array soundResources{ 75 | "./sound/01初始化失败.ogg", 76 | "./sound/02初始化成功.ogg", 77 | "./sound/03试剂不足,请添加.ogg", 78 | "./sound/04试剂已失效,请更新.ogg", 79 | "./sound/05清洗液不足,请添加.ogg", 80 | "./sound/06废液桶即将装满,请及时清空.ogg", 81 | "./sound/07废料箱即将装满,请及时清空.ogg", 82 | "./sound/08激发液A液不足,请添加.ogg", 83 | "./sound/09激发液B液不足,请添加.ogg", 84 | "./sound/10反应杯不足,请添加.ogg", 85 | "./sound/11检测全部完成.ogg" 86 | }; 87 | enum SoundIndex { 88 | InitializationFailed, 89 | InitializationSuccessful, 90 | ReagentInsufficient, 91 | ReagentExpired, 92 | CleaningAgentInsufficient, 93 | WasteBinAlmostFull, 94 | WasteContainerAlmostFull, 95 | LiquidAInsufficient, 96 | LiquidBInsufficient, 97 | ReactionCupInsufficient, 98 | DetectionCompleted, 99 | SoundCount // 总音频数量,用于计数 100 | }; 101 | }; 102 | 103 | #endif // AUDIOPLAYER_H 104 | -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/26使用条件变量实现后台提示音播放.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | using namespace std::chrono_literals; 10 | 11 | class AudioPlayer{ 12 | public: 13 | AudioPlayer() : stop {false}, player_thread{ &AudioPlayer::playMusic, this } 14 | {} 15 | 16 | ~AudioPlayer(){ 17 | while (!audio_queue.empty()){ 18 | std::this_thread::sleep_for(50ms); 19 | } 20 | stop = true; 21 | cond.notify_all(); 22 | if(player_thread.joinable()){ 23 | player_thread.join(); 24 | } 25 | } 26 | 27 | void addAudioPath(const std::string& path){ 28 | std::lock_guard lc{ m }; 29 | audio_queue.push(path); 30 | cond.notify_one(); 31 | } 32 | 33 | private: 34 | void playMusic(){ 35 | while(!stop){ 36 | std::string path; 37 | { 38 | std::unique_lock lock{ m }; 39 | // 条件不满足,就解锁 unlock,让其它线程得以运行 如果被唤醒了,就会重新获取锁 lock 40 | cond.wait(lock, [this] {return !audio_queue.empty() || stop; }); 41 | 42 | if (audio_queue.empty()) return; // 防止对象为空时出问题 43 | 44 | path = audio_queue.front(); // 取出 45 | audio_queue.pop(); // 取出后就删除这个元素,表示此元素以及被使用 46 | } 47 | 48 | if(!music.openFromFile(path)){ 49 | std::cerr << "无法加载音频文件: " << path << std::endl; 50 | continue; 51 | } 52 | 53 | music.play(); // 异步 非阻塞 54 | 55 | while(music.getStatus() == sf::SoundSource::Playing){ 56 | sf::sleep(sf::seconds(0.1f)); // sleep 避免忙等 占用 CPU 57 | } 58 | } 59 | } 60 | 61 | std::atomic stop; // 控制线程的停止与退出 62 | std::thread player_thread; // 后台执行音频播放任务的专用线程 63 | std::mutex m; // 保护共享资源 64 | std::condition_variable cond; // 控制线程的等待和唤醒,当有新的任务的时候通知播放线程 65 | std::queue audio_queue; // 音频任务队列,存储待播放的音频文件的路径 66 | sf::Music music; // SFML 音频播放器对象,用来加载播放音频 67 | 68 | public: 69 | static constexpr std::array soundResources{ 70 | "./sound/01初始化失败.ogg", 71 | "./sound/02初始化成功.ogg", 72 | "./sound/03试剂不足,请添加.ogg", 73 | "./sound/04试剂已失效,请更新.ogg", 74 | "./sound/05清洗液不足,请添加.ogg", 75 | "./sound/06废液桶即将装满,请及时清空.ogg", 76 | "./sound/07废料箱即将装满,请及时清空.ogg", 77 | "./sound/08激发液A液不足,请添加.ogg", 78 | "./sound/09激发液B液不足,请添加.ogg", 79 | "./sound/10反应杯不足,请添加.ogg", 80 | "./sound/11检测全部完成.ogg" 81 | }; 82 | }; 83 | 84 | AudioPlayer audioPlayer; 85 | 86 | int main() { 87 | audioPlayer.addAudioPath(AudioPlayer::soundResources[4]); 88 | audioPlayer.addAudioPath(AudioPlayer::soundResources[5]); 89 | audioPlayer.addAudioPath(AudioPlayer::soundResources[6]); 90 | audioPlayer.addAudioPath(AudioPlayer::soundResources[7]); 91 | 92 | std::thread t{ [] { 93 | std::this_thread::sleep_for(1s); 94 | audioPlayer.addAudioPath(AudioPlayer::soundResources[1]); 95 | } }; 96 | std::thread t2{ [] { 97 | audioPlayer.addAudioPath(AudioPlayer::soundResources[0]); 98 | } }; 99 | 100 | std::cout << "乐\n"; 101 | 102 | t.join(); 103 | t2.join(); 104 | 105 | std::cout << "end\n"; 106 | } -------------------------------------------------------------------------------- /code/ModernCpp-ConcurrentProgramming-Tutorial/41实现一个线程池.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | using namespace std::chrono_literals; 13 | 14 | inline std::size_t default_thread_pool_size() noexcept{ 15 | std::size_t num_threads = std::thread::hardware_concurrency(); 16 | num_threads = num_threads == 0 ? 2 : num_threads; // 防止无法检测当前硬件,让我们线程池至少有 2 个线程 17 | return num_threads; 18 | } 19 | 20 | class ThreadPool{ 21 | public: 22 | using Task = std::packaged_task; 23 | 24 | ThreadPool(const ThreadPool&) = delete; 25 | ThreadPool& operator=(const ThreadPool&) = delete; 26 | 27 | ThreadPool(std::size_t num_thread = default_thread_pool_size()) : 28 | stop_{ false }, num_thread_{ num_thread } 29 | { 30 | start(); 31 | } 32 | ~ThreadPool(){ 33 | stop(); 34 | } 35 | 36 | void stop(){ 37 | stop_ = true; 38 | cv_.notify_all(); 39 | for (auto& thread : pool_){ 40 | if (thread.joinable()) 41 | thread.join(); 42 | } 43 | pool_.clear(); 44 | } 45 | 46 | template 47 | std::future, std::decay_t...>> submit(F&& f, Args&&...args){ 48 | using RetType = std::invoke_result_t, std::decay_t...>; 49 | if(stop_){ 50 | throw std::runtime_error("ThreadPool is stopped"); 51 | } 52 | auto task = std::make_shared>(std::bind(std::forward(f), std::forward(args)...)); 53 | 54 | std::future ret = task->get_future(); 55 | 56 | { 57 | std::lock_guard lc{ mutex_ }; 58 | tasks_.emplace([task] {(*task)(); }); 59 | } 60 | cv_.notify_one(); 61 | 62 | return ret; 63 | } 64 | 65 | void start(){ 66 | for (std::size_t i = 0; i < num_thread_; ++i){ 67 | pool_.emplace_back([this]{ 68 | while (!stop_) { 69 | Task task; 70 | { 71 | std::unique_lock lock{ mutex_ }; 72 | cv_.wait(lock, [this] {return stop_ || !tasks_.empty(); }); 73 | if (tasks_.empty()) return; 74 | task = std::move(tasks_.front()); 75 | tasks_.pop(); 76 | } 77 | task(); 78 | } 79 | }); 80 | } 81 | } 82 | 83 | private: 84 | std::mutex mutex_; 85 | std::condition_variable cv_; 86 | std::atomic stop_; 87 | std::atomic num_thread_; 88 | std::queue tasks_; 89 | std::vector pool_; 90 | }; 91 | 92 | int print_task(int n) { 93 | std::osyncstream{ std::cout } << "Task " << n << " is running on thr: " << 94 | std::this_thread::get_id() << '\n'; 95 | return n; 96 | } 97 | int print_task2(int n) { 98 | std::osyncstream{ std::cout } << "🐢🐢🐢 " << n << " 🐉🐉🐉" << std::endl; 99 | return n; 100 | } 101 | 102 | struct X { 103 | void f(const int& n) const { 104 | std::osyncstream{ std::cout } << &n << '\n'; 105 | } 106 | }; 107 | 108 | int main() { 109 | ThreadPool pool{ 4 }; // 创建一个有 4 个线程的线程池 110 | 111 | X x; 112 | int n = 6; 113 | std::cout << &n << '\n'; 114 | auto t = pool.submit(&X::f, &x, n); // 默认复制,地址不同 115 | auto t2 = pool.submit(&X::f, &x, std::ref(n)); 116 | t.wait(); 117 | t2.wait(); 118 | } // 析构自动 stop()自动 stop() -------------------------------------------------------------------------------- /md/详细分析/02scoped_lock源码解析.md: -------------------------------------------------------------------------------- 1 | # `std::scoped_lock` 的源码实现与解析 2 | 3 | 本单章专门介绍标准库在 C++17 引入的类模板 `std::scoped_lock` 的实现,让你对它再无疑问。 4 | 5 | 这会涉及到不少的模板技术,这没办法,就如同我们先前聊 [`std::thread` 的构造与源码分析](01thread的构造与源码解析.md)最后说的:“**不会模板,你阅读标准库源码,是无稽之谈**”。建议学习[现代C++模板教程](https://mq-b.github.io/Modern-Cpp-templates-tutorial/)。 6 | 7 | 我们还是一样的,以 MSVC STL 实现的 [`std::scoped_lock`](https://github.com/microsoft/STL/blob/8e2d724cc1072b4052b14d8c5f81a830b8f1d8cb/stl/inc/mutex#L476-L528) 代码进行讲解,不用担心,我们也查看了 [`libstdc++`](https://github.com/gcc-mirror/gcc/blob/7a01cc711f33530436712a5bfd18f8457a68ea1f/libstdc%2B%2B-v3/include/std/mutex#L743-L802) 、[`libc++`](https://github.com/llvm/llvm-project/blob/7ac7d418ac2b16fd44789dcf48e2b5d73de3e715/libcxx/include/mutex#L424-L488)的实现,并没有太多区别,更多的是一些风格上的。而且个人觉得 MSVC 的实现是最简单直观的。 8 | 9 | ## `std::scoped_lock` 的数据成员 10 | 11 | `std::scoped_lock` 是一个类模板,它有两个特化,也就是有三个版本,其中的数据成员也是不同的。并且它们都不可移动不可复制,“*管理类*”应该如此。 12 | 13 | 1. 主模板,是一个可变参数类模板,声明了一个类型形参包 `_Mutexes`,**存储了一个 `std::tuple`**,具体类型根据类型形参包决定。 14 | 15 | ```cpp 16 | _EXPORT_STD template 17 | class _NODISCARD_LOCK scoped_lock { // class with destructor that unlocks mutexes 18 | public: 19 | explicit scoped_lock(_Mutexes&... _Mtxes) : _MyMutexes(_Mtxes...) { // construct and lock 20 | _STD lock(_Mtxes...); 21 | } 22 | 23 | explicit scoped_lock(adopt_lock_t, _Mutexes&... _Mtxes) noexcept // strengthened 24 | : _MyMutexes(_Mtxes...) {} // construct but don't lock 25 | 26 | ~scoped_lock() noexcept { 27 | _STD apply([](_Mutexes&... _Mtxes) { (..., (void) _Mtxes.unlock()); }, _MyMutexes); 28 | } 29 | 30 | scoped_lock(const scoped_lock&) = delete; 31 | scoped_lock& operator=(const scoped_lock&) = delete; 32 | 33 | private: 34 | tuple<_Mutexes&...> _MyMutexes; 35 | }; 36 | ``` 37 | 38 | 2. 对模板类型形参包只有一个类型情况的**偏特化**,是不是很熟悉,和 `lock_guard` 几乎没有任何区别,**保有一个互斥量的引用**,构造上锁,析构解锁,提供一个额外的构造函数让构造的时候不上锁。所以用 `scoped_lock` 替代 `lock_guard` 不会造成任何额外开销。 39 | 40 | ```cpp 41 | template 42 | class _NODISCARD_LOCK scoped_lock<_Mutex> { 43 | public: 44 | using mutex_type = _Mutex; 45 | 46 | explicit scoped_lock(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock 47 | _MyMutex.lock(); 48 | } 49 | 50 | explicit scoped_lock(adopt_lock_t, _Mutex& _Mtx) noexcept // strengthened 51 | : _MyMutex(_Mtx) {} // construct but don't lock 52 | 53 | ~scoped_lock() noexcept { 54 | _MyMutex.unlock(); 55 | } 56 | 57 | scoped_lock(const scoped_lock&) = delete; 58 | scoped_lock& operator=(const scoped_lock&) = delete; 59 | 60 | private: 61 | _Mutex& _MyMutex; 62 | }; 63 | ``` 64 | 65 | 3. 对类型形参包为空的情况的全特化,**没有数据成员**。 66 | 67 | ```cpp 68 | template <> 69 | class scoped_lock<> { 70 | public: 71 | explicit scoped_lock() = default; 72 | explicit scoped_lock(adopt_lock_t) noexcept /* strengthened */ {} 73 | 74 | scoped_lock(const scoped_lock&) = delete; 75 | scoped_lock& operator=(const scoped_lock&) = delete; 76 | }; 77 | ``` 78 | 79 | --- 80 | 81 | ```cpp 82 | std::mutex m1,m2; 83 | 84 | std::scoped_lock lc{ m1 }; // 匹配到偏特化版本 保有一个 std::mutex& 85 | std::scoped_lock lc2{ m1,m2 }; // 匹配到主模板 保有一个 std::tuple 86 | std::scoped_lock<> lc3; // 匹配到全特化版本 空 87 | ``` 88 | 89 | ## `std::scoped_lock`的构造与析构 90 | 91 | 在上一节讲 `scoped_lock` 的数据成员的时候已经把这个模板类的全部源码,三个版本的代码都展示了,就不再重复。 92 | 93 | 这三个版本中,**只有两个版本需要介绍**,也就是 94 | 95 | 1. 形参包元素数量为一的偏特化,只管理一个互斥量的。 96 | 2. 主模板,可以管理任意个数的互斥量。 97 | 98 | 那这两个的共同点是什么呢?***构造上锁,析构解锁***。这很明显,明确这一点我们就开始讲吧。 99 | 100 | --- 101 | 102 | ```cpp 103 | std::mutex m; 104 | void f(){ 105 | m.lock(); 106 | std::lock_guard lc{ m, std::adopt_lock }; 107 | } 108 | void f2(){ 109 | m.lock(); 110 | std::scoped_locksp{ std::adopt_lock,m }; 111 | } 112 | ``` 113 | 114 | 这段代码为你展示了 `std::lock_guard` 和 `std::scoped_lock` 形参包元素数量为一的偏特化的唯一区别:**调用不会上锁的构造函数的参数顺序不同**。那么到此也就够了。 115 | 116 | 接下来我们进入 `std::scoped_lock` 主模板的讲解: 117 | 118 | ```cpp 119 | explicit scoped_lock(_Mutexes&... _Mtxes) : _MyMutexes(_Mtxes...) { // construct and lock 120 | _STD lock(_Mtxes...); 121 | } 122 | ``` 123 | 124 | 这个构造函数做了两件事情,初始化数据成员 `_MyMutexes`让它保有这些互斥量的引用,以及给所有互斥量上锁,使用了 [`std::lock`](https://zh.cppreference.com/w/cpp/thread/lock) 帮助我们完成这件事情。 125 | 126 | ```cpp 127 | explicit scoped_lock(adopt_lock_t, _Mutexes&... _Mtxes) noexcept // strengthened 128 | : _MyMutexes(_Mtxes...) {} // construct but don't lock 129 | ``` 130 | 131 | 这个构造函数不上锁,只是初始化数据成员 `_MyMutexes`让它保有这些互斥量的引用。 132 | 133 | ```cpp 134 | ~scoped_lock() noexcept { 135 | _STD apply([](_Mutexes&... _Mtxes) { (..., (void) _Mtxes.unlock()); }, _MyMutexes); 136 | } 137 | ``` 138 | 139 | 析构函数就要稍微聊一下了,主要是用 [`std::apply`](https://zh.cppreference.com/w/cpp/utility/apply) 去遍历 [`std::tuple`](https://zh.cppreference.com/w/cpp/utility/tuple) ,让元组保有的互斥量引用都进行解锁。简单来说是 `std::apply` 可以将元组存储的参数全部拿出,用于调用这个可变参数的可调用对象,我们就能利用折叠表达式展开形参包并对其调用 `unlock()`。 140 | 141 | > 不在乎其返回类型只用来实施它的副作用,显式转换为 `(void)` 也就是[*弃值表达式*](https://zh.cppreference.com/w/cpp/language/expressions#.E5.BC.83.E5.80.BC.E8.A1.A8.E8.BE.BE.E5.BC.8F)。在我们之前讲的 `std::thread` 源码中也有这种[用法](https://github.com/microsoft/STL/blob/8e2d724cc1072b4052b14d8c5f81a830b8f1d8cb/stl/inc/thread#L82)。 142 | > 143 | > 不过你可能有疑问:“我们的标准库的那些[互斥量](https://zh.cppreference.com/w/cpp/thread#.E4.BA.92.E6.96.A5) `unlock()` 返回类型都是 `void` 呀,为什么要这样?” 144 | > 145 | > 的确,这是个好问题,[libstdc++](https://github.com/gcc-mirror/gcc/blob/7a01cc711f33530436712a5bfd18f8457a68ea1f/libstdc%2B%2B-v3/include/std/mutex#L757-L758) 和 [libc++](https://github.com/llvm/llvm-project/blob/7ac7d418ac2b16fd44789dcf48e2b5d73de3e715/libcxx/include/mutex#L472-L475) 都没这样做,或许 MSVC STL 想着会有人设计的互斥量让它的 `unlock()` 返回类型不为 `void`,毕竟 [*互斥体* *(Mutex)*](https://zh.cppreference.com/w/cpp/named_req/Mutex) 没有要求 `unlock()` 的返回类型。 146 | 147 | --- 148 | 149 | ```cpp 150 | template< class F, class Tuple > 151 | constexpr decltype(auto) apply( F&& f, Tuple&& t ); 152 | ``` 153 | 154 | 这个函数模板接受两个参数,一个[*可调用* *(Callable)*](https://zh.cppreference.com/w/cpp/named_req/Callable)对象 f,以及一个元组 t,用做调用 f 。我们可以自己简单实现一下它,其实不算难,这种遍历元组的方式在之前讲 `std::thread` 的源码的时候也提到过。 155 | 156 | ```cpp 157 | template 158 | constexpr decltype(auto) Apply_impl(Callable&& obj,Tuple&& tuple,std::index_sequence){ 159 | return std::invoke(std::forward(obj), std::get(std::forward(tuple))...); 160 | } 161 | 162 | template 163 | constexpr decltype(auto) apply(Callable&& obj, Tuple&& tuple){ 164 | return Apply_impl(std::forward(obj), std::forward(tuple), 165 | std::make_index_sequence>>{}); 166 | } 167 | ``` 168 | 169 | 其实就是把元组给解包了,利用了 `std::index_sequence` + `std::make_index_sequence` 然后就用 `std::get` 形参包展开用 `std::invoke` 调用可调用对象即可,**非常经典的处理可变参数做法**,这个非常重要,一定要会使用。 170 | 171 | 举一个简单的调用例子: 172 | 173 | ```cpp 174 | std::tuple tuple{ 66,"😅",'c' }; 175 | ::apply([](const auto&... t) { ((std::cout << t << ' '), ...); }, tuple); 176 | ``` 177 | 178 | > [运行测试](https://godbolt.org/z/n4aKo4xbr)。 179 | 180 | 使用了[折叠表达式](https://zh.cppreference.com/w/cpp/language/fold)展开形参包,打印了元组所有的元素。 181 | 182 | ## 总结 183 | 184 | **如你所见,其实这很简单**。至少使用与了解其设计原理是很简单的。唯一的难度或许只有那点源码,处理可变参数,这会涉及不少模板技术,既常见也通用。还是那句话:“***不会模板,你阅读标准库源码,是无稽之谈***”。 185 | 186 | 相对于 `std::thread` 的源码解析,`std::scoped_lock` 还是简单的多。 187 | -------------------------------------------------------------------------------- /md/详细分析/01thread的构造与源码解析.md: -------------------------------------------------------------------------------- 1 | # `std::thread` 的构造-源码解析 2 | 3 | 我们这单章是为了专门解释一下 C++11 引入的 `std::thread` 是如何构造的,是如何创建线程传递参数的,让你彻底了解这个类。 4 | 5 | 我们以 **MSVC** 实现的 [`std::thread`](https://github.com/microsoft/STL/blob/8e2d724cc1072b4052b14d8c5f81a830b8f1d8cb/stl/inc/thread) 代码进行讲解,MSVC STL 很早之前就不支持 C++11 了,它的实现完全基于 **C++14**,出于某些原因 **C++17** 的一些库(如 [`invoke`](https://zh.cppreference.com/w/cpp/utility/functional/invoke), _v 变量模板)被向后移植到了 **C++14** 模式,所以即使是 C++11 标准库设施,实现中可能也是使用到了 C++14、17 的东西。 6 | 7 | ## `std::thread` 的数据成员 8 | 9 | - **了解一个庞大的类,最简单的方式就是先看它的数据成员有什么**。 10 | 11 | `std::thread` 只保有一个私有数据成员 [`_Thr`](https://github.com/microsoft/STL/blob/8e2d724cc1072b4052b14d8c5f81a830b8f1d8cb/stl/inc/thread#L163): 12 | 13 | ```cpp 14 | private: 15 | _Thrd_t _Thr; 16 | ``` 17 | 18 | [`_Thrd_t`](https://github.com/microsoft/STL/blob/8e2d724cc1072b4052b14d8c5f81a830b8f1d8cb/stl/inc/__msvc_threads_core.hpp#L20-L24) 是一个结构体,它保有两个数据成员: 19 | 20 | ```cpp 21 | using _Thrd_id_t = unsigned int; 22 | struct _Thrd_t { // thread identifier for Win32 23 | void* _Hnd; // Win32 HANDLE 24 | _Thrd_id_t _Id; 25 | }; 26 | ``` 27 | 28 | 结构很明确,这个结构体的 `_Hnd` 成员是指向线程的句柄,`_Id` 成员就是保有线程的 ID。 29 | 30 | 在64 位操作系统,因为内存对齐,指针 8 ,无符号 int 4,这个结构体 `_Thrd_t` 就是占据 16 个字节。也就是说 `sizeof(std::thread)` 的结果应该为 **16**。 31 | 32 | ## `std::thread` 的构造函数 33 | 34 | `std::thread` 有四个[构造函数](https://zh.cppreference.com/w/cpp/thread/thread/thread),分别是: 35 | 36 | 1. 默认构造函数,构造不关联线程的新 std::thread 对象。 37 | 38 | ```cpp 39 | thread() noexcept : _Thr{} {} 40 | ``` 41 | 42 | [值初始化](https://zh.cppreference.com/w/cpp/language/value_initialization#:~:text=%E5%87%BD%E6%95%B0%E7%9A%84%E7%B1%BB%EF%BC%89%EF%BC%8C-,%E9%82%A3%E4%B9%88%E9%9B%B6%E5%88%9D%E5%A7%8B%E5%8C%96%E5%AF%B9%E8%B1%A1,-%EF%BC%8C%E7%84%B6%E5%90%8E%E5%A6%82%E6%9E%9C%E5%AE%83)了数据成员 _Thr ,这里的效果相当于给其成员 `_Hnd` 和 `_Id` 都进行[零初始化](https://zh.cppreference.com/w/cpp/language/zero_initialization)。 43 | 44 | 2. 移动构造函数,转移线程的所有权,构造 other 关联的执行线程的 `std::thread` 对象。此调用后 other 不再表示执行线程失去了线程的所有权。 45 | 46 | ```cpp 47 | thread(thread&& _Other) noexcept : _Thr(_STD exchange(_Other._Thr, {})) {} 48 | ``` 49 | 50 | [_STD](https://github.com/microsoft/STL/blob/8e2d724cc1072b4052b14d8c5f81a830b8f1d8cb/stl/inc/yvals_core.h#L1934) 是一个宏,展开就是 `::std::`,也就是 [`::std::exchange`](https://zh.cppreference.com/w/cpp/utility/exchange),将 `_Other._Thr` 赋为 `{}` (也就是置空),返回 `_Other._Thr` 的旧值用以初始化当前对象的数据成员 `_Thr`。 51 | 52 | 3. 复制构造函数被定义为弃置的,std::thread 不可复制。两个 std::thread 不可表示一个线程,std::thread 对线程资源是独占所有权。 53 | 54 | ```cpp 55 | thread(const thread&) = delete; 56 | ``` 57 | 58 | 4. 构造新的 `std::thread` 对象并将它与执行线程关联。**表示新的执行线程开始执行**。 59 | 60 | ```cpp 61 | template , thread>, int> = 0> 62 | _NODISCARD_CTOR_THREAD explicit thread(_Fn&& _Fx, _Args&&... _Ax) { 63 | _Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...); 64 | } 65 | ``` 66 | 67 | --- 68 | 69 | 前三个构造函数都没啥要特别聊的,非常简单,只有第四个构造函数较为复杂,且是我们本章重点,需要详细讲解。(*注意 MSVC 使用标准库的内容很多时候不加 **std::**,脑补一下就行*) 70 | 71 | 如你所见,这个构造函数本身并没有做什么,它只是一个可变参数成员函数模板,增加了一些 [SFINAE](https://zh.cppreference.com/w/cpp/language/sfinae) 进行约束我们传入的[可调用](https://zh.cppreference.com/w/cpp/named_req/Callable)对象的类型不能是 `std::thread`。关于这个约束你可能有问题,因为 `std::thread` 他并没有 `operator()` 的重载,不是可调用类型,这个 `enable_if_t` 的意义是什么呢?其实很简单,如下: 72 | 73 | ```cpp 74 | struct X{ 75 | X(X&& x)noexcept{} 76 | template 77 | X(Fn&& f,Args&&...args){} 78 | X(const X&) = delete; 79 | }; 80 | 81 | X x{ [] {} }; 82 | X x2{ x }; // 选择到了有参构造函数,不导致编译错误 83 | ``` 84 | 85 | 以上这段代码可以正常的[通过编译](https://godbolt.org/z/6zhW6xjqP)。这是重载决议的事情,我们知道,`std::thread` 是不可复制的,这种代码自然不应该让它通过编译,选择到我们的有参构造,所以我们添加一个约束让其不能选择到我们的有参构造: 86 | 87 | ```cpp 88 | template , X>, int> = 0> 89 | ``` 90 | 91 | 这样,这段代码就会正常的出现[编译错误](https://godbolt.org/z/Mc1h1GcdT),信息如下: 92 | 93 | ```txt 94 | error C2280: “X::X(const X &)”: 尝试引用已删除的函数 95 | note: 参见“X::X”的声明 96 | note: “X::X(const X &)”: 已隐式删除函数 97 | ``` 98 | 99 | 也就满足了我们的要求,重载决议选择到了弃置复制构造函数产生编译错误,这也就是源码中添加约束的目的。 100 | 101 | 而构造函数体中调用了一个函数 [**`_Start`**](https://github.com/microsoft/STL/blob/8e2d724cc1072b4052b14d8c5f81a830b8f1d8cb/stl/inc/thread#L72-L87),将我们构造函数的参数全部完美转发,去调用它,这个函数才是我们的重点,如下: 102 | 103 | ```cpp 104 | template 105 | void _Start(_Fn&& _Fx, _Args&&... _Ax) { 106 | using _Tuple = tuple, decay_t<_Args>...>; 107 | auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...); 108 | constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{}); 109 | 110 | _Thr._Hnd = 111 | reinterpret_cast(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id)); 112 | 113 | if (_Thr._Hnd) { // ownership transferred to the thread 114 | (void) _Decay_copied.release(); 115 | } else { // failed to start thread 116 | _Thr._Id = 0; 117 | _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN); 118 | } 119 | } 120 | ``` 121 | 122 | 1. 它也是一个可变参数成员函数模板,接受一个可调用对象 `_Fn` 和一系列参数 `_Args...` ,这些东西用来创建一个线程。 123 | 124 | 2. `using _Tuple = tuple, decay_t<_Args>...>` 125 | 126 | - 定义了一个[元组](https://zh.cppreference.com/w/cpp/utility/tuple)类型 `_Tuple` ,它包含了可调用对象和参数的类型,这里使用了 [`decay_t`](https://zh.cppreference.com/w/cpp/types/decay) 来去除了类型的引用和 cv 限定。 127 | 128 | 3. `auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...)` 129 | 130 | - 使用 [`make_unique`](https://zh.cppreference.com/w/cpp/memory/unique_ptr/make_unique) 创建了一个独占指针,指向的是 `_Tuple` 类型的对象,**存储了传入的函数对象和参数的副本**。 131 | 132 | 4. `constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{})` 133 | 134 | - 调用 [`_Get_invoke`](https://github.com/microsoft/STL/blob/8e2d724cc1072b4052b14d8c5f81a830b8f1d8cb/stl/inc/thread#L65-L68) 函数,传入 `_Tuple` 类型和一个参数序列的索引序列(为了遍历形参包)。这个函数用于获取一个函数指针,指向了一个静态成员函数 [`_Invoke`](https://github.com/microsoft/STL/blob/8e2d724cc1072b4052b14d8c5f81a830b8f1d8cb/stl/inc/thread#L55-L63),它是线程实际执行的函数。这两个函数都非常的简单,我们来看看: 135 | 136 | ```cpp 137 | template 138 | _NODISCARD static constexpr auto _Get_invoke(index_sequence<_Indices...>) noexcept { 139 | return &_Invoke<_Tuple, _Indices...>; 140 | } 141 | 142 | template 143 | static unsigned int __stdcall _Invoke(void* _RawVals) noexcept /* terminates */ { 144 | // adapt invoke of user's callable object to _beginthreadex's thread procedure 145 | const unique_ptr<_Tuple> _FnVals(static_cast<_Tuple*>(_RawVals)); 146 | _Tuple& _Tup = *_FnVals.get(); // avoid ADL, handle incomplete types 147 | _STD invoke(_STD move(_STD get<_Indices>(_Tup))...); 148 | _Cnd_do_broadcast_at_thread_exit(); // TRANSITION, ABI 149 | return 0; 150 | } 151 | ``` 152 | 153 | _Get_invoke 函数很简单,就是接受一个元组类型,和形参包的索引,传递给 _Invoke 静态成员函数模板,实例化,获取它的函数指针。 154 | 155 | > 它的形参类型我们不再过多介绍,你只需要知道 [`index_sequence`](https://en.cppreference.com/w/cpp/utility/integer_sequence) 这个东西可以用来接收一个由 `make_index_sequence` 创建的索引形参包,帮助我们进行遍历元组即可。[示例代码](https://godbolt.org/z/dv88aPGac)。 156 | 157 | **_Invoke 是重中之重,它是线程实际执行的函数**,如你所见它的形参类型是 `void*` ,这是必须的,要符合 `_beginthreadex` 执行函数的类型要求。虽然是 `void*`,但是我可以将它转换为 `_Tuple*` 类型,构造一个独占智能指针,然后调用 get() 成员函数获取底层指针,解引用指针,得到元组的引用初始化`_Tup` 。 158 | 159 | 此时,我们就可以进行调用了,使用 [`std::invoke`](https://zh.cppreference.com/w/cpp/utility/functional/invoke) + `std::move`(默认移动) ,这里有一个形参包展开,`_STD get<_Indices>(_Tup))...`,_Tup 就是 std::tuple 的引用,我们使用 `std::get<>` 获取元组存储的数据,需要传入一个索引,这里就用到了 `_Indices`。展开之后,就等于 invoke 就接受了我们构造 std::thread 传入的可调用对象,调用可调用对象的参数,invoke 就可以执行了。 160 | 161 | 5. `_Thr._Hnd = reinterpret_cast(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id))` 162 | 163 | - 调用 [`_beginthreadex`](https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/beginthread-beginthreadex?view=msvc-170) 函数来启动一个线程,并将线程句柄存储到 `_Thr._Hnd` 中。传递给线程的参数为 `_Invoker_proc`(一个静态函数指针,就是我们前面讲的 **_Invoke**)和 `_Decay_copied.get()`(存储了函数对象和参数的副本的指针)。 164 | 165 | 6. `if (_Thr._Hnd) {` 166 | 167 | - 如果线程句柄 `_Thr._Hnd` 不为空,则表示线程已成功启动,将独占指针的所有权转移给线程。 168 | 169 | 7. `(void) _Decay_copied.release()` 170 | 171 | - 释放独占指针的所有权,因为已经将参数传递给了线程。 172 | 173 | 8. `} else { // failed to start thread` 174 | 175 | - 如果线程启动失败,则进入这个分支 176 | 177 | 9. `_Thr._Id = 0;` 178 | 179 | - 将线程ID设置为0。 180 | 181 | 10. `_Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);` 182 | 183 | - 抛出一个 C++ 错误,表示资源不可用,请再次尝试。 184 | 185 | ## 总结 186 | 187 | 需要注意,libstdc++ 和 libc++ 可能不同,就比如它们 64 位环境下 `sizeof(std::thread)` 的结果就可能是 **8**。libstdc++ 的实现只[保有一个 `std::thread::id`](https://github.com/gcc-mirror/gcc/blob/3e3d115c946944c81d8231dfbe778d4dae26cbb7/libstdc%2B%2B-v3/include/bits/std_thread.h#L123)。[参见](https://github.com/gcc-mirror/gcc/blob/3e3d115c946944c81d8231dfbe778d4dae26cbb7/libstdc%2B%2B-v3/include/bits/std_thread.h#L81-L85)。不过实测 gcc 不管是 `win32` 还是 `POSIX` 线程模型,线程对象的大小都是 8,宏 `_GLIBCXX_HAS_GTHREADS` 的值都为 1([GThread](https://docs.gtk.org/glib/struct.Thread.html))。 188 | 189 | > ```cpp 190 | > class thread 191 | > { 192 | > public: 193 | > #ifdef _GLIBCXX_HAS_GTHREADS 194 | > using native_handle_type = __gthread_t; 195 | > #else 196 | > using native_handle_type = int; 197 | > #endif 198 | > ``` 199 | > 200 | > `__gthread_t` 即 `void*`。 201 | 202 | 我们这里的源码解析涉及到的 C++ 技术很多,我们也没办法每一个都单独讲,那会显得文章很冗长,而且也不是重点。 203 | 204 | 相信你也感受到了,**不会模板,你阅读标准库源码,是无稽之谈**,市面上很多教程教学,教导一些实现容器,过度简化了,真要去出错了去看标准库的代码,那是不现实的。不需要模板的水平有多高,也不需要会什么元编程,但是基本的需求得能做到,得会,这里推荐一下:[**现代C++模板教程**](https://github.com/Mq-b/Modern-Cpp-templates-tutorial)。 205 | 206 | --- 207 | 208 | 学习完了也不要忘记了回答最初的问题: 209 | 210 | 1. **如何做到的默认按值复制?** 211 | 212 | `_Start` 的第一行代码展示了这一点。我们将传入的所有参数包装成一个元组类型,这些类型先经过 `decay_t` 处理,去除了引用与 cv 限定,自然就实现了默认复制。 213 | 214 | ```cpp 215 | using _Tuple = tuple, decay_t<_Args>...>; 216 | ``` 217 | 218 | 2. **为什么需要 `std::ref` ?** 219 | 220 | 实现中将类型先经过 `decay` 处理,如果要传递引用,则必须用类包装一下才行,使用 `std::ref` 函数就会返回一个包装对象。 221 | 222 | 3. **如何支持只能移动的对象?** 223 | 224 | 参数通过完美转发,最终调用时使用 `std::move`,这在线程实际执行的函数 `_Invoke` 中体现出来: 225 | 226 | ```cpp 227 | _STD invoke(_STD move(_STD get<_Indices>(_Tup))...); 228 | ``` 229 | 230 | 4. 如何做到接受任意[可调用](https://zh.cppreference.com/w/cpp/named_req/Callable)对象? 231 | 232 | 源码的实现很简单,主要是通过两层包装,最终将 `void*` 指针转换到原类型,然后使用 `std::invoke` 进行调用。 233 | 234 | 5. **如何创建的线程?** 235 | 236 | MSVC STL 调用 Win32 API `_beginthreadex` 创建线程;libstdc++ 调用 `__gthread_create` 函数创建线程,在 Windows 上实际上就是调用 `CreateThread`。 237 | `_beginthreadex` 和 `CreateThread` 都是微软提供的用于创建线程的 C 风格接口,它们的主要区别在于前者依赖于 C 运行时库,而后者更适合纯 Windows API 的情况。使用 `_beginthreadex` 可以确保正确初始化和清理 C 运行时库资源,而 `CreateThread` 则适用于不依赖于 C 运行时库的环境。 238 | 239 | 6. **传递参数一节中的:“*`std::thread` 内部会将保有的参数副本转换为右值表达式进行传递*”到底是如何做到的?** 240 | 241 | 这就是第三个问题,差不多,无非是最后调用 `std::invoke` 函数之前,先 `std::move` 了。 242 | -------------------------------------------------------------------------------- /md/详细分析/04线程池.md: -------------------------------------------------------------------------------- 1 | # 线程池 2 | 3 | ## 前言 4 | 5 | 我相信,已经阅读到这里的各位,不会对“*线程池*”这个词感到陌生。大部分开发者早就自己使用、学习,乃至实现过线程池。那不如我们先来进行一下基础的名词解释。 6 | 7 | - ***什么叫线程池?*** 8 | 9 | “**线程**”没什么好说的,是 CPU 调度的最小单位,也是操作系统的一种抽象资源。 10 | 11 | “**池**”?水池装着水,线程池则是装着线程,是一种抽象的指代。 12 | 13 | 抽象的来说,可以当做是一个池子中存放了一堆线程,**故称作线程池**。简而言之,线程池是指代一组**预先创建的**、**可以复用的线程集合**。这些线程由线程池管理,用于执行多个任务而**无需频繁地创建和销毁**线程。 14 | 15 | ```mermaid 16 | graph TD 17 | subgraph 线程池 18 | 线程1 19 | 线程2 20 | 线程3 21 | 线程4 22 | 线程5 23 | ... 24 | end 25 | 26 | 任务队列[任务队列] 27 | 调度器 --> 线程1 28 | 调度器 --> 线程2 29 | 调度器 --> 线程3 30 | 调度器 --> 线程4 31 | 调度器 --> 线程5 32 | 33 | 任务1 --> 调度器 34 | 任务2 --> 调度器 35 | 任务3 --> 调度器 36 | 任务4 --> 调度器 37 | 任务5 --> 调度器 38 | 任务6 --> 调度器 39 | 任务7 --> 调度器 40 | 41 | 线程1 --> 执行任务1[执行任务1] 42 | 线程2 --> 执行任务2[执行任务2] 43 | 线程3 --> 执行任务3[执行任务3] 44 | 线程4 --> 执行任务4[执行任务4] 45 | 线程5 --> 执行任务5[执行任务5] 46 | 47 | 执行任务1 --> 休眠1[休眠等待] 48 | 执行任务2 --> 休眠2[休眠等待] 49 | 执行任务3 --> 休眠3[休眠等待] 50 | 执行任务4 --> 休眠4[休眠等待] 51 | 执行任务5 --> 休眠5[休眠等待] 52 | 53 | 任务6 --> 调度器 --> 唤醒线程1[唤醒线程1] 54 | 唤醒线程1 -.-> 线程1 55 | 线程1 --> 执行任务6[执行任务6] 56 | 执行任务6 --> 休眠1 57 | 58 | 任务7 --> 调度器 --> 唤醒线程2[唤醒线程2] 59 | 唤醒线程2 -.-> 线程2 60 | 线程2 --> 执行任务7[执行任务7] 61 | 执行任务7 --> 休眠2 62 | 63 | ``` 64 | 65 | > 这是一个典型的线程池结构。线程池包含一个**任务队列**,当有新任务加入时,调度器会将任务分配给线程池中的空闲线程进行执行。线程在执行完任务后会进入**休眠状态**,等待**调度器**的下一次**唤醒**。当有新的任务加入队列,并且有线程处于休眠状态时,调度器会唤醒休眠的线程,并分配新的任务给它们执行。线程执行完新任务后,会再次进入休眠状态,直到有新的任务到来,调度器**才可能**会再次唤醒它们。 66 | > 67 | > 图中线程1 就是被调度器分配了任务1,执行完毕后休眠,然而新任务的到来让调度器再次将它唤醒,去执行任务6,执行完毕后继续休眠。 68 | 69 | 使用线程池的益处我们已经加粗了,然而这其实并不是“*线程池*”独有的,任何创建和销毁存在较大开销的设施,都可以进行所谓的“***池化***”。 70 | 71 | 常见的还有:**套接字连接池**、**数据库连接池**、**内存池**、**对象池**。 72 | 73 | --- 74 | 75 | 了解以上这些基础概念是第一步也是最后一步,随着水平的提升,对这些概念的理解也会逐渐提升。 76 | 77 | ## 市面上常见的线程池 78 | 79 | 在了解了线程池的基本概念与运行逻辑后,我们不用着急就尝试实现。我们可以先来聊一聊,使用一下市面上常见的那些 C++ 线程池设施,了解它们提供的功能,接口设计的方式。 80 | 81 | ### `boost::asio::thread_pool` 82 | 83 | [`boost::asio::thread_pool`](https://think-async.com/Asio/asio-1.11.0/doc/asio/reference/thread_pool.html) 是 [`Boost.Asio`](https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio.html) 库提供的一种线程池实现。 84 | 85 | > Asio 是一个跨平台的 C++ 库,用于**网络**和低级 I/O 编程,使用 **现代C++** 方法为开发人员提供一致的异步模型。 86 | 87 | 使用方法: 88 | 89 | 1. 创建线程池对象,指定或让 Asio 自动决定线程数量。 90 | 91 | 2. 提交任务:通过 [`boost::asio::post`](https://beta.boost.org/doc/libs/1_82_0/doc/html/boost_asio/reference/post.html) 函数模板提交任务到线程池中。 92 | 93 | 3. 阻塞,直到池中的线程完成任务。 94 | 95 | ```cpp 96 | #include 97 | #include 98 | 99 | std::mutex m; 100 | 101 | void print_task(int n) { 102 | std::lock_guard lc{ m }; 103 | std::cout << "Task " << n << " is running on thr: " << 104 | std::this_thread::get_id() << '\n'; 105 | } 106 | 107 | int main() { 108 | boost::asio::thread_pool pool{ 4 }; // 创建一个包含 4 个线程的线程池 109 | 110 | for (int i = 0; i < 10; ++i) { 111 | boost::asio::post(pool, [i] { print_task(i); }); 112 | } 113 | 114 | pool.join(); // 等待所有任务执行完成 115 | } 116 | ``` 117 | 118 | > [运行](https://godbolt.org/z/41445Kab5)测试。 119 | 120 | - 创建线程池时,指定线程数量,线程池会创建对应数量的线程。 121 | 122 | - 使用 `boost::asio::post` 提交任务,任务会被添加到任务队列中。 123 | 124 | - 线程池中的线程会从任务队列中取出任务并执行,任务执行完毕后,线程继续取下一个任务或者休眠。 125 | 126 | - 调用 join 方法等待所有任务执行完毕并关闭线程池。 127 | 128 | 如果我们不自己指明线程池的线程数量,那么 Asio 会根据函数 [`default_thread_pool_size`](https://github.com/boostorg/asio/blob/44238d033e1503c694782925d647811380a067c2/include/boost/asio/impl/thread_pool.ipp#L53-L58) 计算并返回一个**线程池的默认线程数量**。它根据系统的硬件并发能力来决定使用的线程数,通常是硬件并发能力的两倍。 129 | 130 | ```cpp 131 | inline long default_thread_pool_size() 132 | { 133 | std::size_t num_threads = thread::hardware_concurrency() * 2; 134 | num_threads = num_threads == 0 ? 2 : num_threads; 135 | return static_cast(num_threads); 136 | } 137 | 138 | thread_pool::thread_pool() 139 | : scheduler_(add_scheduler(new detail::scheduler(*this, 0, false))), 140 | num_threads_(detail::default_thread_pool_size()) 141 | ``` 142 | 143 | 代码很简单,就是 `thread::hardware_concurrency() * 2` 而已,至于下面的判断是因为 `std::thread::hardware_concurrency()` 在某些特殊情况下可能返回 `0`(例如硬件并发能力无法被检测时),那那将 `num_threads` 设置为 2,确保线程池至少有 2 个线程。 144 | 145 | --- 146 | 147 | Boost.Asio 的线程池对象在[析构](https://github.com/boostorg/asio/blob/44238d033e1503c694782925d647811380a067c2/include/boost/asio/impl/thread_pool.ipp#L98-L103)时会自动调用相关的清理方法,但你也可以手动进行控制。 148 | 149 | ```cpp 150 | thread_pool::~thread_pool() 151 | { 152 | stop(); // 停止接收新任务 153 | join(); // 等待所有线程完成 154 | shutdown(); // 最终清理,释放资源 155 | } 156 | ``` 157 | 158 | - `stop` :修改内部的标志位存在使得线程池能够识别何时需要停止接收新的任务,以及关闭还没开始执行的任务,然后唤醒所有线程。 159 | - `join()` :等待所有线程完成它们的工作,确保所有线程都已终止。 160 | - `shutdown()` :进行最终的清理,释放资源,确保线程池的完全清理和资源的正确释放 161 | 162 | > 此处可阅读部分源码,帮助理解与记忆 163 | 164 | 析构函数先调用了 `stop()` ,然后再进行 `join()` 。那如果我们没有提前显式调用 `join()` 成员函数,**可能导致一些任务没有执行,析构函数并不会等待所有任务执行完毕**: 165 | 166 | ```cpp 167 | boost::asio::thread_pool pool{ 4 }; 168 | 169 | for (int i = 0; i < 10; ++i) { 170 | boost::asio::post(pool, [i] { print_task(i); }); 171 | } 172 | ``` 173 | 174 | > [运行](https://godbolt.org/z/MPoxrY9Yo)测试。 175 | 176 | 因为析构函数并不是阻塞直到执行完所有任务,而是先**停止**,再 `join()` 以及 `shutdown()`。 177 | 178 | `Boost.Asio` 提供的线程池使用十分简单,接口高度封装,几乎无需关心底层具体实现,易于使用。 179 | 180 | 我们的操作几乎只需创建线程池对象、将任务加入线程池、在需要时调用 `join()`。 181 | 182 | ```cpp 183 | boost::asio::thread_pool pool{4}; // 创建线程池 184 | boost::asio::post(pool, task); // 将任务加入线程池 185 | pool.join(); // 等待任务完成 (或者析构自动调用) 186 | ``` 187 | 188 | ### `QThreadPool` 189 | 190 | [`QThreadPool`](https://doc.qt.io/qt-6/qthreadpool.html) 是 Qt 提供的线程池实现,它是用来管理自家的 `QThreads` 的集合。 191 | 192 | ```cpp 193 | #include 194 | #include 195 | #include 196 | #include 197 | 198 | struct MyTask : public QRunnable{ 199 | void run() override { 200 | qDebug() << "🐢🐢🐢🐢🐢"; 201 | } 202 | }; 203 | 204 | int main(int argc, char *argv[]){ 205 | QCoreApplication app(argc, argv); 206 | 207 | QThreadPool *threadPool = QThreadPool::globalInstance(); 208 | 209 | // 线程池最大线程数 210 | qDebug()<< threadPool->maxThreadCount(); 211 | 212 | for (int i = 0; i < 10; ++i) { 213 | MyTask *task = new MyTask{}; 214 | threadPool->start(task); 215 | } 216 | // 当前活跃线程数 10 217 | qDebug()<activeThreadCount(); 218 | 219 | app.exec(); 220 | } 221 | ``` 222 | 223 | 与 `Asio.thread_pool` 不同,`QThreadPool` 采用单例模式,通过静态成员函数 `QThreadPool::globalInstance()` 获取对象实例(不过也可以自己创建)。默认情况下,`QThreadPool` 线程池的最大线程数为当前硬件支持的并发线程数,例如在我的硬件上为 `20`,这点也和 `Asio.thread_pool` 不同。 224 | 225 | `QThreadPool` 依赖于 Qt 的事件循环,因此我们使用了 `QCoreApplication`。 226 | 227 | 而将任务添加到线程池中的做法非常古老原始,我们需要**自定义一个类型继承并重写虚函数 `run`**,创建任务对象,然后将任务对象传递给线程池的 `start` 方法。 228 | 229 | > 这种方法过于原始,如果读者学过 `java` 相信也不会陌生。我们实现的线程池不会是如此。 230 | 231 | 在 Qt6,引入了一个 [`start`](https://doc.qt.io/qt-6/qthreadpool.html#start-1) 的重载版本: 232 | 233 | ```cpp 234 | template > 235 | void QThreadPool::start(Callable &&functionToRun, int priority) 236 | { 237 | start(QRunnable::create(std::forward(functionToRun)), priority); 238 | } 239 | ``` 240 | 241 | 它相当于是对[`start` 原始版本](https://doc.qt.io/qt-5/qthreadpool.html#start)的: 242 | 243 | ```cpp 244 | void start(QRunnable *runnable, int priority = 0); 245 | ``` 246 | 247 | > [源码](https://codebrowser.dev/qt6/qtbase/src/corelib/thread/qthreadpool.cpp.html#_ZN11QThreadPool5startEP9QRunnablei)。 248 | 249 | 进行的一个**包装**,以支持任何的[*可调用(*Callable*)*](https://zh.cppreference.com/w/cpp/named_req/Callable)类型,而无需再繁琐的继承重写 `run` 函数。 250 | 251 | ```cpp 252 | threadPool->start([=]{ 253 | qDebug()< - “*普通的能够满足日常开发需*求的” 281 | > 282 | > 其实绝大部分开发者使用线程池,只是为了不重复多次创建线程罢了。所以只需要一个提供一个外部接口,可以传入任务到任务队列,然后安排线程去执行。无非是使用条件变量、互斥量、原子标志位,这些东西,就足够编写一个满足绝大部分业务需求的线程池。 283 | 284 | 我们先编写一个**最基础的**线程池,首先确定它的数据成员: 285 | 286 | ```cpp 287 | class ThreadPool { 288 | std::mutex mutex_; 289 | std::condition_variable cv_; 290 | std::atomic stop_; 291 | std::atomic num_threads_; 292 | std::queue tasks_; 293 | std::vector pool_; 294 | }; 295 | ``` 296 | 297 | 1. **`std::mutex mutex_`** 298 | - 用于保护共享资源(如任务队列)在多线程环境中的访问,避免数据竞争。 299 | 300 | 2. **`std::condition_variable cv_`** 301 | - 用于线程间的同步,允许线程等待特定条件(如新任务加入队列)并在条件满足时唤醒线程。 302 | 303 | 3. **`std::atomic stop_`** 304 | - 指示线程池是否停止。 305 | 306 | 4. **`std::atomic num_threads_`** 307 | - 表示线程池中的线程数量。 308 | 309 | 5. **`std::queue tasks_`** 310 | - 任务队列,存储等待执行的任务,任务按提交顺序执行。 311 | 312 | 6. **`std::vector pool_`** 313 | 314 | - 线程容器,存储管理线程对象,每个线程从任务队列中获取任务并执行。 315 | 316 | **标头依赖**: 317 | 318 | ```cpp 319 | #include 320 | #include 321 | #include 322 | #include 323 | #include 324 | #include 325 | #include 326 | #include 327 | #include 328 | #include 329 | ``` 330 | 331 | 提供构造析构函数,以及一些外部接口:`submit()`、`start()`、`stop()`、`join()`,也就完成了: 332 | 333 | ```cpp 334 | inline std::size_t default_thread_pool_size()noexcept { 335 | std::size_t num_threads = std::thread::hardware_concurrency() * 2; 336 | num_threads = num_threads == 0 ? 2 : num_threads; 337 | return num_threads; 338 | } 339 | 340 | class ThreadPool { 341 | public: 342 | using Task = std::packaged_task; 343 | 344 | ThreadPool(const ThreadPool&) = delete; 345 | ThreadPool& operator=(const ThreadPool&) = delete; 346 | 347 | ThreadPool(std::size_t num_thread = default_thread_pool_size()) 348 | : stop_{ false }, num_threads_{ num_thread } { 349 | start(); 350 | } 351 | 352 | ~ThreadPool() { 353 | stop(); 354 | } 355 | 356 | void stop() { 357 | stop_.store(true); 358 | cv_.notify_all(); 359 | for (auto& thread : pool_) { 360 | if (thread.joinable()) { 361 | thread.join(); 362 | } 363 | } 364 | pool_.clear(); 365 | } 366 | 367 | template 368 | std::future, std::decay_t...>> submit(F&& f, Args&&...args) { 369 | using RetType = std::invoke_result_t, std::decay_t...>; 370 | if (stop_.load()) { 371 | throw std::runtime_error("ThreadPool is stopped"); 372 | } 373 | 374 | auto task = std::make_shared>( 375 | std::bind(std::forward(f), std::forward(args)...)); 376 | std::future ret = task->get_future(); 377 | 378 | { 379 | std::lock_guard lc{ mutex_ }; 380 | tasks_.emplace([task] {(*task)(); }); 381 | } 382 | cv_.notify_one(); 383 | return ret; 384 | } 385 | 386 | void start() { 387 | for (std::size_t i = 0; i < num_threads_; ++i) { 388 | pool_.emplace_back([this] { 389 | while (!stop_) { 390 | Task task; 391 | { 392 | std::unique_lock lc{ mutex_ }; 393 | cv_.wait(lc, [this] {return stop_ || !tasks_.empty(); }); 394 | if (tasks_.empty()) 395 | return; 396 | task = std::move(tasks_.front()); 397 | tasks_.pop(); 398 | } 399 | task(); 400 | } 401 | }); 402 | } 403 | } 404 | 405 | private: 406 | std::mutex mutex_; 407 | std::condition_variable cv_; 408 | std::atomic stop_; 409 | std::atomic num_threads_; 410 | std::queue tasks_; 411 | std::vector pool_; 412 | }; 413 | ``` 414 | 415 | **测试 demo**: 416 | 417 | ```cpp 418 | int main() { 419 | ThreadPool pool{ 4 }; // 创建一个有 4 个线程的线程池 420 | std::vector> futures; // future 集合,获取返回值 421 | 422 | for (int i = 0; i < 10; ++i) { 423 | futures.emplace_back(pool.submit(print_task, i)); 424 | } 425 | 426 | for (int i = 0; i < 10; ++i) { 427 | futures.emplace_back(pool.submit(print_task2, i)); 428 | } 429 | 430 | int sum = 0; 431 | for (auto& future : futures) { 432 | sum += future.get(); // get() 成员函数 阻塞到任务执行完毕,获取返回值 433 | } 434 | std::cout << "sum: " << sum << '\n'; 435 | } // 析构自动 stop() 436 | ``` 437 | 438 | **可能的[运行结果](https://godbolt.org/z/n7Tana59x)**: 439 | 440 | ```shell 441 | Task 0 is running on thr: 6900 442 | Task 1 is running on thr: 36304 443 | Task 5 is running on thr: 36304 444 | Task 3 is running on thr: 6900 445 | Task 7 is running on thr: 6900 446 | Task 2 is running on thr: 29376 447 | Task 6 is running on thr: 36304 448 | Task 4 is running on thr: 31416 449 | 🐢🐢🐢 1 🐉🐉🐉 450 | Task 9 is running on thr: 29376 451 | 🐢🐢🐢 0 🐉🐉🐉 452 | Task 8 is running on thr: 6900 453 | 🐢🐢🐢 2 🐉🐉🐉 454 | 🐢🐢🐢 6 🐉🐉🐉 455 | 🐢🐢🐢 4 🐉🐉🐉 456 | 🐢🐢🐢 5 🐉🐉🐉 457 | 🐢🐢🐢 3 🐉🐉🐉 458 | 🐢🐢🐢 7 🐉🐉🐉 459 | 🐢🐢🐢 8 🐉🐉🐉 460 | 🐢🐢🐢 9 🐉🐉🐉 461 | sum: 90 462 | ``` 463 | 464 | > 如果等待线程池对象调用析构函数,那么效果如同 `asio::thread_pool`,会先进行 `stop`,这可能导致一些任务无法执行。不过我们在最后**循环遍历了 `futures`**,调用 `get()` 成员函数,不存在这个问题。 465 | 466 | 它支持**任意可调用类型**,当然也包括非静态成员函数。我们使用了 [`std::decay_t`](https://zh.cppreference.com/w/cpp/types/decay),所以参数的传递其实是**按值复制**,而不是引用传递,这一点和大部分库的设计一致。示例如下: 467 | 468 | ```cpp 469 | struct X { 470 | void f(const int& n) const { 471 | std::osyncstream{ std::cout } << &n << '\n'; 472 | } 473 | }; 474 | 475 | int main() { 476 | ThreadPool pool{ 4 }; // 创建一个有 4 个线程的线程池 477 | 478 | X x; 479 | int n = 6; 480 | std::cout << &n << '\n'; 481 | auto t = pool.submit(&X::f, &x, n); // 默认复制,地址不同 482 | auto t2 = pool.submit(&X::f, &x, std::ref(n)); 483 | t.wait(); 484 | t2.wait(); 485 | } // 析构自动 stop() 486 | ``` 487 | 488 | > [运行](https://godbolt.org/z/vY458T44e)测试。 489 | 490 | 我们的线程池的 `submit` 成员函数在传递参数的行为上,与先前介绍的 `std::thread` 和 `std::async` 等设施基本一致。 491 | 492 | 我们稍微介绍线程池的接口: 493 | 494 | **构造函数和析构函数:** 495 | 496 | - **构造函数**:初始化线程池并**启动线程**。 497 | 498 | - **析构函数**:停止线程池并等待所有线程结束。 499 | 500 | **外部接口:** 501 | 502 | - **`stop()`**:停止线程池,通知所有线程退出(不会等待所有任务执行完毕)。 503 | - **`submit()`**:将任务提交到任务队列,并返回一个`std::future`对象用于获取任务结果以及确保任务执行完毕。 504 | - **`start()`**:启动线程池,创建并启动指定数量的线程。 505 | 506 | 我们并没有提供一个功能强大的所谓的“***调度器***”,我们只是利用条件变量和互斥量,让操作系统自行调度而已,它并不具备设置任务优先级之类的调度功能。 507 | 508 | 当然,你可能还希望我们的线程池具备更多功能或改进,比如控制任务优先级、设置最大线程数量、返回当前活跃线程数等。此外,异常处理也是一个值得考虑的方面。 509 | 510 | 有些功能实现起来非常简单,而有些则需要更多的思考和设计。不过,这些功能超出了本次讲解的范围。如果有兴趣,可以尝试自行优化我们提供的线程池实现,添加更多的功能。我们给出的线程池实现简单完善且直观,用来学习再好不过。 511 | 512 | ## 总结 513 | 514 | 在本章中我们详细的介绍了: 515 | 516 | - 线程池的基本概念。 517 | 518 | - 市面上常见的线程池的设计与使用, `boost::asio::thread_pool`、`QThreadPool`。 519 | - 实现一个简易的线程池。 520 | 521 | 总体而言,内容并不构成太大的难度。 522 | 523 | **课后作业**:自己实现一个线程池,可以参考我们给出的线程池实现增加功能,提交到 `homework` 文件夹中。 524 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial-NoDerivatives 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 58 | International Public License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial-NoDerivatives 4.0 International Public 63 | License ("Public License"). To the extent this Public License may be 64 | interpreted as a contract, You are granted the Licensed Rights in 65 | consideration of Your acceptance of these terms and conditions, and the 66 | Licensor grants You such rights in consideration of benefits the 67 | Licensor receives from making the Licensed Material available under 68 | these terms and conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Copyright and Similar Rights means copyright and/or similar rights 84 | closely related to copyright including, without limitation, 85 | performance, broadcast, sound recording, and Sui Generis Database 86 | Rights, without regard to how the rights are labeled or 87 | categorized. For purposes of this Public License, the rights 88 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 89 | Rights. 90 | 91 | c. Effective Technological Measures means those measures that, in the 92 | absence of proper authority, may not be circumvented under laws 93 | fulfilling obligations under Article 11 of the WIPO Copyright 94 | Treaty adopted on December 20, 1996, and/or similar international 95 | agreements. 96 | 97 | d. Exceptions and Limitations means fair use, fair dealing, and/or 98 | any other exception or limitation to Copyright and Similar Rights 99 | that applies to Your use of the Licensed Material. 100 | 101 | e. Licensed Material means the artistic or literary work, database, 102 | or other material to which the Licensor applied this Public 103 | License. 104 | 105 | f. Licensed Rights means the rights granted to You subject to the 106 | terms and conditions of this Public License, which are limited to 107 | all Copyright and Similar Rights that apply to Your use of the 108 | Licensed Material and that the Licensor has authority to license. 109 | 110 | g. Licensor means the individual(s) or entity(ies) granting rights 111 | under this Public License. 112 | 113 | h. NonCommercial means not primarily intended for or directed towards 114 | commercial advantage or monetary compensation. For purposes of 115 | this Public License, the exchange of the Licensed Material for 116 | other material subject to Copyright and Similar Rights by digital 117 | file-sharing or similar means is NonCommercial provided there is 118 | no payment of monetary compensation in connection with the 119 | exchange. 120 | 121 | i. Share means to provide material to the public by any means or 122 | process that requires permission under the Licensed Rights, such 123 | as reproduction, public display, public performance, distribution, 124 | dissemination, communication, or importation, and to make material 125 | available to the public including in ways that members of the 126 | public may access the material from a place and at a time 127 | individually chosen by them. 128 | 129 | j. Sui Generis Database Rights means rights other than copyright 130 | resulting from Directive 96/9/EC of the European Parliament and of 131 | the Council of 11 March 1996 on the legal protection of databases, 132 | as amended and/or succeeded, as well as other essentially 133 | equivalent rights anywhere in the world. 134 | 135 | k. You means the individual or entity exercising the Licensed Rights 136 | under this Public License. Your has a corresponding meaning. 137 | 138 | 139 | Section 2 -- Scope. 140 | 141 | a. License grant. 142 | 143 | 1. Subject to the terms and conditions of this Public License, 144 | the Licensor hereby grants You a worldwide, royalty-free, 145 | non-sublicensable, non-exclusive, irrevocable license to 146 | exercise the Licensed Rights in the Licensed Material to: 147 | 148 | a. reproduce and Share the Licensed Material, in whole or 149 | in part, for NonCommercial purposes only; and 150 | 151 | b. produce and reproduce, but not Share, Adapted Material 152 | for NonCommercial purposes only. 153 | 154 | 2. Exceptions and Limitations. For the avoidance of doubt, where 155 | Exceptions and Limitations apply to Your use, this Public 156 | License does not apply, and You do not need to comply with 157 | its terms and conditions. 158 | 159 | 3. Term. The term of this Public License is specified in Section 160 | 6(a). 161 | 162 | 4. Media and formats; technical modifications allowed. The 163 | Licensor authorizes You to exercise the Licensed Rights in 164 | all media and formats whether now known or hereafter created, 165 | and to make technical modifications necessary to do so. The 166 | Licensor waives and/or agrees not to assert any right or 167 | authority to forbid You from making technical modifications 168 | necessary to exercise the Licensed Rights, including 169 | technical modifications necessary to circumvent Effective 170 | Technological Measures. For purposes of this Public License, 171 | simply making modifications authorized by this Section 2(a) 172 | (4) never produces Adapted Material. 173 | 174 | 5. Downstream recipients. 175 | 176 | a. Offer from the Licensor -- Licensed Material. Every 177 | recipient of the Licensed Material automatically 178 | receives an offer from the Licensor to exercise the 179 | Licensed Rights under the terms and conditions of this 180 | Public License. 181 | 182 | b. No downstream restrictions. You may not offer or impose 183 | any additional or different terms or conditions on, or 184 | apply any Effective Technological Measures to, the 185 | Licensed Material if doing so restricts exercise of the 186 | Licensed Rights by any recipient of the Licensed 187 | Material. 188 | 189 | 6. No endorsement. Nothing in this Public License constitutes or 190 | may be construed as permission to assert or imply that You 191 | are, or that Your use of the Licensed Material is, connected 192 | with, or sponsored, endorsed, or granted official status by, 193 | the Licensor or others designated to receive attribution as 194 | provided in Section 3(a)(1)(A)(i). 195 | 196 | b. Other rights. 197 | 198 | 1. Moral rights, such as the right of integrity, are not 199 | licensed under this Public License, nor are publicity, 200 | privacy, and/or other similar personality rights; however, to 201 | the extent possible, the Licensor waives and/or agrees not to 202 | assert any such rights held by the Licensor to the limited 203 | extent necessary to allow You to exercise the Licensed 204 | Rights, but not otherwise. 205 | 206 | 2. Patent and trademark rights are not licensed under this 207 | Public License. 208 | 209 | 3. To the extent possible, the Licensor waives any right to 210 | collect royalties from You for the exercise of the Licensed 211 | Rights, whether directly or through a collecting society 212 | under any voluntary or waivable statutory or compulsory 213 | licensing scheme. In all other cases the Licensor expressly 214 | reserves any right to collect such royalties, including when 215 | the Licensed Material is used other than for NonCommercial 216 | purposes. 217 | 218 | 219 | Section 3 -- License Conditions. 220 | 221 | Your exercise of the Licensed Rights is expressly made subject to the 222 | following conditions. 223 | 224 | a. Attribution. 225 | 226 | 1. If You Share the Licensed Material, You must: 227 | 228 | a. retain the following if it is supplied by the Licensor 229 | with the Licensed Material: 230 | 231 | i. identification of the creator(s) of the Licensed 232 | Material and any others designated to receive 233 | attribution, in any reasonable manner requested by 234 | the Licensor (including by pseudonym if 235 | designated); 236 | 237 | ii. a copyright notice; 238 | 239 | iii. a notice that refers to this Public License; 240 | 241 | iv. a notice that refers to the disclaimer of 242 | warranties; 243 | 244 | v. a URI or hyperlink to the Licensed Material to the 245 | extent reasonably practicable; 246 | 247 | b. indicate if You modified the Licensed Material and 248 | retain an indication of any previous modifications; and 249 | 250 | c. indicate the Licensed Material is licensed under this 251 | Public License, and include the text of, or the URI or 252 | hyperlink to, this Public License. 253 | 254 | For the avoidance of doubt, You do not have permission under 255 | this Public License to Share Adapted Material. 256 | 257 | 2. You may satisfy the conditions in Section 3(a)(1) in any 258 | reasonable manner based on the medium, means, and context in 259 | which You Share the Licensed Material. For example, it may be 260 | reasonable to satisfy the conditions by providing a URI or 261 | hyperlink to a resource that includes the required 262 | information. 263 | 264 | 3. If requested by the Licensor, You must remove any of the 265 | information required by Section 3(a)(1)(A) to the extent 266 | reasonably practicable. 267 | 268 | 269 | Section 4 -- Sui Generis Database Rights. 270 | 271 | Where the Licensed Rights include Sui Generis Database Rights that 272 | apply to Your use of the Licensed Material: 273 | 274 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 275 | to extract, reuse, reproduce, and Share all or a substantial 276 | portion of the contents of the database for NonCommercial purposes 277 | only and provided You do not Share Adapted Material; 278 | 279 | b. if You include all or a substantial portion of the database 280 | contents in a database in which You have Sui Generis Database 281 | Rights, then the database in which You have Sui Generis Database 282 | Rights (but not its individual contents) is Adapted Material; and 283 | 284 | c. You must comply with the conditions in Section 3(a) if You Share 285 | all or a substantial portion of the contents of the database. 286 | 287 | For the avoidance of doubt, this Section 4 supplements and does not 288 | replace Your obligations under this Public License where the Licensed 289 | Rights include other Copyright and Similar Rights. 290 | 291 | 292 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 293 | 294 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 295 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 296 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 297 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 298 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 299 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 300 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 301 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 302 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 303 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 304 | 305 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 306 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 307 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 308 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 309 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 310 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 311 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 312 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 313 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 314 | 315 | c. The disclaimer of warranties and limitation of liability provided 316 | above shall be interpreted in a manner that, to the extent 317 | possible, most closely approximates an absolute disclaimer and 318 | waiver of all liability. 319 | 320 | 321 | Section 6 -- Term and Termination. 322 | 323 | a. This Public License applies for the term of the Copyright and 324 | Similar Rights licensed here. However, if You fail to comply with 325 | this Public License, then Your rights under this Public License 326 | terminate automatically. 327 | 328 | b. Where Your right to use the Licensed Material has terminated under 329 | Section 6(a), it reinstates: 330 | 331 | 1. automatically as of the date the violation is cured, provided 332 | it is cured within 30 days of Your discovery of the 333 | violation; or 334 | 335 | 2. upon express reinstatement by the Licensor. 336 | 337 | For the avoidance of doubt, this Section 6(b) does not affect any 338 | right the Licensor may have to seek remedies for Your violations 339 | of this Public License. 340 | 341 | c. For the avoidance of doubt, the Licensor may also offer the 342 | Licensed Material under separate terms or conditions or stop 343 | distributing the Licensed Material at any time; however, doing so 344 | will not terminate this Public License. 345 | 346 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 347 | License. 348 | 349 | 350 | Section 7 -- Other Terms and Conditions. 351 | 352 | a. The Licensor shall not be bound by any additional or different 353 | terms or conditions communicated by You unless expressly agreed. 354 | 355 | b. Any arrangements, understandings, or agreements regarding the 356 | Licensed Material not stated herein are separate from and 357 | independent of the terms and conditions of this Public License. 358 | 359 | 360 | Section 8 -- Interpretation. 361 | 362 | a. For the avoidance of doubt, this Public License does not, and 363 | shall not be interpreted to, reduce, limit, restrict, or impose 364 | conditions on any use of the Licensed Material that could lawfully 365 | be made without permission under this Public License. 366 | 367 | b. To the extent possible, if any provision of this Public License is 368 | deemed unenforceable, it shall be automatically reformed to the 369 | minimum extent necessary to make it enforceable. If the provision 370 | cannot be reformed, it shall be severed from this Public License 371 | without affecting the enforceability of the remaining terms and 372 | conditions. 373 | 374 | c. No term or condition of this Public License will be waived and no 375 | failure to comply consented to unless expressly agreed to by the 376 | Licensor. 377 | 378 | d. Nothing in this Public License constitutes or may be interpreted 379 | as a limitation upon, or waiver of, any privileges and immunities 380 | that apply to the Licensor or You, including from the legal 381 | processes of any jurisdiction or authority. 382 | 383 | ======================================================================= 384 | 385 | Creative Commons is not a party to its public 386 | licenses. Notwithstanding, Creative Commons may elect to apply one of 387 | its public licenses to material it publishes and in those instances 388 | will be considered the “Licensor.” The text of the Creative Commons 389 | public licenses is dedicated to the public domain under the CC0 Public 390 | Domain Dedication. Except for the limited purpose of indicating that 391 | material is shared under a Creative Commons public license or as 392 | otherwise permitted by the Creative Commons policies published at 393 | creativecommons.org/policies, Creative Commons does not authorize the 394 | use of the trademark "Creative Commons" or any other trademark or logo 395 | of Creative Commons without its prior written consent including, 396 | without limitation, in connection with any unauthorized modifications 397 | to any of its public licenses or any other arrangements, 398 | understandings, or agreements concerning use of licensed material. For 399 | the avoidance of doubt, this paragraph does not form part of the 400 | public licenses. 401 | 402 | Creative Commons may be contacted at creativecommons.org. 403 | -------------------------------------------------------------------------------- /md/详细分析/03async与future源码解析.md: -------------------------------------------------------------------------------- 1 | # `std::async` 与 `std::future` 源码解析 2 | 3 | ## 前言 4 | 5 | 和之前一样的,我们以 MSVC STL 的实现进行讲解。 6 | 7 | `std::future`,即未来体,是用来管理一个共享状态的类模板,用于等待关联任务的完成并获取其返回值。它自身不包含状态,需要通过如 `std::async` 之类的函数进行初始化。`std::async` 函数模板返回一个已经初始化且具有共享状态的 `std::future` 对象。 8 | 9 | 因此,所有操作的开始应从 `std::async` 开始讲述。 10 | 11 | 需要注意的是,它们的实现彼此之间会共用不少设施,在讲述 `std::async` 源码的时候,对于 `std::future` 的内容同样重要。 12 | 13 | > MSVC STL 很早之前就不支持 C++11 了,它的实现完全基于 **C++14**,出于某些原因 **C++17** 的一些库(如 [`invoke`](https://zh.cppreference.com/w/cpp/utility/functional/invoke), _v 变量模板)被向后移植到了 **C++14** 模式,所以即使是 C++11 标准库设施,实现中可能也是使用到了 C++14、17 的东西。 14 | > 15 | > 注意,不用感到奇怪。 16 | 17 | ## `std::async` 18 | 19 | ```cpp 20 | _EXPORT_STD template 21 | _NODISCARD_ASYNC future<_Invoke_result_t, decay_t<_ArgTypes>...>> async( 22 | launch _Policy, _Fty&& _Fnarg, _ArgTypes&&... _Args) { 23 | // manages a callable object launched with supplied policy 24 | using _Ret = _Invoke_result_t, decay_t<_ArgTypes>...>; 25 | using _Ptype = typename _P_arg_type<_Ret>::type; 26 | _Promise<_Ptype> _Pr( 27 | _Get_associated_state<_Ret>(_Policy, _Fake_no_copy_callable_adapter<_Fty, _ArgTypes...>( 28 | _STD forward<_Fty>(_Fnarg), _STD forward<_ArgTypes>(_Args)...))); 29 | 30 | return future<_Ret>(_From_raw_state_tag{}, _Pr._Get_state_for_future()); 31 | } 32 | 33 | _EXPORT_STD template 34 | _NODISCARD_ASYNC future<_Invoke_result_t, decay_t<_ArgTypes>...>> async( 35 | _Fty&& _Fnarg, _ArgTypes&&... _Args) { 36 | // manages a callable object launched with default policy 37 | return _STD async(launch::async | launch::deferred, _STD forward<_Fty>(_Fnarg), _STD forward<_ArgTypes>(_Args)...); 38 | } 39 | ``` 40 | 41 | 这段代码最直观的信息是,函数模板 `std::async` 有两个重载,其中第二个重载只是给了一个执行策略并将参数全部转发,调用第一个重载。也就是不指明执行策略的时候就会匹配到第二个重载版本。因此我们也只需要关注第二个版本了。 42 | 43 | 1. **模板参数和函数体外部信息**: 44 | 45 | - `_EXPOPT_STD` 是一个宏,当 `_BUILD_STD_MODULE` 宏定义且启用了 C++20 时,会被定义为 `export`,以便导出模块;否则它为空。 46 | 47 | - `_Fty` 表示可调用对象的类型。 48 | - `_ArgTypes` 是一个类型形参包,表示调用该可调用对象所需的参数类型。 49 | - `_NODISCARD_ASYNC` 是一个宏,表示属性 `[[nodiscard]]`,用于标记此函数的返回值不应被忽略。 50 | 51 | 2. 函数返回类型: 52 | 53 | ```cpp 54 | future<_Invoke_result_t, decay_t<_ArgTypes>...>> 55 | ``` 56 | 57 | 虽然看起来复杂,但实际上是通过 `_Invoke_result_t` 获取可调用对象的返回类型。与标准库中的 [`std::invoke_result_t`](https://zh.cppreference.com/w/cpp/types/result_of) 基本相同。 58 | 59 | 可以举一个使用 `std::invoke_result_t` 的例子: 60 | 61 | ```cpp 62 | template 63 | std::future,std::decay_t...>> 64 | test_fun(Fty&& f,ArgTypes&&... args){ 65 | return std::async(std::launch::async, std::forward(f), std::forward(args)...); 66 | } 67 | 68 | auto result = test_fun([](int) {return 1; }, 1); // std::future 69 | ``` 70 | 71 | 值得注意的是,所有类型在传递前都进行了 [`decay`](https://zh.cppreference.com/w/cpp/types/decay) 处理,也就是说不存在引用类型,是默认按值传递与 `std::thread` 的行为一致。 72 | 73 | 3. 函数形参: 74 | 75 | ```cpp 76 | async(launch _Policy, _Fty&& _Fnarg, _ArgTypes&&... _Args) 77 | ``` 78 | 79 | `launch _Policy`: 表示任务的执行策略,可以是 `launch::async`(表示异步执行)或 `launch::deferred`(表示延迟执行),或者两者的组合。 80 | 81 | `_Fty&& _Fnarg`: 可调用对象,通过完美转发机制将其转发给实际的异步任务。 82 | 83 | `_ArgTypes&&... _Args`: 调用该可调用对象时所需的参数,同样通过完美转发机制进行转发。 84 | 85 | 4. `using _Ret = _Invoke_result_t, decay_t<_ArgTypes>...>;` 86 | 87 | `using _Ptype = typename _P_arg_type<_Ret>::type;` 88 | 89 | - 定义 `_Ret` 类型别名,它是使用 `_ArgTypes` 类型参数调用 `_Fty` 类型的可调用对象后得到的结果类型。也就是我们传入的可调用对象的返回类型;同样使用了 `_Invoke_result_t`(等价于 [`std::invoke_result_t`](https://zh.cppreference.com/w/cpp/types/result_of) ) 与 `decay_t`。 90 | 91 | - 其实 `_Ptype` 的定义确实在大多数情况下和 `_Ret` 是相同的,类模板 _P_arg_type 只是为了处理引用类型以及 void 的情况,参见 `_P_arg_type` 的实现: 92 | 93 | ```cpp 94 | template 95 | struct _P_arg_type { // type for functions returning T 96 | using type = _Fret; 97 | }; 98 | 99 | template 100 | struct _P_arg_type<_Fret&> { // type for functions returning reference to T 101 | using type = _Fret*; 102 | }; 103 | 104 | template <> 105 | struct _P_arg_type { // type for functions returning void 106 | using type = int; 107 | }; 108 | ``` 109 | 110 | `_Ptype`:处理异步任务返回值的方式类型,它在语义上强调了异步任务返回值的处理方式,具有不同的实现逻辑和使用场景。在当前我们难以直接展示它的作用,不过可以推测,这个“`P`” 表示的是后文将使用的 `_Promise` 类模板。也就是说,定义 `_Ptype` 是为了配合 `_Promise` 的使用。我们将会在后文详细探讨 `_Promise` 类型的内部实现,并进一步解释 `_Ptype` 的具体作用。 111 | 112 | 5. `_Promise<_Ptype> _Pr` 113 | 114 | [`_Promise`](https://github.com/microsoft/STL/blob/f54203f/stl/inc/future#L999-L1055) 类型本身不重要,很简单,关键还在于其存储的数据成员。 115 | 116 | ```cpp 117 | template 118 | class _Promise { 119 | public: 120 | _Promise(_Associated_state<_Ty>* _State_ptr) noexcept : _State(_State_ptr, false), _Future_retrieved(false) {} 121 | 122 | _Promise(_Promise&&) = default; 123 | 124 | _Promise& operator=(_Promise&&) = default; 125 | 126 | void _Swap(_Promise& _Other) noexcept { 127 | _State._Swap(_Other._State); 128 | _STD swap(_Future_retrieved, _Other._Future_retrieved); 129 | } 130 | 131 | const _State_manager<_Ty>& _Get_state() const noexcept { 132 | return _State; 133 | } 134 | _State_manager<_Ty>& _Get_state() noexcept { 135 | return _State; 136 | } 137 | 138 | _State_manager<_Ty>& _Get_state_for_set() { 139 | if (!_State.valid()) { 140 | _Throw_future_error2(future_errc::no_state); 141 | } 142 | 143 | return _State; 144 | } 145 | 146 | _State_manager<_Ty>& _Get_state_for_future() { 147 | if (!_State.valid()) { 148 | _Throw_future_error2(future_errc::no_state); 149 | } 150 | 151 | if (_Future_retrieved) { 152 | _Throw_future_error2(future_errc::future_already_retrieved); 153 | } 154 | 155 | _Future_retrieved = true; 156 | return _State; 157 | } 158 | 159 | bool _Is_valid() const noexcept { 160 | return _State.valid(); 161 | } 162 | 163 | bool _Is_ready() const noexcept { 164 | return _State._Is_ready(); 165 | } 166 | 167 | _Promise(const _Promise&) = delete; 168 | _Promise& operator=(const _Promise&) = delete; 169 | 170 | private: 171 | _State_manager<_Ty> _State; 172 | bool _Future_retrieved; 173 | }; 174 | ``` 175 | 176 | `_Promise` 类模板是对 **[`_State_manager`](https://github.com/microsoft/STL/blob/f54203f/stl/inc/future#L688-L825) 类模板的包装**,并增加了一个表示状态的成员 `_Future_retrieved`。 177 | 178 | 状态成员用于跟踪 `_Promise` 是否已经调用过 `_Get_state_for_future()` 成员函数;它默认为 `false`,在**第一次**调用 `_Get_state_for_future()` 成员函数时被置为 `true`,如果二次调用,就会抛出 [`future_errc::future_already_retrieved`](https://zh.cppreference.com/w/cpp/thread/future_errc) 异常。 179 | 180 | > 这类似于 `std::promise` 调用 [`get_future()`](https://zh.cppreference.com/w/cpp/thread/promise/get_future) 成员函数。[测试](https://godbolt.org/z/8anc9b3PT)。 181 | 182 | `_Promise` 的构造函数接受的却不是 `_State_manager` 类型的对象,而是 `_Associated_state` 类型的指针,用来初始化数据成员 `_State`。 183 | 184 | ```cpp 185 | _State(_State_ptr, false) 186 | ``` 187 | 188 | 这是因为实际上 `_State_manager` 类型的[实现](https://github.com/microsoft/STL/blob/f54203f/stl/inc/future#L822-L824)就是保有了 [`Associated_state`](https://github.com/microsoft/STL/blob/f54203f/stl/inc/future#L198-L445) 指针,以及一个状态成员: 189 | 190 | ```cpp 191 | private: 192 | _Associated_state<_Ty>* _Assoc_state = nullptr; 193 | bool _Get_only_once = false; 194 | ``` 195 | 196 | 也可以简单理解 `_State_manager` 又是对 `Associated_state` 的包装,其中的大部分接口实际上是调用 `_Assoc_state` 的成员函数,如: 197 | 198 | ```cpp 199 | void wait() const { // wait for signal 200 | if (!valid()) { 201 | _Throw_future_error2(future_errc::no_state); 202 | } 203 | 204 | _Assoc_state->_Wait(); 205 | } 206 | ``` 207 | 208 | - **一切的重点,最终在 `Associated_state` 上**。 209 | 210 | 然而它也是最为复杂的,我们在讲 `std::thread`-构造源码解析 中提到过一句话: 211 | 212 | > - **了解一个庞大的类,最简单的方式就是先看它的数据成员有什么**。 213 | 214 | ```cpp 215 | public: 216 | conditional_t, _Ty, _Result_holder<_Ty>> _Result; 217 | exception_ptr _Exception; 218 | mutex _Mtx; 219 | condition_variable _Cond; 220 | bool _Retrieved; 221 | int _Ready; 222 | bool _Ready_at_thread_exit; // TRANSITION, ABI 223 | bool _Has_stored_result; 224 | bool _Running; 225 | ``` 226 | 227 | 这是 `Associated_state` 的数据成员,其中有许多的 `bool` 类型的状态成员,同时最为明显重要的三个设施是:**异常指针**、**互斥量**、**条件变量**。 228 | 229 | 根据这些数据成员我们就能很轻松的猜测出 `Associated_state` 模板类的作用和工作方式。 230 | 231 | - **异常指针**:用于存储异步任务中可能发生的异常,以便在调用 `future::get` 时能够重新抛出异常。 232 | - **互斥量和条件变量**:用于在异步任务和等待任务之间进行同步。当异步任务完成时,条件变量会通知等待的任务。 233 | 234 | **`_Associated_state` 模板类负责管理异步任务的状态,包括结果的存储、异常的处理以及任务完成的通知。它是实现 `std::future` 和 `std::promise` 的核心组件之一**,通过 `_State_manager` 和 `_Promise` 类模板对其进行封装和管理,提供更高级别的接口和功能。 235 | 236 | ```plaintext 237 | +---------------------+ 238 | | _Promise<_Ty> | 239 | |---------------------| 240 | | - _State | -----> +---------------------+ 241 | | - _Future_retrieved | | _State_manager<_Ty> | 242 | +---------------------+ |----------------------| 243 | | - _Assoc_state | -----> +-------------------------+ 244 | | - _Get_only_once | | _Associated_state<_Ty>* | 245 | +----------------------+ +-------------------------+ 246 | ``` 247 | 248 | 上图是 `_Promise`、_`State_manager`、`_Associated_state` 之间的**包含关系示意图**,理解这个关系对我们后面**非常重要**。 249 | 250 | 到此就可以了,我们不需要在此处就详细介绍这三个类,但是你需要大概的看一下,这非常重要。 251 | 252 | 6. 初始化 `_Promie` 对象: 253 | 254 | `_Get_associated_state<_Ret>(_Policy, _Fake_no_copy_callable_adapter<_Fty, _ArgTypes...>(_STD forward<_Fty>(_Fnarg), _STD forward<_ArgTypes>(_Args)...))` 255 | 256 | 很明显,这是一个函数调用,将我们 `std::async` 的参数全部转发给它,它是重要而直观的。 257 | 258 | [`_Get_associated_state`](https://github.com/microsoft/STL/blob/f54203f/stl/inc/future#L1400-L1410) 函数根据启动模式(`launch`)来决定创建的异步任务状态对象类型: 259 | 260 | ```cpp 261 | template 262 | _Associated_state::type>* _Get_associated_state(launch _Psync, _Fty&& _Fnarg) { 263 | // construct associated asynchronous state object for the launch type 264 | switch (_Psync) { // select launch type 265 | case launch::deferred: 266 | return new _Deferred_async_state<_Ret>(_STD forward<_Fty>(_Fnarg)); 267 | case launch::async: // TRANSITION, fixed in vMajorNext, should create a new thread here 268 | default: 269 | return new _Task_async_state<_Ret>(_STD forward<_Fty>(_Fnarg)); 270 | } 271 | } 272 | ``` 273 | 274 | `_Get_associated_state` 函数返回一个 `_Associated_state` 指针,该指针指向一个新的 `_Deferred_async_state` 或 `_Task_async_state` 对象。这两个类分别对应于异步任务的两种不同执行策略:**延迟执行**和**异步执行**。 275 | 276 | > 其实就是父类指针指向了子类对象,注意 **`_Associated_state` 是有虚函数的**,子类进行覆盖,这很重要。比如在后续聊 `std::future` 的 `get()` 成员函数的时候就会讲到 277 | 278 | > 这段代码也很好的说明在 MSVC STL 中,`launch::async | launch::deferred` 和 `launch::async` 的行为是相同的,即都是异步执行。 279 | 280 | --- 281 | 282 | **[`_Task_async_state`](https://github.com/microsoft/STL/blob/f54203f/stl/inc/future#L654-L686) 与 `_Deferred_async_state` 类型** 283 | 284 | ```cpp 285 | template 286 | class _Task_async_state : public _Packaged_state<_Rx()> 287 | template 288 | class _Deferred_async_state : public _Packaged_state<_Rx()> 289 | ``` 290 | 291 | `_Task_async_state` 与 `_Deferred_async_state` 都继承自 [`_Packaged_state`](https://github.com/microsoft/STL/blob/f54203f/stl/inc/future#L462-L597),用于异步执行任务。它们的构造函数都接受一个函数对象,并将其转发给基类 `_Packaged_state` 的构造函数。 292 | 293 | **`_Packaged_state` 类型只有一个数据成员 `std::function` 类型的对象 `_Fn`,它用来存储需要执行的异步任务**,而它又继承自 `_Associated_state`。 294 | 295 | ```cpp 296 | template 297 | class _Packaged_state<_Ret(_ArgTypes...)> 298 | : public _Associated_state<_Ret> 299 | ``` 300 | 301 | ```mermaid 302 | classDiagram 303 | class _Associated_state { 304 | ... 305 | } 306 | 307 | class _Packaged_state { 308 | -std::function _Fn 309 | } 310 | 311 | class _Task_async_state { 312 | -::Concurrency::task _Task 313 | } 314 | 315 | class _Deferred_async_state { 316 | } 317 | 318 | _Associated_state <|-- _Packaged_state : 继承 319 | _Packaged_state <|-- _Task_async_state : 继承 320 | _Packaged_state <|-- _Deferred_async_state : 继承 321 | 322 | ``` 323 | 324 | 我们直接先看 `_Task_async_state` 与 `_Deferred_async_state` 类型的构造函数实现即可: 325 | 326 | ```cpp 327 | template 328 | _Task_async_state(_Fty2&& _Fnarg) : _Mybase(_STD forward<_Fty2>(_Fnarg)) { 329 | _Task = ::Concurrency::create_task([this]() { // do it now 330 | this->_Call_immediate(); 331 | }); 332 | 333 | this->_Running = true; 334 | } 335 | template 336 | _Deferred_async_state(const _Fty2& _Fnarg) : _Packaged_state<_Rx()>(_Fnarg) {} 337 | 338 | template 339 | _Deferred_async_state(_Fty2&& _Fnarg) : _Packaged_state<_Rx()>(_STD forward<_Fty2>(_Fnarg)) {} 340 | ``` 341 | 342 | `_Task_async_state` 它的数据成员: 343 | 344 | ```cpp 345 | private: 346 | ::Concurrency::task _Task; 347 | ``` 348 | 349 | `_Task_async_state` 的实现使用到了微软自己实现的 [并行模式库](https://learn.microsoft.com/zh-cn/cpp/parallel/concrt/parallel-patterns-library-ppl?view=msvc-170)(PPL),简而言之 `launch::async` 策略并不是单纯的创建线程让任务执行,而是使用了微软的 `::Concurrency::create_task` ,它从**线程池**中获取线程并执行任务返回包装对象。 350 | 351 | `this->_Call_immediate();` 是调用 `_Task_async_state` 的父类 `_Packaged_state` 的成员函数 `_Call_immediate` 。 352 | 353 | **`_Packaged_state` 有三个偏特化**,`_Call_immediate` 自然也拥有三个不同版本,用来应对我们传入的函数对象**返回类型**的三种情况: 354 | 355 | - 返回普通类型 [`_Packaged_state<_Ret(_ArgTypes...)>`](https://github.com/microsoft/STL/blob/f54203f/stl/inc/future#L554) 356 | 357 | ```cpp 358 | void _Call_immediate(_ArgTypes... _Args) { 359 | _TRY_BEGIN 360 | // 调用函数对象并捕获异常 传递返回值 361 | this->_Set_value(_Fn(_STD forward<_ArgTypes>(_Args)...), false); 362 | _CATCH_ALL 363 | // 函数对象抛出异常就记录 364 | this->_Set_exception(_STD current_exception(), false); 365 | _CATCH_END 366 | } 367 | ``` 368 | 369 | - 返回引用类型 [`_Packaged_state<_Ret&(_ArgTypes...)>`](https://github.com/microsoft/STL/blob/f54203f/stl/inc/future#L510) 370 | 371 | ```cpp 372 | void _Call_immediate(_ArgTypes... _Args) { 373 | _TRY_BEGIN 374 | // 调用函数对象并捕获异常 传递返回值的地址 375 | this->_Set_value(_STD addressof(_Fn(_STD forward<_ArgTypes>(_Args)...)), false); 376 | _CATCH_ALL 377 | // 函数对象抛出异常就记录 378 | this->_Set_exception(_STD current_exception(), false); 379 | _CATCH_END 380 | } 381 | ``` 382 | 383 | - 返回 void 类型 [`_Packaged_state`](https://github.com/microsoft/STL/blob/f54203f/stl/inc/future#L554) 384 | 385 | ```cpp 386 | void _Call_immediate(_ArgTypes... _Args) { // call function object 387 | _TRY_BEGIN 388 | // 调用函数对象并捕获异常 因为返回 void 不获取返回值 而是直接 _Set_value 传递一个 1 389 | _Fn(_STD forward<_ArgTypes>(_Args)...); 390 | this->_Set_value(1, false); 391 | _CATCH_ALL 392 | // 函数对象抛出异常就记录 393 | this->_Set_exception(_STD current_exception(), false); 394 | _CATCH_END 395 | } 396 | ``` 397 | 398 | 说白了,无非是把返回引用类型的可调用对象返回的引用获取地址传递给 `_Set_value`,把返回 void 类型的可调用对象传递一个 1 表示正确执行的状态给 `_Set_value`。 399 | 400 | `_Call_immediate` 则又调用了父类 `_Associated_state` 的成员函数(`_Set_value`、`_set_exception`),传递的可调用对象执行结果,以及可能的异常,将结果或异常存储在 `_Associated_state` 中。 401 | 402 | `_Deferred_async_state` 并不会在线程中执行任务,但它同样调用 `_Call_immediate` 函数执行保有的函数对象,它有一个 `_Run_deferred_function` 函数: 403 | 404 | ```cpp 405 | void _Run_deferred_function(unique_lock& _Lock) override { // run the deferred function 406 | _Lock.unlock(); 407 | _Packaged_state<_Rx()>::_Call_immediate(); 408 | _Lock.lock(); 409 | } 410 | ``` 411 | 412 | 然后也就和上面说的没什么区别了 。 413 | 414 | 7. 返回 `std::future` 415 | 416 | `return future<_Ret>(_From_raw_state_tag{}, _Pr._Get_state_for_future());` 417 | 418 | 它选择到了 `std::future` 的构造函数是: 419 | 420 | ```cpp 421 | future(_From_raw_state_tag, const _Mybase& _State) noexcept : _Mybase(_State, true) {} 422 | ``` 423 | 424 | > ```cpp 425 | > using _Mybase = _State_manager<_Ty*>; 426 | > ``` 427 | 428 | `_From_raw_state_tag` 是一个空类,并没有什么特殊作用,只是为了区分重载。 429 | 430 | `_Get_state_for_future` 代码如下: 431 | 432 | ```cpp 433 | _State_manager<_Ty>& _Get_state_for_future() { 434 | if (!_State.valid()) { 435 | _Throw_future_error2(future_errc::no_state); 436 | } 437 | 438 | if (_Future_retrieved) { 439 | _Throw_future_error2(future_errc::future_already_retrieved); 440 | } 441 | 442 | _Future_retrieved = true; 443 | return _State; 444 | } 445 | ``` 446 | 447 | 检查状态,修改状态,返回底层 `_State` ,完成转移状态。 448 | 449 | 总而言之这行代码通过调用 `std::future` 的特定构造函数,将 `_Promise` 对象中的 `_State_manager` 状态转移到 `std::future` 对象中,从而创建并返回一个 `std::future` 对象。这使得 `std::future` 可以访问并管理异步任务的状态,包括获取任务的结果或异常,并等待任务的完成。 450 | 451 | ## `std::future` 452 | 453 | 先前的 `std::async` 的内容非常之多,希望各位开发者不要搞晕了,其实重中之重主要是那几个类,关系图如下: 454 | 455 | ```plaintext 456 | +---------------------+ 457 | | _Promise<_Ty> | 458 | |---------------------| 459 | | - _State | -----> +---------------------+ 460 | | - _Future_retrieved | | _State_manager<_Ty> | 461 | +---------------------+ |----------------------| 462 | | - _Assoc_state | -----> +-------------------------+ 463 | | - _Get_only_once | | _Associated_state<_Ty>* | 464 | +----------------------+ +-------------------------+ 465 | ``` 466 | 467 | > `_Promise`、_`State_manager`、`_Associated_state` 之间的**包含关系示意图**。 468 | 469 | ```mermaid 470 | classDiagram 471 | class _Associated_state { 472 | ... 473 | } 474 | 475 | class _Packaged_state { 476 | -std::function _Fn 477 | } 478 | 479 | class _Task_async_state { 480 | -::Concurrency::task _Task 481 | } 482 | 483 | class _Deferred_async_state { 484 | } 485 | 486 | _Associated_state <|-- _Packaged_state : 继承 487 | _Packaged_state <|-- _Task_async_state : 继承 488 | _Packaged_state <|-- _Deferred_async_state : 继承 489 | 490 | ``` 491 | 492 | > `_Asscociated_state`、`_Packaged_state`、`_Task_async_state`、`_Deferred_async_state` **继承关系示意图**。 493 | 494 | 这其中的 `_Associated_state`、`_State_manager` 类型是我们的核心,它在后续 `std::future` 乃至其它并发设施都有众多使用。 495 | 496 | --- 497 | 498 | 介绍 `std::future` 的源码我认为无需过多篇幅或者示例,引入过多的源码实现等等从头讲解,只会让各位开发者感觉复杂难。 499 | 500 | 我们直接从它的最重要、常见的 `get()`、`wait()` 成员函数开始即可。 501 | 502 | ```cpp 503 | std::future future = std::async([] { return 0; }); 504 | future.get(); 505 | ``` 506 | 507 | 我们先前已经详细介绍过了 `std::async` 返回 `std::future` 的步骤。以上这段代码,唯一的问题是:*`future.get()` 做了什么?* 508 | 509 | ```cpp 510 | _EXPORT_STD template 511 | class future : public _State_manager<_Ty> { 512 | // class that defines a non-copyable asynchronous return object that holds a value 513 | private: 514 | using _Mybase = _State_manager<_Ty>; 515 | 516 | public: 517 | static_assert(!is_array_v<_Ty> && is_object_v<_Ty> && is_destructible_v<_Ty>, 518 | "T in future must meet the Cpp17Destructible requirements (N4950 [futures.unique.future]/4)."); 519 | 520 | future() = default; 521 | 522 | future(future&& _Other) noexcept : _Mybase(_STD move(_Other), true) {} 523 | 524 | future& operator=(future&&) = default; 525 | 526 | future(_From_raw_state_tag, const _Mybase& _State) noexcept : _Mybase(_State, true) {} 527 | 528 | _Ty get() { 529 | // block until ready then return the stored result or throw the stored exception 530 | future _Local{_STD move(*this)}; 531 | return _STD move(_Local._Get_value()); 532 | } 533 | 534 | _NODISCARD shared_future<_Ty> share() noexcept { 535 | return shared_future<_Ty>(_STD move(*this)); 536 | } 537 | 538 | future(const future&) = delete; 539 | future& operator=(const future&) = delete; 540 | }; 541 | ``` 542 | 543 | > `std::future` 其实还有两种特化,不过整体大差不差。 544 | > 545 | > ```cpp 546 | > template 547 | > class future<_Ty&> : public _State_manager<_Ty*> 548 | > ``` 549 | > 550 | > ```cpp 551 | > template <> 552 | > class future : public _State_manager 553 | > ``` 554 | > 555 | > 也就是对返回类型为引用和 void 的情况了。其实先前已经聊过很多次了,无非就是内部的返回引用实际按指针操作,返回 void,那么也得给个 1。参见前面的 `_Call_immediate` 实现。 556 | 557 | 可以看到 `std::future` 整体代码实现很少,很简单,那是因为其实现细节都在其父类 `_State_manager` 。然而 `_State_manager` 又保有一个 `_Associated_state<_Ty>*` 类型的成员。而 **`_Associated_state` 又是一切的核心**,之前已经详细描述过了。 558 | 559 | 阅读 `std::future` 的源码你可能注意到了一个问题:*没有 `wait()`*成员函数?* 560 | 561 | 它的定义来自于父类 `_State_manager` : 562 | 563 | ```cpp 564 | void wait() const { // wait for signal 565 | if (!valid()) { 566 | _Throw_future_error2(future_errc::no_state); 567 | } 568 | 569 | _Assoc_state->_Wait(); 570 | } 571 | ``` 572 | 573 | 然而这还不够,实际上还需要调用了 `_Associated_state` 的 `wait()` 成员函数: 574 | 575 | ```cpp 576 | virtual void _Wait() { // wait for signal 577 | unique_lock _Lock(_Mtx); 578 | _Maybe_run_deferred_function(_Lock); 579 | while (!_Ready) { 580 | _Cond.wait(_Lock); 581 | } 582 | } 583 | ``` 584 | 585 | 先使用锁进行保护,然后调用函数,再循环等待任务执行完毕。`_Maybe_run_deferred_function`: 586 | 587 | ```cpp 588 | void _Maybe_run_deferred_function(unique_lock& _Lock) { // run a deferred function if not already done 589 | if (!_Running) { // run the function 590 | _Running = true; 591 | _Run_deferred_function(_Lock); 592 | } 593 | } 594 | ``` 595 | 596 | `_Run_deferred_function` 相信你不会陌生,在讲述 `std::async` 源码中其实已经提到了,就是解锁然后调用 `_Call_immediate` 罢了。 597 | 598 | ```cpp 599 | void _Run_deferred_function(unique_lock& _Lock) override { // run the deferred function 600 | _Lock.unlock(); 601 | _Packaged_state<_Rx()>::_Call_immediate(); 602 | _Lock.lock(); 603 | } 604 | ``` 605 | 606 | > `_Call_immediate` 就是执行我们实际传入的函数对象,先前已经提过。 607 | 608 | 在 `_Wait` 函数中调用 `_Maybe_run_deferred_function` 是为了确保延迟执行(`launch::deferred`)的任务能够在等待前被启动并执行完毕。这样,在调用 `wait` 时可以正确地等待任务完成。 609 | 610 | 至于下面的循环等待部分: 611 | 612 | ```cpp 613 | while (!_Ready) { 614 | _Cond.wait(_Lock); 615 | } 616 | ``` 617 | 618 | 这段代码使用了条件变量、互斥量、以及一个状态对象,主要目的有两个: 619 | 620 | 1. **避免虚假唤醒**: 621 | - 条件变量的 `wait` 函数在被唤醒后,会重新检查条件(即 `_Ready` 是否为 `true`),确保只有在条件满足时才会继续执行。这防止了由于虚假唤醒导致的错误行为。 622 | 2. **等待 `launch::async` 的任务在其它线程执行完毕**: 623 | - 对于 `launch::async` 模式的任务,这段代码确保当前线程会等待任务在另一个线程中执行完毕,并接收到任务完成的信号。只有当任务完成并设置 `_Ready` 为 `true` 后,条件变量才会被通知,从而结束等待。 624 | 625 | 这样,当调用 `wait` 函数时,可以保证无论任务是 `launch::deferred` 还是 `launch::async` 模式,当前线程都会正确地等待任务的完成信号,然后继续执行。 626 | 627 | --- 628 | 629 | `wait()` 介绍完了,那么接下来就是 `get()` : 630 | 631 | ```cpp 632 | // std::future 633 | void get() { 634 | // block until ready then return or throw the stored exception 635 | future _Local{_STD move(*this)}; 636 | _Local._Get_value(); 637 | } 638 | // std::future 639 | _Ty get() { 640 | // block until ready then return the stored result or throw the stored exception 641 | future _Local{_STD move(*this)}; 642 | return _STD move(_Local._Get_value()); 643 | } 644 | // std::future 645 | _Ty& get() { 646 | // block until ready then return the stored result or throw the stored exception 647 | future _Local{_STD move(*this)}; 648 | return *_Local._Get_value(); 649 | } 650 | ``` 651 | 652 | 在第四章的 “*future 的状态变化*”一节中我们也详细聊过 `get()` 成员函数。由于 future 本身有三个特化,`get()` 成员函数自然那也有三个版本,不过总体并无多大区别。 653 | 654 | 它们都是将当前对象(`*this`)的共享状态转移给了这个局部对象 `_Local`,然后再去调用父类`_State_manager` 的成员函数 `_Get_value()` 获取值并返回。而局部对象 `_Local` 在函数结束时析构。这意味着当前对象(`*this`)失去共享状态,并且状态被完全销毁。 655 | 656 | `_Get_value()` : 657 | 658 | ```cpp 659 | _Ty& _Get_value() const { 660 | if (!valid()) { 661 | _Throw_future_error2(future_errc::no_state); 662 | } 663 | 664 | return _Assoc_state->_Get_value(_Get_only_once); 665 | } 666 | ``` 667 | 668 | 先进行一下状态判断,如果拥有共享状态则继续,调用 `_Assoc_state` 的成员函数 `_Get_value` ,传递 `_Get_only_once` 参数,其实就是代表这个成员函数只能调用一次,次参数是里面进行状态判断的而已。 669 | 670 | `_Assoc_state` 的类型是 ` _Associated_state<_Ty>* ` ,是一个指针类型,它实际会指向自己的子类对象,我们在讲 `std::async` 源码的时候提到了,它必然指向 `_Deferred_async_state` 或者 `_Task_async_state`。 671 | 672 | `_Assoc_state->_Get_value` 这其实是个多态调用,父类有这个虚函数: 673 | 674 | ```cpp 675 | virtual _Ty& _Get_value(bool _Get_only_once) { 676 | unique_lock _Lock(_Mtx); 677 | if (_Get_only_once && _Retrieved) { 678 | _Throw_future_error2(future_errc::future_already_retrieved); 679 | } 680 | 681 | if (_Exception) { 682 | _STD rethrow_exception(_Exception); 683 | } 684 | 685 | // TRANSITION: `_Retrieved` should be assigned before `_Exception` is thrown so that a `future::get` 686 | // that throws a stored exception invalidates the future (N4950 [futures.unique.future]/17) 687 | _Retrieved = true; 688 | _Maybe_run_deferred_function(_Lock); 689 | while (!_Ready) { 690 | _Cond.wait(_Lock); 691 | } 692 | 693 | if (_Exception) { 694 | _STD rethrow_exception(_Exception); 695 | } 696 | 697 | if constexpr (is_default_constructible_v<_Ty>) { 698 | return _Result; 699 | } else { 700 | return _Result._Held_value; 701 | } 702 | } 703 | 704 | ``` 705 | 706 | 但是子类 `_Task_async_state` 进行了重写,以 `launch::async` 策略创建的 future,那么实际会调用 `_Task_async_state::_Get_value` : 707 | 708 | ```cpp 709 | _State_type& _Get_value(bool _Get_only_once) override { 710 | // return the stored result or throw stored exception 711 | _Task.wait(); 712 | return _Mybase::_Get_value(_Get_only_once); 713 | } 714 | ``` 715 | 716 | > `_Deferred_async_state` 则没有进行重写,就是直接调用父类虚函数。 717 | 718 | `_Task` 就是 `::Concurrency::task _Task;`,调用 `wait()` 成员函数确保任务执行完毕。 719 | 720 | `_Mybase::_Get_value(_Get_only_once)` 其实又是回去调用父类的虚函数了。 721 | 722 | > ### `_Get_value` 方法详细解释 723 | 724 | 1. **状态检查**: 725 | - 如果`_Get_only_once`为真并且结果已被检索过,则抛出`future_already_retrieved`异常。 726 | 2. **异常处理**: 727 | - 如果存在存储的异常,重新抛出该异常。 728 | 3. **标记结果已被检索**: 729 | - 将`_Retrieved`设置为`true`。 730 | 4. **执行延迟函数**: 731 | - 调用`_Maybe_run_deferred_function`来运行可能的延迟任务。这个函数很简单,就是单纯的执行延时任务而已,在讲述 `wait` 成员函数的时候已经讲完了。 732 | 5. **等待结果就绪**: 733 | - 如果结果尚未准备好,等待条件变量通知结果已就绪。(这里和 `std::async` 和 `std::future` 的组合无关,因为如果是 `launch::async` 模式创建的任务,重写的 `_Get_value` 是先调用了 `_Task.wait();` 确保异步任务执行完毕,此处根本无需等待它) 734 | 6. **再次检查异常**: 735 | - 再次检查是否有存储的异常,并重新抛出它。 736 | 7. **返回结果**: 737 | - 如果`_Ty`是默认可构造的,返回结果`_Result`。 738 | - 否则,返回`_Result._Held_value`。 739 | 740 | `_Result` 是通过执行 `_Call_immediate` 函数,然后 `_Call_immediate` 再执行 `_Set_value` ,`_Set_value` 再执行 `_Emplace_result`,`_Emplace_result` 再执行 `_Emplace_result` 获取到我们执行任务的值的。以 `Ty` 的偏特化为例: 741 | 742 | ```cpp 743 | // _Packaged_state 744 | void _Call_immediate(_ArgTypes... _Args) { 745 | _TRY_BEGIN 746 | // 调用函数对象并捕获异常 传递返回值 747 | this->_Set_value(_Fn(_STD forward<_ArgTypes>(_Args)...), false); 748 | _CATCH_ALL 749 | // 函数对象抛出异常就记录 750 | this->_Set_exception(_STD current_exception(), false); 751 | _CATCH_END 752 | } 753 | 754 | // _Asscoiated_state 755 | void _Set_value(const _Ty& _Val, bool _At_thread_exit) { // store a result 756 | unique_lock _Lock(_Mtx); 757 | _Set_value_raw(_Val, &_Lock, _At_thread_exit); 758 | } 759 | void _Set_value_raw(const _Ty& _Val, unique_lock* _Lock, bool _At_thread_exit) { 760 | // store a result while inside a locked block 761 | if (_Already_has_stored_result()) { 762 | _Throw_future_error2(future_errc::promise_already_satisfied); 763 | } 764 | 765 | _Emplace_result(_Val); 766 | _Do_notify(_Lock, _At_thread_exit); 767 | } 768 | template 769 | void _Emplace_result(_Ty2&& _Val) { 770 | // TRANSITION, incorrectly assigns _Result when _Ty is default constructible 771 | if constexpr (is_default_constructible_v<_Ty>) { 772 | _Result = _STD forward<_Ty2>(_Val); // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 773 | } else { 774 | ::new (static_cast(_STD addressof(_Result._Held_value))) _Ty(_STD forward<_Ty2>(_Val)); 775 | _Has_stored_result = true; 776 | } 777 | } 778 | ``` 779 | 780 | ## 总结 781 | 782 | 好了,到此也就可以了。 783 | 784 | 你不会期待我们将每一个成员函数都分析一遍吧?首先是没有必要,其次是篇幅限制。 785 | 786 | `std::future` 的继承关系让人感到头疼,但是如果耐心的看了一遍,全部搞明白了继承关系, `std::async` 如何创建的 `std::future` 也就没有问题了。 787 | 788 | 其实各位不用着急完全理解,可以慢慢看,至少有许多的显著的信息,比如: 789 | 790 | 1. `sttd::future` 的很多部分,如 `get()` 成员函数实现中,实际使用了虚函数。 791 | 2. `std::async` 创建 `std::future` 对象中,内部其实也有父类指针指向子类对象,以及多态调用。 792 | 3. `std::async` 的非延迟执行策略,使用到了自家的 PPL 库。 793 | 4. 微软的 `std::async` 策略实现并不符合标准,不区分 `launch::async | launch::deferred` 和 `launch::async`。 794 | 5. `std::future` 内部使用到了互斥量、条件变量、异常指针等设施。 795 | -------------------------------------------------------------------------------- /md/05内存模型与原子操作.md: -------------------------------------------------------------------------------- 1 | # 内存模型与原子操作 2 | 3 | - 内存模型定义了多线程程序中,读写操作如何在不同线程之间可见,以及这些操作在何种顺序下执行。内存模型确保程序的行为在并发环境下是可预测的。 4 | 5 | - 原子操作即**不可分割的操作**。系统的所有线程,不可能观察到原子操作完成了一半。 6 | 7 | 最基础的概念就是如此,这里不再过多赘述,后续还会详细展开内存模型的问题。 8 | 9 | ## 原子操作 10 | 11 | ```cpp 12 | int a = 0; 13 | void f(){ 14 | ++a; 15 | } 16 | ``` 17 | 18 | 显然,`++a` 是非原子操作,也就是说在多线程中可能会被另一个线程观察到只完成一半。 19 | 20 | 1. 线程 A 和线程 B 同时开始修改变量 `a` 的值。 21 | 2. 线程 A 对 `a` 执行递增操作,但还未完成。 22 | 3. 在线程 A 完成递增操作之前,线程 B 也执行了递增操作。 23 | 4. 线程 C 读取 `a` 的值。 24 | 25 | 线程 C 到底读取到多少不确定,a 的值是多少也不确定。显然,这构成了数据竞争,出现了[未定义行为](https://zh.cppreference.com/w/cpp/language/ub)。 26 | 27 | 在之前的内容中,我们讲述了使用很多同步设施,如互斥量,来保护共享资源。 28 | 29 | ```cpp 30 | std::mutex m; 31 | void f() { 32 | std::lock_guard lc{ m }; 33 | ++a; 34 | } 35 | ``` 36 | 37 | 通过互斥量的保护,即使 `++a` 本身不是原子操作,**逻辑上也可视为原子操作**。互斥量确保了对共享资源的读写是线程安全的,避免了数据竞争问题。 38 | 39 | 不过这显然不是我们的重点。我们想要的是一种**原子类型**,它的所有操作都直接是**原子**的,不需要额外的同步设施进行保护。C++11 引入了原子类型 [`std::atomic`](https://zh.cppreference.com/w/cpp/atomic/atomic),在下节我们会详细讲解。 40 | 41 | ### 原子类型 `std::atomic` 42 | 43 | 标准原子类型定义在头文件 [``](https://zh.cppreference.com/w/cpp/header/atomic) 中。这些类型的操作都是原子的,语言定义中只有这些类型的操作是原子的,虽然也可以用互斥量来模拟原子操作(见上文)。 44 | 45 | 标准原子类型的实现通常包括一个 `is_lock_free()` 成员函数,允许用户查询特定原子类型的操作是否是通过直接的原子指令实现(返回 true),还是通过锁来实现(返回 false)。 46 | 47 | > **如果一个线程写入原子对象,同时另一线程从它读取,那么行为有良好定义**(数据竞争的细节见[内存模型](https://zh.cppreference.com/w/cpp/language/memory_model))。 48 | 49 | 原子操作可以在一些时候代替互斥量,来进行同步操作,也能带来更高的性能。但是如果它的内部使用互斥量实现,那么不可能有性能的提升。 50 | 51 | 在 C++17 中,所有原子类型都有一个 `static constexpr` 的数据成员 [`is_always_lock_free`](https://zh.cppreference.com/w/cpp/atomic/atomic/is_always_lock_free) 。如果当前环境上的原子类型 X 是无锁类型,那么 `X::is_always_lock_free` 将返回 `true` 。例如: 52 | 53 | ```cpp 54 | std::atomic::is_always_lock_free // true 或 false 55 | ``` 56 | 57 | 标准库还提供了一组宏 [`ATOMIC_xxx_LOCK_FREE`](https://zh.cppreference.com/w/cpp/atomic/atomic_is_lock_free) ,在编译时对各种整数原子类型是否无锁进行判断。 58 | 59 | ```cpp 60 | // (C++11 起) 61 | #define ATOMIC_BOOL_LOCK_FREE /* 未指定 */ 62 | #define ATOMIC_CHAR_LOCK_FREE /* 未指定 */ 63 | #define ATOMIC_CHAR16_T_LOCK_FREE /* 未指定 */ 64 | #define ATOMIC_CHAR32_T_LOCK_FREE /* 未指定 */ 65 | #define ATOMIC_WCHAR_T_LOCK_FREE /* 未指定 */ 66 | #define ATOMIC_SHORT_LOCK_FREE /* 未指定 */ 67 | #define ATOMIC_INT_LOCK_FREE /* 未指定 */ 68 | #define ATOMIC_LONG_LOCK_FREE /* 未指定 */ 69 | #define ATOMIC_LLONG_LOCK_FREE /* 未指定 */ 70 | #define ATOMIC_POINTER_LOCK_FREE /* 未指定 */ 71 | // (C++20 起) 72 | #define ATOMIC_CHAR8_T_LOCK_FREE /* 未指定 */ 73 | ``` 74 | 75 | - 对于一定**有锁**的内建原子类型是 0; 76 | - 对于**有时无锁**的内建原子类型是 1; 77 | - 对于一定**无锁**的内建原子类型是 2。 78 | 79 | 我们可以使用这些宏来对代码进行编译时的优化和检查,以确保在特定平台上原子操作的性能。例如,如果我们知道某些操作在目标平台上是无锁的,那么我们可以利用这一点进行性能优化。如果这些操作在目标平台上是有锁的,我们可能会选择其它同步机制。 80 | 81 | ```cpp 82 | // 检查 std::atomic 是否总是无锁 83 | if constexpr(std::atomic::is_always_lock_free) { 84 | std::cout << "当前环境 std::atomic 始终是无锁" << std::endl; 85 | } 86 | else { 87 | std::cout << "当前环境 std::atomic 并不总是无锁" << std::endl; 88 | } 89 | 90 | // 使用 ATOMIC_INT_LOCK_FREE 宏进行编译时检查 91 | #if ATOMIC_INT_LOCK_FREE == 2 92 | std::cout << "int 类型的原子操作一定无锁的。" << std::endl; 93 | #elif ATOMIC_INT_LOCK_FREE == 1 94 | std::cout << "int 类型的原子操作有时是无锁的。" << std::endl; 95 | #else 96 | std::cout << "int 类型的原子操作一定有锁的。" << std::endl; 97 | #endif 98 | ``` 99 | 100 | > [运行](https://godbolt.org/z/q5x7Wfd5r)测试。 101 | 102 | 如你所见,我们写了一个简单的示例,展示了如何使用 C++17 的静态数据成员 `is_always_lock_free` 和预处理宏来让程序执行不同的代码。 103 | 104 | 因为 `is_always_lock_free` 是编译期常量,所以我们可以使用 C++17 引入的 `constexpr if` ,它可以在编译阶段进行决策,避免了运行时的判断开销,提高了性能。 105 | 106 | 宏则更是简单了,最基本的预处理器判断,在预处理阶段就选择编译合适的代码。 107 | 108 | 在实际应用中,如果一个类型的原子操作总是无锁的,我们可以更放心地在性能关键的代码路径中使用它。例如,在高频交易系统、实时系统或者其它需要高并发性能的场景中,无锁的原子操作可以显著减少锁的开销和竞争,提高系统的吞吐量和响应时间。 109 | 110 | 另一方面,如果发现某些原子类型在目标平台上是有锁的,我们可以考虑以下优化策略: 111 | 112 | 1. **使用不同的数据结构**:有时可以通过改变数据结构来避免对原子操作的依赖。 113 | 2. **减少原子操作的频率**:通过批处理等技术,减少对原子操作的调用次数。 114 | 3. **使用更高效的同步机制**:在一些情况下,其它同步机制(如读写锁)可能比原子操作更高效。 115 | 116 | 当然,其实很多时候根本没这种性能的担忧,我们很多时候使用原子对象只是为了简单方便,比如 `std::atomic` 表示状态、`std::atomic` 进行计数等。即使它们是用了锁,那也是封装好了的,起码用着方便,而不需要在代码中引入额外的互斥量来保护,更加简洁。这也是很正常的需求,各位不但要考虑程序的性能,同时也要考虑代码的简洁性、易用性。即使使用原子类型无法带来效率的提升,那也没有负提升。 117 | 118 | --- 119 | 120 | 除了直接使用 `std::atomic` 模板外,也可以使用原子类型的别名。这个数量非常之多,见 [MSVC STL](https://github.com/microsoft/STL/blob/daeb0a6/stl/inc/atomic#L2745-L2805)。 121 | 122 | 对于标准内建类型的别名,就是在原子类型的类型名前面加上 `atomic_` 的前缀:`atomic_T`。不过 `signed` 缩写 `s`、`unsigned` 缩写 `u`、`long long` 缩写 `llong`。 123 | 124 | ```cpp 125 | using atomic_char = atomic; 126 | using atomic_schar = atomic; 127 | using atomic_uchar = atomic; 128 | using atomic_short = atomic; 129 | using atomic_ushort = atomic; 130 | using atomic_int = atomic; 131 | using atomic_uint = atomic; 132 | using atomic_long = atomic; 133 | using atomic_ulong = atomic; 134 | using atomic_llong = atomic; 135 | using atomic_ullong = atomic; 136 | ``` 137 | 138 | --- 139 | 140 | 通常 `std::atomic` 对象不可进行复制、移动、赋值,因为它们的[复制构造](https://zh.cppreference.com/w/cpp/atomic/atomic/atomic)与[复制赋值运算符](https://zh.cppreference.com/w/cpp/atomic/atomic/operator%3D)被定义为[弃置](https://zh.cppreference.com/w/cpp/language/function#.E5.BC.83.E7.BD.AE.E5.87.BD.E6.95.B0)的。不过可以**隐式转换**成对应的内置类型,因为它有[转换函数](https://zh.cppreference.com/w/cpp/atomic/atomic/operator_T)。 141 | 142 | ```cpp 143 | atomic(const atomic&) = delete; 144 | atomic& operator=(const atomic&) = delete; 145 | operator T() const noexcept; 146 | ``` 147 | 148 | 可以使用 `load()`、`store()`、`exchange()`、`compare_exchange_weak()` 和 `compare_exchange_strong()` 等成员函数对 `std::atomic` 进行操作。如果是[整数类型](https://zh.cppreference.com/w/cpp/atomic/atomic#.E7.89.B9.E5.8C.96.E6.88.90.E5.91.98.E5.87.BD.E6.95.B0)的特化,还支持 `++`、`--`、`+=`、`-=`、`&=`、`|=`、`^=` 、`fetch_add`、`fetch_sub` 等操作方式。在后面详细的展开使用。 149 | 150 | `std::atomic` 类模板不仅只能使用标准库为我们定义的特化类型,我们也完全可以自定义类型创建对应的原子对象。不过因为是通用模板,操作仅限 `load()`、`store()`、`exchange()`、`compare_exchange_weak()` 、 `compare_exchange_strong()`,以及一个转换函数。 151 | 152 | 模板 `std::atomic` 可用任何满足[*可复制构造 (CopyConstructible)*](https://zh.cppreference.com/w/cpp/named_req/CopyConstructible)及[*可复制赋值 (CopyAssignable)*](https://zh.cppreference.com/w/cpp/named_req/CopyAssignable)的[*可平凡复制 (TriviallyCopyable)*](https://zh.cppreference.com/w/cpp/named_req/TriviallyCopyable)类型 `T` 实例化。 153 | 154 | ```cpp 155 | struct trivial_type { 156 | int x{}; 157 | float y{}; 158 | 159 | trivial_type() {} 160 | 161 | trivial_type(int a, float b) : x{ a }, y{ b } {} 162 | 163 | trivial_type(const trivial_type& other) = default; 164 | 165 | trivial_type& operator=(const trivial_type& other) = default; 166 | 167 | ~trivial_type() = default; 168 | }; 169 | ``` 170 | 171 | 验证自己的类型是否满足 `std::atomic` 要求,我们可以使用[静态断言](https://zh.cppreference.com/w/cpp/language/static_assert): 172 | 173 | ```cpp 174 | static_assert(std::is_trivially_copyable::value, ""); 175 | static_assert(std::is_copy_constructible::value, ""); 176 | static_assert(std::is_move_constructible::value, ""); 177 | static_assert(std::is_copy_assignable::value, ""); 178 | static_assert(std::is_move_assignable::value, ""); 179 | ``` 180 | 181 | 程序能通过编译即代表满足要求。如果不满足要求,静态断言求值中第一个表达式求值为 false,则编译错误。显然我们的类型满足要求,我们可以尝试使用一下它: 182 | 183 | ```cpp 184 | // 创建一个 std::atomic 对象 185 | std::atomic atomic_my_type { trivial_type{ 10, 20.5f } }; 186 | 187 | // 使用 store 和 load 操作来设置和获取值 188 | trivial_type new_value{ 30, 40.5f }; 189 | atomic_my_type.store(new_value); 190 | 191 | trivial_type loadedValue = atomic_my_type.load(); 192 | std::cout << "x: " << loadedValue.x << ", y: " << loadedValue.y << std::endl; 193 | 194 | // 使用 exchange 操作 195 | trivial_type exchanged_value = atomic_my_type.exchange(trivial_type{ 50, 60.5f }); 196 | std::cout << "交换前的 x: " << exchanged_value.x 197 | << ", 交换前的 y: " << exchanged_value.y << std::endl; 198 | std::cout << "交换后的 x: " << atomic_my_type.load().x 199 | << ", 交换后的 y: " << atomic_my_type.load().y << std::endl; 200 | ``` 201 | 202 | > [运行](https://godbolt.org/z/jG59c3b9M)测试。 203 | 204 | 没有问题,不过其实我们的 `trivial_type` 直接改成: 205 | 206 | ```cpp 207 | struct trivial_type { 208 | int x; 209 | float y; 210 | }; 211 | ``` 212 | 213 | > [运行](https://godbolt.org/z/ozPK1qefx)测试。 214 | 215 | 也是完全可以的,满足要求。先前只是为了展示一下显式写明的情况。 216 | 217 | --- 218 | 219 | 原子类型的每个操作函数,都有一个内存序参数,这个参数可以用来指定执行顺序,在后面的内容会详细讲述,现在只需要知道操作分为三类: 220 | 221 | 1. **Store 操作(存储操作)**:可选的内存序包括 `memory_order_relaxed`、`memory_order_release`、`memory_order_seq_cst`。 222 | 223 | 2. **Load 操作(加载操作)**:可选的内存序包括 `memory_order_relaxed`、`memory_order_consume`、`memory_order_acquire`、`memory_order_seq_cst`。 224 | 225 | 3. **Read-modify-write(读-改-写)操作**:可选的内存序包括 `memory_order_relaxed`、`memory_order_consume`、`memory_order_acquire`、`memory_order_release`、`memory_order_acq_rel`、`memory_order_seq_cst`。 226 | 227 | > 本节主要广泛介绍 `std::atomic`,而未展开具体使用。在后续章节中,我们将更详细地讨论一些版本,如 `std::atomic`,并介绍其成员函数和使用方法。 228 | > 229 | > 最后强调一下:任何 [std::atomic](https://zh.cppreference.com/w/cpp/atomic/atomic) 类型,**初始化不是原子操作**。 230 | 231 | ### `std::atomic_flag` 232 | 233 | `std::atomic_flag` 是最简单的原子类型,这个类型的对象可以在两个状态间切换:**设置(true)**和**清除(false)**。它很简单,通常只是用作构建一些库设施,不会单独使用或直接面向普通开发者。 234 | 235 | 在 C++20 之前,`std::atomic_flag` 类型的对象需要以 [`ATOMIC_FLAG_INIT`](https://zh.cppreference.com/w/cpp/atomic/ATOMIC_FLAG_INIT) 初始化,可以确保此时对象处于 236 | "清除"(false)状态。 237 | 238 | ```cpp 239 | std::atomic_flag f = ATOMIC_FLAG_INIT; 240 | ``` 241 | 242 | 在 `C++20` 中 `std::atomic_flag` 的默认[构造函数](https://zh.cppreference.com/w/cpp/atomic/atomic_flag/atomic_flag)保证对象为“清除”(false)状态,就不再需要使用 `ATOMIC_FLAG_INIT`。 243 | 244 | `ATOMIC_FLAG_INIT` 其实并不是什么复杂的东西,它在不同的标准库实现中只是简单的初始化:在 [`MSVC STL`](https://github.com/microsoft/STL/blob/daeb0a6/stl/inc/atomic#L2807-L2808) 它只是一个 `{}`,在 [`libstdc++`](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/atomic_base.h) 与 [`libc++`](https://github.com/llvm/llvm-project/blob/00e80fb/clang/lib/Headers/stdatomic.h#L169) 它只是一个 `{ 0 }`。也就是说我们可以这样初始化: 245 | 246 | ```cpp 247 | std::atomic_flag f ATOMIC_FLAG_INIT; 248 | std::atomic_flag f2 = {}; 249 | std::atomic_flag f3{}; 250 | std::atomic_flag f4{ 0 }; 251 | ``` 252 | 253 | 使用 ATOMIC_FLAG_INIT 宏只是为了统一,我们知道即可。 254 | 255 | 当标志对象已初始化,它只能做三件事情:**销毁、清除、设置**。这些操作对应的函数分别是: 256 | 257 | 1. **`clear()`** (清除):将标志对象的状态原子地更改为清除(false) 258 | 2. **`test_and_set`**(测试并设置):将标志对象的状态原子地更改为设置(true),并返回它先前保有的值。 259 | 3. **销毁**:对象的生命周期结束时,自动调用析构函数进行销毁操作。 260 | 261 | 每个操作都可以指定内存顺序。`clear()` 是一个“读-改-写”操作,可以应用任何内存顺序。默认的内存顺序是 `memory_order_seq_cst`。例如: 262 | 263 | ```cpp 264 | f.clear(std::memory_order_release); 265 | bool r = f.test_and_set(); 266 | ``` 267 | 268 | 1. 将 `f` 的状态原子地更改为清除(false),指明 `memory_order_release` 内存序。 269 | 270 | 2. 将 `f` 的状态原子地更改为设置(true),并返回它先前保有的值给 `r`。使用默认的 `memory_order_seq_cst` 内存序。 271 | 272 | > 不用着急,这里还不是详细展开聊内存序的时候。 273 | 274 | `std::atomic_flag` [不可复制](https://zh.cppreference.com/w/cpp/atomic/atomic_flag/atomic_flag)不可移动[不可赋值](https://zh.cppreference.com/w/cpp/atomic/atomic_flag/operator%3D)。这不是 `std::atomic_flag` 特有的,而是所有原子类型共有的属性。原子类型的所有操作都是原子的,而赋值和复制涉及两个对象,破坏了操作的原子性。复制构造和复制赋值会先读取第一个对象的值,然后再写入另一个对象。对于两个独立的对象,这里实际上有两个独立的操作,合并这两个操作无法保证其原子性。因此,这些操作是不被允许的。 275 | 276 | 有限的特性使得 `std::atomic_flag` 非常适合用作制作**自旋锁**。 277 | 278 | > 自旋锁可以理解为一种***忙等锁***,因为它在等待锁的过程中不会主动放弃 CPU,而是持续检查锁的状态。 279 | > 280 | > 与此相对,`std::mutex` 互斥量是一种***睡眠锁***。当线程请求锁(`lock()`)而未能获取时,它会放弃 CPU 时间片,让其他线程得以执行,从而有效利用系统资源。 281 | > 282 | > 从性能上看,自旋锁的响应更快,但是睡眠锁更加节省资源,高效。 283 | 284 | ```cpp 285 | class spinlock_mutex { 286 | std::atomic_flag flag{}; 287 | public: 288 | spinlock_mutex()noexcept = default; 289 | void lock()noexcept { 290 | while (flag.test_and_set(std::memory_order_acquire)); 291 | } 292 | 293 | void unlock()noexcept { 294 | flag.clear(std::memory_order_release); 295 | } 296 | }; 297 | ``` 298 | 299 | 我们可以简单的使用测试一下,它是有效的: 300 | 301 | ```cpp 302 | spinlock_mutex m; 303 | 304 | void f(){ 305 | std::lock_guard lc{ m }; 306 | std::cout << "😅😅" << "❤️❤️\n"; 307 | } 308 | ``` 309 | 310 | > [运行](https://godbolt.org/z/T583YYTh8)测试。 311 | 312 | 稍微聊一下原理,我们的 `spinlock_mutex` 对象中存储的 `flag` 对象在默认构造时是清除 (`false`) 状态。在 `lock()` 函数中调用 `test_and_set` 函数,它是原子的,只有一个线程能成功调用并将 `flag` 的状态原子地更改为设置 (`true`),并返回它先前的值 (`false`)。此时,该线程成功获取了锁,退出循环。 313 | 314 | 当 `flag` 对象的状态为设置 (`true`) 时,其它线程调用 `test_and_set` 函数会返回 `true`,导致它们继续在循环中自旋,无法退出。直到先前持有锁的线程调用 `unlock()` 函数,将 `flag` 对象的状态原子地更改为清除 (`false`) 状态。此时,等待的线程中会有一个线程成功调用 `test_and_set` 返回 `false`,然后退出循环,成功获取锁。 315 | 316 | > 值得注意的是,我们只是稍微的讲一下使用 `std::atomic_flag` 实现自旋锁。不过并不推荐各位在实践中使用它,具体可参见 [**Linus Torvalds**](https://en.wikipedia.org/wiki/Linus_Torvalds) 的[文章](https://www.realworldtech.com/forum/?threadid=189711&curpostid=189723)。其中有一段话说得很直接: 317 | > 318 | > - **我再说一遍:不要在用户空间中使用自旋锁,除非你真的知道自己在做什么。请注意,你知道自己在做什么的可能性基本上为零。** 319 | > I repeat: **do not use spinlocks in user space, unless you actually know what you're doing**. And be aware that the likelihood that you know what you are doing is basically nil. 320 | > 321 | > 然后就是推荐使用 `std::mutex`、`pthread_mutex` ,比自旋好的多。 322 | 323 | `std::atomic_flag` 的局限性太强,甚至不能当普通的 bool 标志那样使用。一般最好使用 `std::atomic`,下节,我们来使用它。 324 | 325 | ### `std::atomic` 326 | 327 | `std::atomic` 是最基本的**整数原子类型** ,它相较于 `std::atomic_flag` 提供了更加完善的布尔标志。虽然同样不可复制不可移动,但可以使用非原子的 bool 类型进行构造,初始化为 true 或 false,并且能从非原子的 bool 对象赋值给 `std::atomic`: 328 | 329 | ```cpp 330 | std::atomic b{ true }; 331 | b = false; 332 | ``` 333 | 334 | 不过这个 [`operator=`](https://zh.cppreference.com/w/cpp/atomic/atomic/operator%3D) 不同于通常情况,赋值操作 `b = false` 返回一个普通的 `bool` 值。 335 | 336 | > 这个行为不仅仅适用于`std::atomic`,而是适用于所有`std::atomic`类型。 337 | 338 | 如果原子变量的赋值操作返回了一个引用,那么依赖这个结果的代码需要显式地进行加载(load),以确保数据的正确性。例如: 339 | 340 | ```cpp 341 | std::atomicb {true}; 342 | auto& ref = (b = false); // 假设返回 atomic 引用 343 | bool flag = ref.load(); // 那就必须显式调用 load() 加载 344 | ``` 345 | 346 | 通过返回非原子值进行赋值,可以避免多余的加载(load)过程,得到实际存储的值。 347 | 348 | ```cpp 349 | std::atomic b{ true }; 350 | bool new_value = (b = false); // new_value 将是 false 351 | ``` 352 | 353 | 使用 `store` 原子的替换当前对象的值,远好于 `std::atomic_flag` 的 `clear()`。`test_and_set()` 也可以换为更加通用常见的 `exchange`,它可以原子的使用新的值替换已经存储的值,并返回旧值。 354 | 355 | 获取 `std::atomic` 的值有两种方式,调用 `load()` 函数,或者[隐式转换](https://zh.cppreference.com/w/cpp/atomic/atomic/operator_T)。 356 | 357 | `store` 是一个存储操作、`load` 是一个*加载操作*、`exchange` 是一个“*读-改-写*”操作: 358 | 359 | ```cpp 360 | std::atomic b; 361 | bool x = b.load(std::memory_order_acquire); 362 | b.store(true); 363 | x = b.exchange(false, std::memory_order_acq_rel); 364 | ``` 365 | 366 | --- 367 | 368 | `std::atomic` 提供多个“*读-改-写*”的操作,exchange 只是其中之一。它还提供了一种存储方式:**当前值与预期一致时,存储新值。** 369 | 370 | 这种操作叫做“比较/交换”,它的形式表现为 [`compare_exchange_weak()`](https://zh.cppreference.com/w/cpp/atomic/atomic/compare_exchange) 和 `compare_exchang_strong()` 371 | 372 | - **compare_exchange_weak**:尝试将原子对象的当前值与预期值进行*比较*[^1],如果相等则将其更新为新值并返回 `true`;否则,将原子对象的值加载进 expected(进行加载操作)并返回 `false`。**此操作可能会由于某些硬件的特性而出现*假失败*[^2],需要在循环中重试**。 373 | 374 | ```cpp 375 | std::atomic flag{ false }; 376 | bool expected = false; 377 | 378 | while (!flag.compare_exchange_weak(expected, true)); 379 | ``` 380 | 381 | > [运行](https://godbolt.org/z/YToPYf3hd)测试。 382 | 383 | 返回 `false` 即代表出现了*假失败*,因此需要在循环中重试。。 384 | 385 | - **compare_exchange_strong**:类似于 `compare_exchange_weak`,**但不会出现假失败,因此不需要重试**。适用于需要确保操作成功的场合。 386 | 387 | ```cpp 388 | std::atomic flag{ false }; 389 | bool expected = false; 390 | 391 | void try_set_flag() { 392 | // 尝试将 flag 设置为 true,如果当前值为 false 393 | if (flag.compare_exchange_strong(expected, true)) { 394 | std::cout << "flag 为 false,设为 true。\n"; 395 | } 396 | else { 397 | std::cout << "flag 为 true, expected 设为 true。\n"; 398 | } 399 | } 400 | ``` 401 | 402 | > [运行](https://godbolt.org/z/zz4q8vsoe)测试。 403 | 404 | 假设有两个线程运行 `try_set_flag` 函数,那么第一个线程调用 `compare_exchange_strong` 将原子对象 `flag` 设置为 `true`。第二个线程调用 `compare_exchange_strong`,当前原子对象的值为 `true`,而 `expected` 为 `false`,不相等,将原子对象的值设置给 `expected`。此时 `flag` 与 `expected` 均为 `true`。 405 | 406 | 与 `exchange` 的另一个不同是,`compare_exchange_weak` 和 `compare_exchange_strong` 允许指定成功和失败情况下的内存序。这意味着你可以根据成功或失败的情况,为原子操作指定不同的内存序。 407 | 408 | ```cpp 409 | std::atomic data{ false }; 410 | bool expected = false; 411 | 412 | // 成功时的内存序为 memory_order_release,失败时的内存序为 memory_order_acquire 413 | if (data.compare_exchange_weak(expected, true, 414 | std::memory_order_release, std::memory_order_acquire)) { 415 | // 操作成功 416 | } 417 | else { 418 | // 操作失败 419 | } 420 | ``` 421 | 422 | 另一个简单的原子类型是特化的原子指针,即:**`std::atomic`**,下一节我们来看看它是如何工作的。 423 | 424 | [^1]:注: 比较和复制是逐位的(类似 [std::memcmp](https://zh.cppreference.com/w/cpp/string/byte/memcmp) 和 [std::memcpy](https://zh.cppreference.com/w/cpp/string/byte/memcpy));不使用构造函数、赋值运算符或比较运算符。 425 | 426 | [^2]:注:即使 expected 与原子对象的值相等,表现如同 `*this != expected` 427 | 428 | ### `std::atomic` 429 | 430 | `std::atomic` 是一个原子指针类型,`T` 是指针所指向的对象类型。操作是针对 `T` 类型的指针进行的。虽然 `std::atomic` 不能被拷贝和移动,但它可以通过符合类型的指针进行构造和赋值。 431 | 432 | `std::atomic` 拥有以下成员函数: 433 | 434 | - `load()`:以原子方式读取指针值。 435 | - `store()`:以原子方式存储指针值。 436 | - `exchange()`:以原子方式交换指针值。 437 | - `compare_exchange_weak()` 和 `compare_exchange_strong()`:以原子方式比较并交换指针值。 438 | 439 | 这些函数接受并返回的类型都是 **T***。此外,`std::atomic` 还提供了以下操作: 440 | 441 | - `fetch_add`:以原子方式增加指针的值。(`p.fetch_add(1)` 会将指针 `p` 向前移动一个元素,并返回操作前的指针值) 442 | 443 | - `fetch_sub`:以原子方式减少指针的值。返回操作前的指针值。 444 | 445 | - `operator+=` 和 `operator-=`:以原子方式增加或减少指针的值。返回操作后的指针值。 446 | 447 | 这些操作确保在多线程环境下进行安全的指针操作,避免数据竞争和并发问题。 448 | 449 | 使用示例如下: 450 | 451 | ```cpp 452 | struct Foo {}; 453 | 454 | Foo array[5]{}; 455 | std::atomic p{ array }; 456 | 457 | // p 加 2,并返回原始值 458 | Foo* x = p.fetch_add(2); 459 | assert(x == array); 460 | assert(p.load() == &array[2]); 461 | 462 | // p 减 1,并返回操作后的值 463 | x = (p -= 1); 464 | assert(x == &array[1]); 465 | assert(p.load() == &array[1]); 466 | 467 | // 函数也允许内存序作为给定函数的参数 468 | p.fetch_add(3, std::memory_order_release); 469 | ``` 470 | 471 | 这个特化十分简单,我们无需过多赘述。 472 | 473 | ### `std::atomic` 474 | 475 | 在前文中,我们多次提到 `std::shared_ptr`: 476 | 477 | > 第四章中提到:*多个线程能在不同的 shared_ptr 对象上调用**所有成员函数**[^3](包含复制构造函数与复制赋值)而不附加同步,即使这些实例是同一对象的副本且共享所有权也是如此。若多个执行线程访问**同一 shared_ptr** 对象而不同步,且任一线程使用 shared_ptr 的非 const 成员函数,则将出现数据竞争;`std::atomic` 能用于避免数据竞争。[文档](https://zh.cppreference.com/w/cpp/memory/shared_ptr#:~:text=%E5%A4%9A%E4%B8%AA%E7%BA%BF%E7%A8%8B%E8%83%BD%E5%9C%A8%E4%B8%8D%E5%90%8C%E7%9A%84%20shared_ptr%20%E5%AF%B9%E8%B1%A1%E4%B8%8A%E8%B0%83%E7%94%A8%E6%89%80%E6%9C%89%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%EF%BC%88%E5%8C%85%E5%90%AB%E5%A4%8D%E5%88%B6%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E4%B8%8E%E5%A4%8D%E5%88%B6%E8%B5%8B%E5%80%BC%EF%BC%89%E8%80%8C%E4%B8%8D%E9%99%84%E5%8A%A0%E5%90%8C%E6%AD%A5%EF%BC%8C%E5%8D%B3%E4%BD%BF%E8%BF%99%E4%BA%9B%E5%AE%9E%E4%BE%8B%E6%98%AF%E5%90%8C%E4%B8%80%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%89%AF%E6%9C%AC%E4%B8%94%E5%85%B1%E4%BA%AB%E6%89%80%E6%9C%89%E6%9D%83%E4%B9%9F%E6%98%AF%E5%A6%82%E6%AD%A4%E3%80%82%E8%8B%A5%E5%A4%9A%E4%B8%AA%E6%89%A7%E8%A1%8C%E7%BA%BF%E7%A8%8B%E8%AE%BF%E9%97%AE%E5%90%8C%E4%B8%80%20shared_ptr%20%E5%AF%B9%E8%B1%A1%E8%80%8C%E4%B8%8D%E5%90%8C%E6%AD%A5%EF%BC%8C%E4%B8%94%E4%BB%BB%E4%B8%80%E7%BA%BF%E7%A8%8B%E4%BD%BF%E7%94%A8%20shared_ptr%20%E7%9A%84%E9%9D%9E%20const%20%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%EF%BC%8C%E5%88%99%E5%B0%86%E5%87%BA%E7%8E%B0%E6%95%B0%E6%8D%AE%E7%AB%9E%E4%BA%89%EF%BC%9Bstd%3A%3Aatomic%3Cshared_ptr%3E%20%E8%83%BD%E7%94%A8%E4%BA%8E%E9%81%BF%E5%85%8D%E6%95%B0%E6%8D%AE%E7%AB%9E%E4%BA%89%E3%80%82)。* 478 | 479 | 一个在互联网上非常热门的八股问题是:***`std::shared_ptr` 是不是线程安全?*** 480 | 481 | 显然,它并不是完全线程安全的,尽管在多线程环境中有很大的保证,但这还不够。在 C++20 中,原子模板 `std::atomic` 引入了一个偏特化版本 [`std::atomic`](https://zh.cppreference.com/w/cpp/memory/shared_ptr/atomic2) 允许用户原子地操纵 `shared_ptr` 对象。因为它是 `std::atomic` 的特化版本,即使我们还没有深入讲述它,也能知道它是**原子类型**,这意味着它的所有操作都是**原子操作**。 482 | 483 | 若多个执行线程不同步地同时访问**同一** `std::shared_ptr` 对象,且任何这些访问使用了 shared_ptr 的**非 const 成员函数**,则将**出现数据竞争**,**除非通过 `std::atomic` 的实例进行所有访问**。 484 | 485 | ```cpp 486 | class Data { 487 | public: 488 | Data(int value = 0) : value_(value) {} 489 | int get_value() const { return value_; } 490 | void set_value(int new_value) { value_ = new_value; } 491 | private: 492 | int value_; 493 | }; 494 | 495 | auto data = std::make_shared(); 496 | 497 | void writer(){ 498 | for (int i = 0; i < 10; ++i) { 499 | std::shared_ptr new_data = std::make_shared(i); 500 | data.swap(new_data); // 调用非 const 成员函数 501 | std::this_thread::sleep_for(100ms); 502 | } 503 | } 504 | 505 | void reader(){ 506 | for (int i = 0; i < 10; ++i) { 507 | if (data) { 508 | std::cout << "读取线程值: " << data->get_value() << std::endl; 509 | } 510 | else { 511 | std::cout << "没有读取到数据" << std::endl; 512 | } 513 | std::this_thread::sleep_for(100ms); 514 | } 515 | } 516 | 517 | int main(){ 518 | std::thread writer_thread{ writer }; 519 | std::thread reader_thread{ reader }; 520 | 521 | writer_thread.join(); 522 | reader_thread.join(); 523 | } 524 | ``` 525 | 526 | > [运行](https://godbolt.org/z/6zo7hK8h1)测试。 527 | 528 | 以上这段代码是典型的**线程不安全**,它满足: 529 | 530 | 1. 多个线程不同步地同时访问**同一** `std::shared_ptr` 对象 531 | 532 | 2. 任一线程使用 shared_ptr 的**非 const** 成员函数 533 | 534 | 那么**为什么呢**?为什么满足这些概念就是线程不安全呢?为了理解这些概念,首先需要了解 shared_ptr 的内部实现: 535 | 536 | shared_ptr 的通常实现只保有两个指针 537 | 538 | - 指向底层元素的指针([get()](https://zh.cppreference.com/w/cpp/memory/shared_ptr/get)) 所返回的指针) 539 | - 指向*控制块* 的指针 540 | 541 | **控制块**是一个动态分配的对象,其中包含: 542 | 543 | - 指向被管理对象的指针或被管理对象本身 544 | - 删除器(类型擦除) 545 | - 分配器(类型擦除) 546 | - 持有被管理对象的 `shared_ptr` 的数量 547 | - 涉及被管理对象的 `weak_ptr` 的数量 548 | 549 | **控制块是线程安全的**,这意味着多个线程可以安全地操作引用计数和访问管理对象,即使这些 `shared_ptr` 实例是同一对象的副本且共享所有权也是如此。因此,多个线程可以安全地创建、销毁和复制 `shared_ptr` 对象,因为这些操作仅影响控制块中的引用计数。 550 | 551 | 然而,`shared_ptr` 对象实例本身并不是线程安全的。`shared_ptr` 对象实例包含一个指向控制块的指针和一个指向底层元素的指针。这两个指针的操作在多个线程中并没有同步机制。因此,如果多个线程同时访问同一个 `shared_ptr` 对象实例并调用非 `const` 成员函数(如 `reset` 或 `operator=`),这些操作会导致对这些指针的并发修改,进而引发数据竞争。 552 | 553 | 如果不是同一 shared_ptr 对象,每个线程读写的指针也不是同一个,控制块又是线程安全的,那么自然不存在数据竞争,可以安全的调用所有成员函数。 554 | 555 | --- 556 | 557 | 使用 `std::atomic` 修改: 558 | 559 | ```cpp 560 | std::atomic> data = std::make_shared(); 561 | 562 | void writer() { 563 | for (int i = 0; i < 10; ++i) { 564 | std::shared_ptr new_data = std::make_shared(i); 565 | data.store(new_data); // 原子地替换所保有的值 566 | std::this_thread::sleep_for(10ms); 567 | } 568 | } 569 | 570 | void reader() { 571 | for (int i = 0; i < 10; ++i) { 572 | if (auto sp = data.load()) { 573 | std::cout << "读取线程值: " << sp->get_value() << std::endl; 574 | } 575 | else { 576 | std::cout << "没有读取到数据" << std::endl; 577 | } 578 | std::this_thread::sleep_for(10ms); 579 | } 580 | } 581 | ``` 582 | 583 | 很显然,这是线程安全的,`store` 是原子操作,而 `sp->get_value()` 只是个读取操作。 584 | 585 | 我知道,你肯定会想着:*能不能调用 `load()` 成员函数原子地返回底层的 `std::shared_ptr` 再调用 `swap` 成员函数?* 586 | 587 | 可以,但是**没有意义**,因为 `load()` 成员函数返回的是底层 `std::shared_ptr` 的**副本**,也就是一个临时对象。对这个临时对象调用 `swap` 并不会改变 `data` 本身的值,因此这种操作没有实际意义,尽管这不会引发数据竞争(因为是副本)。 588 | 589 | 由于我们没有对读写操作进行同步,只是确保了操作的线程安全,所以多次运行时可能会看到一些无序的打印,这是正常的。 590 | 591 | 不过事实上 `std::atomic` 的功能相当有限,单看它提供的修改接口(`=`、`store`、`load`、`exchang`)就能明白。如果要操作其保护的共享指针指向的资源还是得 `load()` 获取底层共享指针的副本。此时再进行操作时就得考虑 `std::shared_ptr` 本身在多线程的支持了。 592 | 593 | [^3]: 不用感到奇怪,之所以多个线程通过 shared_ptr 的副本可以调用一切成员函数,甚至包括非 const 的成员函数 `operator=`、`reset`,是因为 `shared_ptr` 的**控制块是线程安全的**。 594 | 595 | --- 596 | 597 | 在使用 `std::atomic` 的时候,我们要注意第三章中关于共享数据的一句话: 598 | 599 | > **切勿将受保护数据的指针或引用传递到互斥量作用域之外**,不然保护将**形同虚设**。 600 | 601 | 原子类型也有类似的问题,以下是一个例子: 602 | 603 | ```cpp 604 | std::atomic> ptr = std::make_shared(10); 605 | *ptr.load() = 100; 606 | ``` 607 | 608 | 1. 调用 `load()` 成员函数,原子地返回底层共享指针的**副本** `std::shared_ptr` 609 | 2. 解引用,等价 `*get()`,返回了 `int&` 610 | 3. 直接修改这个引用所指向的资源。 611 | 612 | 在第一步时,已经脱离了 `std::atomic` 的保护,第二步就获取了被保护的数据的引用,第三步进行了修改,这导致了数据竞争。当然了,这种做法非常的愚蠢,只是为了表示,所谓的线程安全,也是要靠**开发者的正确使用**。 613 | 614 | 正确的用法如下: 615 | 616 | ```cpp 617 | std::atomic> ptr = std::make_shared(10); 618 | std::atomic_ref ref{ *ptr.load() }; 619 | ref = 100; // 原子地赋 100 给被引用的对象 620 | ``` 621 | 622 | 通过使用 [`std::atomic_ref`](https://zh.cppreference.com/w/cpp/atomic/atomic_ref) 我们得以确保在修改共享资源时保持操作的原子性,从而避免了数据竞争。 623 | 624 | --- 625 | 626 | 最后再来稍微聊一聊提供的 `wait`、`notify_one` 、`notify_all` 成员函数。这并非是 `std::atomic` 专属,C++20 以后任何 atomic 的特化都拥有这些成员函数,使用起来也都十分的简单,我们这里用一个简单的例子为你展示一下: 627 | 628 | ```cpp 629 | std::atomic> ptr = std::make_shared(); 630 | 631 | void wait_for_wake_up(){ 632 | std::osyncstream{ std::cout } 633 | << "线程 " 634 | << std::this_thread::get_id() 635 | << " 阻塞,等待更新唤醒\n"; 636 | 637 | // 等待 ptr 变为其它值 638 | ptr.wait(ptr.load()); 639 | 640 | std::osyncstream{ std::cout } 641 | << "线程 " 642 | << std::this_thread::get_id() 643 | << " 已被唤醒\n"; 644 | } 645 | 646 | void wake_up(){ 647 | std::this_thread::sleep_for(5s); 648 | 649 | // 更新值并唤醒 650 | ptr.store(std::make_shared(10)); 651 | ptr.notify_one(); 652 | } 653 | ``` 654 | 655 | ## 内存次序 656 | 657 | ### 前言 658 | 659 | 事实上我们在前面就用到了不少的内存次序,只不过一直没详细展开讲解。 660 | 661 | 在开始学习之前,我们需要强调一些基本的认识: 662 | 663 | 1. **内存次序是非常底层知识**:对于普通开发者来说,了解内存次序并非硬性需求。如果您主要关注业务开发,可以直接跳过本节内容。如果您对内存次序感兴趣,则需要注意其复杂性和难以观察的特性,这将使学习过程具有一定挑战性。 664 | 665 | 2. **内存次序错误的使用难以察觉**:即使通过多次(数以万计)运行也难以发现。这是因为许多内存次序问题是由于极端的、少见的情况下的竞争条件引起的,而这些情况很难被重现。此外,即使程序在某些平台上运行正常,也不能保证它在其他平台上也能表现良好,因为不同的 CPU 和编译器可能对内存操作的顺序有不同的处理(例如 x86 架构内存模型:Total Store Order (TSO),是比较严格的内存模型)。因此,开发者必须依赖自己的知识和经验,以及可能的测试和调试技术,来发现和解决内存次序错误。 666 | 667 | 错误难以被我们观察到的原因其实可以简单的说: 668 | 669 | - **CPU 与编译器不是神经病,没有*好处*不会闲的没事给你指令重排**。 670 | 671 | --- 672 | 673 | - 编译器重排:编译器在编译代码时,为了提高性能,可以按照一定规则重新安排代码的执行顺序。例如,可以将不相关的指令重排,使得 CPU 流水线更加高效地执行指令。编译器优化需要遵守一个“[**如同规则**](https://zh.cppreference.com/w/cpp/language/as_if)(as-if rule)”,即不可改变可观察的副作用。 674 | 675 | - CPU 重排:CPU 在运行程序时,也会对指令进行重排,以提高执行效率,减少等待时间。这种重排通常遵循一些硬件层面的优化规则,如内存访问的优化。 676 | 677 | 你们可能还有疑问:“**单线程能不能指令重排**?” 678 | 679 | CPU 的指令重排必须遵循一定的规则,以确保程序的**可观察副作用**不受影响。对于单线程程序,CPU 会保证外部行为的一致性。对于多线程程序,需要开发者使用同步原语来显式地控制内存操作的顺序和可见性,确保多线程环境下的正确性。而标准库中提供的原子对象的原子操作,还可以设置内存次序。 680 | 681 | 那有没有可能: 682 | 683 | - “*end 重排到 start 前面了!指令重排了!*” 684 | 685 | 这也就是前面说的,把 CPU 与编译器当神经病。各位写代码难道还要考虑下面这段,会不会指令重排导致先输出 `end` 吗?这显然不现实。 686 | 687 | ```txt 688 | print("start"); // 1 689 | print("end"); // 2 690 | ``` 691 | 692 | 不禁止就是有可能,但是我们无需在乎,**就算真的 CPU 将 end 重排到 start 前面了,也得在可观测行为发生前回溯了**。所以我一直在强调,这些东西,**我们无需在意**。 693 | 694 | 好了,到此,基本认识也就足够了,以上的示例更多的是泛指,知道其表达的意思就好,这些还是简单直接且符合直觉的。 695 | 696 | ### 可见 697 | 698 | **可见** 是 C++ 多线程并发编程中的一个重要概念,它描述了一个线程中的数据修改对其他线程的可见程度。具体来说,如果线程 A 对变量 x 进行了修改,那么**其他线程 B 是否能够看到线程 A 对 x 的修改**,就涉及到可见的问题。 699 | 700 | 在讨论多线程的内存模型和执行顺序时,虽然经常会提到 CPU 重排、编译器优化、缓存等底层细节,但真正核心的概念是*可见*,而不是这些底层实现细节。 701 | 702 | **C++ 标准中的可见**: 703 | 704 | - 如果线程 A 对变量 x 进行了修改,而线程 B 能够读取到线程 A 对 x 的修改,那么我们说线程 B 能看到线程 A 对 x 的修改。也就是说,线程 A 的修改对线程 B 是***可见***的。 705 | 706 | C++ 标准通过内存序(memory order)来定义如何确保这种*可见*,而不必直接关心底层的 CPU 和编译器的具体行为。内存序提供了操作之间的顺序关系,确保即使存在 CPU 重排、编译器优化或缓存问题,线程也能正确地看到其他线程对共享数据的修改。 707 | 708 | 例如,通过使用合适的内存序(如 memory_order_release 和 memory_order_acquire),可以确保线程 A 的写操作在其他线程 B 中是可见的,从而避免数据竞争问题。 709 | 710 | 总结: 711 | 712 | - *可见* 关注的是线程之间的数据一致性,而不是底层的实现细节。 713 | 714 | - 使用 C++ 的内存序机制可以确保数据修改的可见,而不必过多关注具体的 CPU 和编译器行为。 715 | 716 | 这种描述方式可以帮助更清楚地理解和描述多线程并发编程中如何通过 C++ 标准的内存模型来确保线程之间的数据一致性,而无需太多关注底层细节。 717 | 718 | --- 719 | 720 | 我知道各位肯定有疑问,我们大多数时候写多线程代码都从来没使用过内存序,一般都是互斥量、条件变量等高级同步设施,这没有可见性的问题吗? 721 | 722 | 没有,这些设施自动确保数据的可见性。例如: `std::mutex` 的 `unlock()` 保证: 723 | 724 | - 此操作*同步于*任何后继的取得同一互斥体所有权的锁定操作。 725 | 726 | 也就是 [`unlock()`](https://zh.cppreference.com/w/cpp/thread/mutex/unlock) *同步于* `lock()`。 727 | 728 | “*同步于*”:操作 A 的完成会确保操作 B 在其之后的执行中,能够看到操作 A 所做的所有修改。 729 | 730 | 也就是说: 731 | 732 | - `std::mutex` 的 `unlock()` 操作*同步于*任何随后的 `lock()` 操作。这意味着,线程在调用 `unlock()` 时,对共享数据的修改会对之后调用 `lock()` 的线程*可见*。 733 | 734 | ### `std::memory_order` 735 | 736 | `std::memory_order` 是一个枚举类型,用来指定原子操作的内存顺序,影响这些操作的行为。 737 | 738 | ```cpp 739 | typedef enum memory_order { 740 | memory_order_relaxed, 741 | memory_order_consume, 742 | memory_order_acquire, 743 | memory_order_release, 744 | memory_order_acq_rel, 745 | memory_order_seq_cst 746 | } memory_order; 747 | 748 | // C++20 起则为: 749 | 750 | enum class memory_order : /* 未指明 */ { 751 | relaxed, consume, acquire, release, acq_rel, seq_cst 752 | }; 753 | inline constexpr memory_order memory_order_relaxed = memory_order::relaxed; 754 | inline constexpr memory_order memory_order_consume = memory_order::consume; 755 | inline constexpr memory_order memory_order_acquire = memory_order::acquire; 756 | inline constexpr memory_order memory_order_release = memory_order::release; 757 | inline constexpr memory_order memory_order_acq_rel = memory_order::acq_rel; 758 | inline constexpr memory_order memory_order_seq_cst = memory_order::seq_cst; 759 | ``` 760 | 761 | 这 6 个常量,每一个常量都表示不同的内存次序。 762 | 763 | 大体来说我们可以将它们分为三类。 764 | 765 | 1. `memory_order_relaxed` 宽松定序:不是定序约束,**仅对此操作要求原子性**。 766 | 2. `memory_order_seq_cst` 序列一致定序,这是库中所有原子操作的**默认行为**,也是**最严格的内存次序**,是**绝对安全**的。 767 | 768 | 剩下的就是第三类。 769 | 770 | ### 其它概念 771 | 772 | 777 | 778 | ### `x86` 和 `ARM` 的内存模型:强一致性与弱一致性 779 | 780 | 783 | 784 | **内存模型是软件与实现之间的一种约定契约**。它定义了在多线程或并发环境中,如何对内存操作的顺序和一致性进行规范,以确保程序的正确性和可靠性。 785 | 786 | C++ 标准为我们定义了 C++ 标准内存模型,使我们能够无需关心底层硬件环境就编写出跨平台的应用程序。不过,了解底层硬件架构的内存模型对扩展知识面和深入理解编程细节也非常有帮助。 787 | 788 | 最经典与常见的两种 CPU 指令集架构就是:`x86` 与 `ARM`。 789 | 790 | - `x86` 架构:是一种复杂指令集计算([CISC](https://zh.wikipedia.org/wiki/%E8%A4%87%E9%9B%9C%E6%8C%87%E4%BB%A4%E9%9B%86%E9%9B%BB%E8%85%A6))架构,因其强大的性能被广泛应用于桌面电脑、笔记本电脑和服务器中。`x86` 架构采用的是 TSO(Total Store Order)[**内存一致性模型**](https://jamesbornholt.com/blog/memory-models/),是一种**强一致性模型**,**简化了多线程编程中的内存同步问题**(后文中会提到)。 791 | 792 | - `ARM` 架构:是一种精简指令集计算([RISC](https://zh.wikipedia.org/wiki/%E7%B2%BE%E7%AE%80%E6%8C%87%E4%BB%A4%E9%9B%86%E8%AE%A1%E7%AE%97%E6%9C%BA))架构,因其高能效和低功耗特点广泛应用于移动设备、嵌入式系统和物联网设备中。`ARM` 架构采用的是**弱序内存模型**([weakly-ordered memory](https://developer.arm.com/documentation/102336/0100/Memory-ordering)),允许**更灵活**的内存优化,但这需要程序员使用内存屏障等机制来确保正确性。 793 | 794 | 这两种架构在设计理念和应用领域上存在显著差异,这也是它们在不同应用场景中表现出色的原因。 795 | 796 | 如果你从事嵌入式系统或者学术研究等,可能也听说过 `RISC-V` 架构,它目前在国内的应用也逐渐增多。 797 | 798 | RISC-V 是一种开源的精简指令集计算(RISC)架构,旨在提供一种高效、模块化且开放的指令集。与 x86 和 ARM 架构不同,RISC-V 的设计目标是简化指令集,同时保持高度的灵活性和扩展性。它在内存模型方面也有自己独特的特性。 799 | 800 | RISC-V 采用的也是**弱序内存模型**(weakly-ordered memory model),这与 x86 的强一致性模型(TSO)和 ARM 的弱一致性模型有所不同。你可能会有疑问: 801 | 802 | - `ARM` 和 `RISC-V` 都是弱序内存模型,为什么不同? 803 | 804 | 各位一定要区分,这种强弱其实也只是一种分类而已,不同的指令集架构大多都还是有所不同的,并不会完全一样。例如: `x86` 的 TSO(Total Store Order)是强一致性模型的一种,但并不是所有强一致性模型都是 TSO。 805 | 806 | ### 宽松定序 807 | 808 | ### 释放-获取定序 809 | 810 | ### 释放-消费定序 811 | 812 | ### 序列一致定序 813 | 814 | ### 与 `volatile` 的关系 815 | 816 | 819 | --------------------------------------------------------------------------------