C++ 11 中的并发多线程模式

C++ 11 中的并发多线程模式
目录一 线程与进程概述1. 操作系统的并发与并行2. 并发的两种方式的异同2.1 多线程与多进程区别2.2 应用场景二 std::thread1. 概述2. 常用函数介绍3. std::thread 使用三 线程加锁与同步1. 多线程打印2. 多线程加锁打印3.多线程轮流打印 1 - 1003.1 std::condition_variable 介绍3.2 示例4. 原子变量四 线程获取结果一 线程与进程概述1. 操作系统的并发与并行并发并发指的是计算机上的多个任务在同一时间段内完成。比如我们在电脑上启动浏览器看 CSDN 博客同时又启动了音乐播放器播放音乐用微信QQ 与好友聊天等等这些看着是同时发生的但是对于计算机的 CPU 来讲却是一会运行这个程序一会又运行另外一个程序只不过切换的很快对我们来讲这是同时发生的。并行并行指的是计算机上同一时刻同时运行多个程序。如果我们的电脑是多核 CPU那前面的例子多个程序是可以实现同一时刻运行多个的。2. 并发的两种方式的异同计算机中的并发方式有两种多线程与多进程进程进程是操作系统分配和管理资源的最小单位线程线程是操作系统中任务调度的最小单位是比进程更小的独立运行的单位一个进程可以包含多个线程并发运行线程也被称为轻量级进程。2.1 多线程与多进程区别线程间切换比进程间切换的开销小同一进程中的多个线程共享该进程中的内存等资源每个线程所占的资源较少只保留一些寄存器相关上下文切换的信息因而线程间切换的开销较小创建销毁线程的开销比进程小线程可以看成是轻量级的进程因为每个线程所持有的资源少因而创建销毁时的开销也要更小一些线程间通信方式不同多线程因为都在同一进程中共享进程的资源使用相同的内存地址因而多线程间的通信可以通过共享某个原子的全局变量或者一块加锁的内存空间即可而每个进程都是单独用于自己的内存资源所以进程间的通信比线程要复杂一些常见进程间通信方式有 socket管道消息队列共享内存mmap,信号多进程比多线程的程序健壮由于每个进程的资源是相互独立的所以某个进程崩溃是不会影响到其他进程的然而多线程是运行在同一个进程中共享一份资源因此某个线程若是崩溃很容易导致整个进程崩溃掉2.2 应用场景1.多进程应用场景对独立资源保护要求比较高的场景比如游戏服务器中每个用户拥有自己的一些数据资源等此时就可以使用多进程每个进程代表一个用户2.多线程应用场景互联网中的后端服务器处理用户的大量请求时常常采用多线程的方式二 std::thread1. 概述c 语言中引入了 pthread 来实现多线程但是并不好用因而 c 11 中引入了 std::thread 类来实现 c 中的线程2. 常用函数介绍函数说明thread() noexcept默认构造函数创建一个线程thread( thread other ) noexcept移动构造函数将 other 对象移到当前构造函数构造出新对象template class F, class... Args explicit thread( F f, Args... args )构造函数f 为要运行的函数args 为函数 f 的输入参数thread( const thread ) delete拷贝构造函数被禁止使用~thread()析构函数join()等待该线程执行完毕后调用线程再继续执行会阻塞detach()允许线程脱离调用线程独立执行调用线程不必等待线程执行便可继续运行joinable()判断当前线程是否可以 joinget_id()返回线程 idthread operator( thread other )移动赋值函数3. std::thread 使用#include iostream #include string #include thread void foo() { std::this_thread::sleep_for(std::chrono::milliseconds(50)); std::cout threadId: std::this_thread::get_id() , running thread. std::endl; } void testThreadFoo() { std::thread t1; std::cout before starting, joinable: t1.joinable() \n; std::cout thread join ....... std::endl; std::vectorstd::thread vec; for (int i 0; i 3; i) { std::thread t2(foo); std::cout after starting, joinable: t1.joinable() \n; vec.push_back(std::move(t2)); } for(auto th : vec) { th.join(); } std::cout thread detatch ....... std::endl; std::thread th3(foo); th3.detach(); std::cout after detach, joinable: t1.joinable() \n; std::this_thread::sleep_for(std::chrono::milliseconds(500)); } int main(int argc, char *argv[]) { testThreadFoo(); std::cout end main ...... std::endl; return 0; }输出三 线程加锁与同步题目用多线程的方式 按照顺序依次打印 1 - 1001. 多线程打印示例代码#include iostream #include string #include thread int count 0; void printCount() { std::this_thread::sleep_for(std::chrono::milliseconds(5)); std::cout threadId: std::this_thread::get_id() , count: count std::endl; } void func1() { while (count 100) { printCount(); count; } } void testThread() { std::vectorstd::thread vec; for (int i 0; i 5; i) { vec.emplace_back(func1); } for(auto th : vec) { th.join(); } } int main(int argc, char *argv[]) { std::cout enter into main ...... std::endl; testThread(); std::cout end main ...... std::endl; return 0; }输出(输出太长只输出部分)问题从输出的截图中可以清晰的看出不同线程打印出了相同的数字这是因为在多个线程里调用 printCount 函数时 count 的值还未来得及自增count便被打印了出来,因此不同线程却打印了相同的数字这与题目中按照顺序打印数字是不相符合的。在实际的应用程序中我们也会有类似的问题某一段代码一个时间段内只允许某个线程单独执行完成后其他线程才能执行此段代码。如何解决这样的问题呢大多数语言都引入了加锁机制与原子变量的概念。2. 多线程加锁打印为了多个线程在同一时间段内同时执行某段代码多数语言引入了加锁的机制。这里我们复习一下操作系统的锁机制锁的分类互斥锁、自旋锁、共享锁、条件锁互斥锁是一种控制多线程对于共享资源的互斥访问的信号量当一个线程获取到该共享资源的控制权后会将该共享资源加互斥锁那么在线程操作完共享资源前锁都是未释放的状态当其他线程也尝试获取该共享资源时会检查共享资源互斥锁是否释放若是释放的话才能获取该共享资源的控制权否则线程就进入了阻塞状态自旋锁与互斥锁不同之处在于当其他线程尝试获取共享资源的时候检查锁的状态若是处于未释放状态时线程不会进入阻塞状态而会持续检查锁的状态直到获取共享资源成功。问题 自旋锁一直不停的获取共享资源的机制会不会很浪费资源答互斥锁与自旋锁的应用场景不同。我们需要知道一点线程间的切换是有一定的开销的若是对共享资源的操作开销远远大于线程间切换此时采用互斥锁比较划算若是对共享资源的操作开销远远小于线程间切换此时采用自旋锁比较划算。共享锁往往与互斥锁一同使用作为读写锁。所谓读写锁允许多个线程读共享资源此时加共享锁若是线程写的话需要加写锁互斥锁不允许其他线程获取共享资源条件锁条件锁就是所谓的条件变量当某个线程因为某个条件变量未满足时而陷入了阻塞状态一旦条件满足就会以信号量的方式唤醒 因为该条件而陷入阻塞状态的线程。c 中的互斥锁std::mutex 与 std::unique_lock 共同使用c 中的共享锁C关于锁的总结一 - 封fenghl - 博客园 (cnblogs.com)2.1 互斥锁示例代码#include iostream #include string #include thread #include mutex #include condition_variable std::mutex mtx; int count 0; void printCount() { std::this_thread::sleep_for(std::chrono::milliseconds(5)); std::cout threadId: std::this_thread::get_id() , count: count std::endl; } void func2() { while (count 100) { std::unique_lockstd::mutex lck(mtx); if (count 100) break; printCount(); count; } } void testThread() { std::vectorstd::thread vec; for (int i 0; i 5; i) { vec.emplace_back(func2); } for(auto th : vec) { th.join(); } } int main(int argc, char *argv[]) { std::cout enter into main ...... std::endl; testThread(); std::cout end main ...... std::endl; return 0; }输出部分从截图中可以看出加了互斥锁以后多个线程获取共享资源后可以实现按照顺序打印 1 - 100 数字。2.2 互斥锁与共享锁示例代码#includeiostream #includethread #includeatomic #include mutex #include shared_mutex #include vector #include chrono std::shared_mutex shared_mtx; // c11 std::thread int read_count 0; // 读线程共享锁多线程并发读取 void reader(int miseconds) { for (int i 0; i 2; i) { std::shared_lockstd::shared_mutex shared_lock(shared_mtx); read_count; std::cout reader thread id: std::this_thread::get_id() , i i , read_count: read_count std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(miseconds)); } } // 写线程独占锁同一时间只能一个写 void writer() { for (int i 0; i 2; i) { std::unique_lockstd::shared_mutex lock(shared_mtx); read_count; std::cout writer thread id: std::this_thread::get_id() , i i , read_count: read_count std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(200)); } } void threadTest2() { std::vectorstd::thread thread_vec; for (int i 0; i 3; i) { // 读线程 thread_vec.emplace_back(reader,10); } // 读线程 thread_vec.emplace_back(reader, 500); // 写线程 thread_vec.emplace_back(writer); for (std::thread th : thread_vec) { th.join(); } std::cout read_count: read_count std::endl; } int main() { threadTest2(); return 0; }运行逻辑说明3 个读线程同时拿到共享锁并行打印数据不会互相阻塞写线程必须等所有读锁全部释放后才能拿到独占锁写线程持有锁期间所有新读、写线程全部阻塞RAII 自动释放锁无需手动unlock避免死锁​​​​​​​运行结果截图从结果看出读线程之间是不互斥的写线程是互斥的3.多线程轮流打印 1 - 100在原来的题目上做些修改现在要求线程之间轮流打印 1 -100 。普通的加锁行为是满足不了该要求的因为我们无法限定哪个线程来打印哪些数字。为了解决这样的问题我们引入了条件锁也就是条件变量假设两个线程 1 与 2 轮流打印奇数与偶数线程1 与 线程2 获取获取到锁后会继续判断当前条件变量若是 count 为奇数则线程1 继续打印线程 2 进入阻塞状态相反亦然。c 中的条件变量是 std::condition_variable3.1 std::condition_variable 介绍函数说明condition_variable()默认构造函数condition_variable( const condition_variable ) delete禁止拷贝构造函数void notify_one() noexcept唤醒一个被当前条件阻塞的线程void notify_all() noexcept唤醒所有被当前条件阻塞的线程template class Predicate void wait( std::unique_lockstd::mutex lock, Predicate pred )阻塞当前线程直到条件变量被唤醒std::condition_variable - cppreference.com3.2 示例示例代码1#include iostream #include string #include thread #include mutex #include condition_variable std::mutex mtx; std::condition_variable condition; int count 0; void printCount() { std::this_thread::sleep_for(std::chrono::milliseconds(5)); std::cout threadId: std::this_thread::get_id() , count: count std::endl; } void printCont(int num, int waitVal) { while (true) { std::unique_lockstd::mutex lck(mtx); condition.wait(lck,[](){return count % num waitVal;}); // 条件变量 if (count 100) break; printCount(); count; condition.notify_all(); } } void testThread2() { std::vectorstd::thread vec; int n 5; for (int i 0; i n; i) { vec.emplace_back(printCont, n, i); } for(auto th : vec) { th.join(); } } int main(int argc, char *argv[]) { std::cout enter into main ...... std::endl; testThread2() std::cout end main ...... std::endl; return 0; }输出前面有点问题出现了线程死锁的问题导致程序夯住了。两个线程轮流打印1 - 100示例代码2#include iostream #include string #include thread #include mutex #include condition_variable std::mutex mtx; std::condition_variable condition; bool flag true; int count 0; void printCount1() { while (count 100) { std::unique_lockstd::mutex lck(mtx); condition.wait(lck,[](){return flag;}); printCount(); count; flag false; condition.notify_one(); } } void printCount2() { while (count 100) { std::unique_lockstd::mutex lck(mtx); condition.wait(lck,[](){return !flag;}); printCount(); count; flag true; condition.notify_one(); } } void testThread1() { std::thread th1(printCount1); std::thread th2(printCount2); th1.join(); th2.join(); } int main(int argc, char *argv[]) { std::cout enter into main ...... std::endl; testThread2(); std::cout end main ...... std::endl; return 0; }输出4. 原子变量c 中的 atomic 原子化学上原子在最小的不可变颗粒用在计算机世界就代表某个原子变量的操作在一段时间内只能有一个线程操作即是线程安全的。相比与加锁的方式原子变量的性能更好。std::atomic - cppreference.com示例代码#include iostream #include string #include thread #include mutex #include atomic std::atomic_int atomaticVal(0); int count 0; void printVal() { for (int i 0; i 10000; i) { atomaticVal; count; } } void testThread3() { std::vectorstd::thread vec; int n 10; for (int i 0; i n; i) { vec.emplace_back(printVal); } for(auto th : vec) { th.join(); } std::cout atomaticVal: atomaticVal std::endl; std::cout count: count std::endl; } int main(int argc, char *argv[]) { std::cout enter into main ...... std::endl; testThread3(); std::cout end main ...... std::endl; return 0; }输出可以看出原子变量的自增是线程安全的非原子变量的自增不是线程安全的。四 线程获取结果前面介绍的线程执行的函数都是无返回值的如果线程执行的函数有返回值的话应该怎么办呢介绍一下 c11 引入的 std::future 与 std::package_task详情见std::future - cppreference.comstd::packaged_task - cppreference.com示例代码如下#include thread #include mutex #include condition_variable #include atomic #include future #includefunctional int addFunc(int a, int b) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); return a b; } int sumFunc(int n) { int result 0; for(int i 1; i n; i) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); result i; } return result; } void futureTest() { std::packaged_taskint() task1 std::packaged_taskint()(std::bind(addFunc, 2, 5)); std::futureint resultFuture task1.get_future(); task1(); std::cout resultFuture.get() std::endl; std::packaged_taskint(int, int) task2 std::packaged_taskint(int, int)(addFunc); std::futureint resultFuture2 task2.get_future(); std::thread td(std::move(task2), 1, 5); td.join(); std::cout resultFuture2.get() std::endl; std::packaged_taskint(int) task3 std::packaged_taskint(int)(sumFunc); std::packaged_taskint(int) task4 std::packaged_taskint(int)(sumFunc); std::futureint resultFuture3 task3.get_future(); std::futureint resultFuture4 task4.get_future(); std::thread th3(std::move(task3), 100); std::thread th4(std::move(task4), 200); th3.join(); th4.join(); std::cout resultFuture3.get() std::endl; std::cout resultFuture4.get() std::endl; } int main(int argc, char *argv[]) { std::cout enter into main ...... std::endl; futureTest(); std::cout end main ...... std::endl; return 0; }输出