一文搞定c++多线程

c++11引入了用于多线程操作的thread类,该库移植性更高,并且使得写多线程变得简洁了一些。

多线程头文件支持

为了支持多线程操作,c++11新标准引入了一些头文件来支持多线程编程:

  • <thread>:内部声明了 std::thread 类,用于创建多线程
  • <atomic>:内部声明std::atomic 和 std::atomic_flag两个类,可以利用这两个类实现原子类型的各种特性,并且声明了一些原子操作函数
  • <mutex>:提供了多种互斥操作,可以显式避免数据竞争,内部包含mutex类型、lock类型以及功能函数.
  • <condition_variable>:声明了与条件变量相关的类,包括 std::condition_variable 和 std::condition_variable_any
  • <future>:通过特殊的provider进行数据的异步访问,实现线程间的通信,主要用于支持异步访问。

多线程示例

简单多线程

一个简单的多线程:

#include <iostream>
#include <thread>
using namespace std;
void thread_test()
{std::cout << "I'm thread_test()\n";
}
int main()
{std::thread t1(thread_test);cout << "I'm main_thread\n";//do somtthingt1.join();return 0;
}

部分需要补充的点:

  • main函数构建了一个std::thread对象t1,构造的时候传递了一个函数参数,这个参数就是线程的入口函数,函数执行完了,整个线程也就执行完了,线程创建成功后,就会立即启动
  • 一旦线程开始运行, 就需要显式的决定是要等待它完成(join),或者分离它让它自行运行(detach)。注意:只需要在std::thread对象被销毁之前做出这个决定
  • 调用join(),主线程会一直阻塞着,直到子线程完成,join()函数的另一个任务是回收该线程中使用的资源,如果采用detach的话表示子线程和主线程分离,这样子线程将有操作系统管理,主线程结束后也thread对象被析构,但是该线程仍将继续执行至结束。
detach和join的区别

在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。也就是说,分离状态还是可结合状态主要决定一个线程以怎样的方式终结。

当thread::join()函数被调用后,调用它的线程会被block,直到线程的执行被完成。基本上,这是一种可以用来知道一个线程已结束的机制。当thread::join()返回时,OS的执行的线程已经完成,C++线程对象可以被销毁。

当thread::detach()函数被调用后,执行的线程从线程对象中被分离,已不再被一个线程对象所表达–这是两个独立的事情。C++线程对象可以被销毁,同时OS执行的线程可以继续,也就是说主线程结束后该线程仍然可以继续运行,因此如果不关心一个线程的结束状态,那么也可以将一个线程设置为 detached 状态,从而让操作系统在该线程结束时来回收它所占的资源。

实际上,程序员应该在thread对象执行流程到析构函数前总是要么join,要么detach一个线程。当一个程序终止时(比如main返回),剩下的在后台的detached线程执行不会再等待;相反它们的执行会被挂起并且它们的本地线程对象会被销毁。通常情况下使用join即可。

否则会有问题:

#include <iostream>
#include <thread>
#include <Windows.h>
using namespace std;
void test_thread()
{  cout << "Another thread  is working!\n" ;
}
int main()
{  thread task(test_thread);  cout << "Main thread is working!\n";  system("pause");
}

这是一个很简单的多线程程序,但是并没有对task线程进行join或者detach,运行结果:

PS D:\vscode_c> ./test2
Main thread is working!
Another thread is working!
请按任意键继续. . .
terminate called without an active exception

出现这个情况的原因就是std::thread在main()结束的时候,被销毁了,解决办法就是对每一个进程都进行join或者detach(根据具体需求)。在代码上加入task.join(); 即可

join多线程

join函数会阻塞主流程,所以子线程都执行完成之后才继续执行主线程

#include <iostream>
#include <thread>
#include <Windows.h>
using namespace std;
void thread1()
{  for (int i = 0; i < 5; i++)  {  cout << "Thread 1 is working!\n" ;  Sleep(200);  }
}
void thread2()
{  for (int i = 0; i < 5; i++)  {  cout << "Thread 2 is working!\n" ;  Sleep(100);  }
}  int main()
{  thread task01(thread1);  thread task02(thread2);task01.join();task02.join();for (int i = 0; i < 5; i++)  {  cout << "Main thread is working!" << endl;  Sleep(200);  }system("pause");
}

运行结果:

PS D:\vscode_c> ./test
Thread 1 is working!
Thread 2 is working!
Thread 2 is working!
Thread 1 is working!
Thread 2 is working!
Thread 2 is working!
Thread 1 is working!
Thread 2 is working!
Thread 1 is working!
Thread 1 is working!
Main thread is working!
Main thread is working!
Main thread is working!
Main thread is working!
Main thread is working!
detach多线程

可以使用detach将子线程从主流程中分离,独立运行,不会阻塞主线程:这样对于系统而言,主线程和产生的子线程实际上并没有直接依赖关系了。

#include <iostream>
#include <thread>
#include <Windows.h>  using namespace std;  void thread1()
{  for (int i = 0; i < 5; i++)  {  cout << "Thread 1 is working!\n" ;  Sleep(200);  }
}
void thread2()
{  for (int i = 0; i < 5; i++)  {  cout << "Thread 2 is working!\n" ;  Sleep(100);  }
}  int main()
{  thread task01(thread1);  thread task02(thread2);task01.detach();task02.detach();for (int i = 0; i < 5; i++)  {  cout << "Main thread is working!\n";  Sleep(200);  }system("pause");
}

运行结果:

PS D:\vscode_c> ./test
Main thread is working!
Thread 1 is working!
Thread 2 is working!
Thread 2 is working!
Main thread is working!
Thread 1 is working!
Thread 2 is working!
Thread 2 is working!
Main thread is working!
Thread 1 is working!
Thread 2 is working!
Main thread is working!
Thread 1 is working!
Main thread is working!
Thread 1 is working!

可以看到detach情况下两个子线程还没有结束主线程也在继续运行,即 thread1 和 thread2 以及main thread三者是完全并行的。

带参数多线程

因为thread创建时用了函数的地址,那么对于有参数的函数如何传递参数呢?答案很简单,在创建时后面的参数列表放置参数即可

#include <iostream>
#include <thread>
#include <Windows.h>using namespace std;//定义带参数子线程
void thread_test(int num)
{for (int i = 0; i < num; i++){cout << "Test thread is working!\n";Sleep(100);}
}
int main()
{thread task(thread_test, 5); //带参数子线程task.detach();for (int i = 0; i < 5; i++){cout << "Main thread is working!\n";Sleep(200);}system("pause");
}

运行结果:

PS D:\vscode_c> ./test
Main thread is working!
Test thread is working!
Test thread is working!
Main thread is working!
Test thread is working!
Test thread is working!
Main thread is working!
Test thread is working!
Main thread is working!
Main thread is working!
多线程同步

当多个线程对一个共享变量进行操作时,就需要格外注意了! 因为由于线程执行顺序的不确定性,可能会导致产生不符合预期的结果。因此需要对一些变量进行保护和限制才行。

#include <iostream>
#include <thread>
#include <Windows.h>
using namespace std;
int totalNum = 20;
void thread1()
{while (totalNum > 0){cout << totalNum <<endl;totalNum--;Sleep(100);}
}
void thread2()
{while (totalNum > 0){cout << totalNum <<endl;totalNum--;Sleep(100);}
}
void thread3()
{while (totalNum > 0){cout << totalNum << endl;totalNum--;Sleep(100);}
}
int main()
{thread task1(thread1);thread task2(thread2);thread task3(thread3);task1.join();task2.join();task3.join();
}

三个线程做的事情一模一样,就是如果这个全局变量大于0,那么输出一下并且执行减1操作。那么预期的结果希望是每一行一个数字并且换行,并且每个数字是依次递减的,但是这个程序实际上结果是不确定的,下面是某一次的运行结果:

PS D:\vscode_c> ./test
202020
17
16
16
14
14
13
11
10
9
8
8
6
5
5
3
2
2

可以看到,有些数字重复输出了,并且有些数字没有输出,并且甚至换行也可能出现不按照预期的结果。主要原因是由于第一个线程对变量操作的过程中,第二个线程也对同一个变量进行各操作,导致第一个线程处理完后的输出有可能是线程二操作的结果。

因此,为了对数据进行正确的访问,需要引入mutex互斥机制,从而支持线程互斥访问。

#include <iostream>
#include <thread>
#include <Windows.h>
#include<mutex>
using namespace std;
int totalNum = 20;
mutex mut; //互斥对象
void thread1()
{while (totalNum > 0){mut.lock(); //加锁if (totalNum < 0)break;cout << totalNum <<endl;totalNum--;Sleep(100);mut.unlock(); //解锁}
}
void thread2()
{while (totalNum > 0){mut.lock(); //加锁if (totalNum < 0)break;cout << totalNum << endl;totalNum--;Sleep(100);mut.unlock(); //解锁}
}
void thread3()
{while (totalNum > 0){mut.lock(); //加锁if (totalNum < 0)break;cout << totalNum << endl;totalNum--;Sleep(100);mut.unlock(); //解锁}
}
int main()
{thread task1(thread1);thread task2(thread2);thread task3(thread3);task1.join();task2.join();task3.join();
}

运行结果:

PS D:\vscode_c> ./test
20
19
18
17
16
15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
0

深入探索thread类

std::thread类的构造函数是使用可变参数模板实现的,即可以传递任意个参数,第一个参数是线程的入口函数,而后面的若干个参数是该函数的参数。第一个参数可以是函数指针、仿函数、lambda表达式或者std::function;

thread类的构造函数
  • 默认构造函数,创建一个空的 std::thread 执行对象

    thread() noexcept;
    
  • 初始化构造函数,创建一个 std::thread 对象,该 std::thread 对象可被 joinable,新产生的线程会调用 fn 函数,该函数的参数由 args 给出

    template <class Fn, class... Args>
    explicit thread(Fn&& fn, Args&&... args);
    
  • 拷贝构造函数(被禁用),意味着 std::thread 对象不可拷贝构造

    thread(const thread&) = delete;
    
  • 移动语义构造函数,调用成功之后 x 不代表任何 std::thread 执行对象

    thread(thread&& x) noexcept;
    

Tips:可被 joinablestd::thread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached.

线程对象只能移动,不能复制。

构造函数的代码例子:

#include <iostream>
#include <thread>
#include <Windows.h>
using namespace std;
void thread1()
{cout << "this is thread1!\n";
}
void thread2(int n)
{cout << "this is thread2! n is "<<n<<"\n";
}int main()
{thread task0; //task0并不是一个线程thread task1(thread1); //无参数构造函数thread task2(thread2, 1); //含参数的构造函数//thread task3(thread2); //拷贝构造函数是被禁止的thread task4(std::move(task2)); //move语义task1.join();task4.join();
}
thread类的一些常用函数

get_id: 获取线程 ID,返回一个类型为 std::thread::id 的对象

joinable: 检查线程是否可被 join。检查当前的线程对象是否表示了一个活动的执行线程,由默认构造函数创建的线程是不能被 join 的。另外,如果某个线程 已经执行完任务,但是没有被 join 的话,该线程依然会被认为是一个活动的执行线程,因此也是可以被 join 的

detach: Detach 线程。 将当前线程对象所代表的执行实例与该线程对象分离,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放

swap: Swap 线程,交换两个线程对象所代表的底层句柄(underlying handles)

native_handle: 返回 native handle

std::this_thread 命名空间中相关辅助函数

get_id: 获取线程 ID

yield: 当前线程放弃执行,操作系统调度另一线程继续执行

sleep_until: 线程休眠至某个指定的时刻(time point),该线程才被重新唤醒

sleep_for: 线程休眠某个指定的时间片(time span),该线程才被重新唤醒,不过由于线程调度等原因,实际休眠时间可能比 sleep_duration 所表示的时间片更长

竞争条件

数据竞争

并发代码中最常见的错误之一就是竞争条件(race condition),因此在部分变量的访问时需要设置限制条件使其互斥。

例如之前的程序中,会看到多线程的std::cout就会有非预期输出,主要是因为cout就是一个典型的共享变量,多个线程执行cout会共享缓冲区,导致一个线程刚放入一些内容到缓冲区就被另一个进程输出了。

#include <iostream>
#include <thread>
#include <Windows.h>
using namespace std;
void thread1()
{cout << "this is thread1!"<<endl;
}
void thread2(int n)
{cout << "this is thread2! n is "<<n<<endl;
}int main()
{thread task0; //task0并不是一个线程thread task1(thread1); //无参数构造函数thread task2(thread2, 1); //含参数的构造函数task1.join();task2.join();cout << "main thread!" << endl;
}

运行结果:

PS D:\vscode_c> ./test
this is thread1!this is thread2! n is 1
main thread!

可以看到输出并不符合我们的预期,主要原因就是这些线程的cout是共享缓冲区的,执行顺序是不可预期的。

互斥单元

解决办法就是要对cout这个共享资源进行保护。在c++中,可以使用互斥锁std::mutex进行资源保护,头文件是#include <mutex>,共有两种操作:锁定(lock)与解锁(unlock)。将cout重新封装成一个线程安全的函数。从而使得多线程对cout这个共享资源访问互斥。

#include <iostream>
#include <thread>
#include <Windows.h>
#include<mutex>
using namespace std;
std::mutex mu;    //通过mutex变量对共享部分加锁从而实现互斥访问
void thread1()
{mu.lock();cout << "this is thread1!"<<endl;mu.unlock();
}
void thread2(int n)
{mu.lock();cout << "this is thread2! n is "<<n<<endl;mu.unlock();
}int main()
{thread task0; //task0并不是一个线程thread task1(thread1); //无参数构造函数thread task2(thread2, 1); //含参数的构造函数task1.detach();task2.detach();mu.lock();cout << "main thread!" << endl;mu.unlock();
}

运行结果:

PS D:\vscode_c> ./test
this is thread1!
this is thread2! n is 1
main thread!
死锁

如果将某个mutex上锁了,却一直不释放,另一个线程访问该锁保护的资源的时候,就会发生死锁,这种情况下使用lock_guard可以保证析构的时候能够释放锁。c++库已经提供了std::lock_guard类模板,可以保证在类的构造函数中创建资源,在析构函数中释放资源,因为就算发生了异常,c++也能保证类的析构函数能够执行。

但是如果有多个mutex变量,仅仅使用lock_guard并不能保证不会发生死锁。

Thread A              Thread B
_mu.lock()          _mu2.lock()//死锁               //死锁
_mu2.lock()         _mu.lock()

这种情况下通过一些逻辑严格限制两个锁的顺序也是可以避免死锁的。

c++标准库中提供了std::lock()函数,能够保证将多个互斥锁同时上锁,

std::lock(_mu, _mu2);

1114. 按序打印

我们提供了一个类:

public class Foo {public void first() { print("first"); }public void second() { print("second"); }public void third() { print("third"); }
}

三个不同的线程将会共用一个 Foo 实例。

请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

示例 1:

输入: [1,2,3]
输出: "firstsecondthird"
解释:
有三个线程会被异步启动。
输入 [1,2,3] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 second() 方法,线程 C 将会调用 third() 方法。
正确的输出是 "firstsecondthird"。

分析:由于需要two()one()之前执行,所以two()必须等待one()执行后的某个条件达成,使用锁来实现同步。因此通过两个mutex变量,起初将两个变量锁上,只有第一个完成后将打开一把锁,然后第二个将运行,然后第二个运行后将打开另一把锁,第三个运行,需要两把锁都解开才可以运行。

class Foo {public:mutex smx;mutex tmx;Foo() {smx.lock();tmx.lock();}void first(function<void()> printFirst) {// printFirst() outputs "first". Do not change or remove this line.printFirst();smx.unlock();}void second(function<void()> printSecond) {lock_guard<mutex> lg(smx);// printSecond() outputs "second". Do not change or remove this line.printSecond();tmx.unlock();}void third(function<void()> printThird) {lock_guard<mutex> lg(tmx);// printThird() outputs "third". Do not change or remove this line.printThird();}
};

一文搞定c++多线程相关推荐

  1. 一文搞定c++多线程同步机制

    c++多线程同步机制 前序文章:一文搞定c++多线程 同步与互斥 现代操作系统都是多任务操作系统,通常同一时刻有大量可执行实体,则运行着的大量任务可能需要访问或使用同一资源,或者说这些任务之间具有依赖 ...

  2. php带参数单元测试_一文搞定单元测试核心概念

    基础概念 单元测试(unittesting),是指对软件中的最小可测试单元进行检查和验证,这里的最小可测试单元通常是指函数或者类.单元测试是即所谓的白盒测试,一般由开发人员负责测试,因为开发人员知道被 ...

  3. 【Python基础】一文搞定pandas的数据合并

    作者:来源于读者投稿 出品:Python数据之道 一文搞定pandas的数据合并 在实际处理数据业务需求中,我们经常会遇到这样的需求:将多个表连接起来再进行数据的处理和分析,类似SQL中的连接查询功能 ...

  4. 一文搞定Swing和Qt按钮和文本框的创建

    一文搞定Swing和Qt按钮和文本框的创建 Qt的截图 java的 源码 package com.lujun;import java.awt.Container;import javax.swing. ...

  5. 一文搞定C#关于NPOI类库的使用读写Excel以及io流文件的写出

    一文搞定C#关于NPOI类库的使用读写Excel以及io流文件的写出 今天我们使用NPOI类库读写xlsx文件, 最终实现的效果如图所示 从太平洋官网下载相应的类库,大概4~5MB,不要从github ...

  6. 一文搞定Qt读写excel以及qt读写xml数据

    一文搞定Qt读写excel以及qt读写xml数据 最终的实现效果图 RC_ICONS = logo.ico .pro文件同级目录下加入 logo.ico 图标文件,运行文件,文件的图标就被写入软件 u ...

  7. 一文搞定 Spring Data Redis 详解及实战

    转载自  一文搞定 Spring Data Redis 详解及实战 SDR - Spring Data Redis的简称. Spring Data Redis提供了从Spring应用程序轻松配置和访问 ...

  8. 一文搞定面试中的二叉树问题

    一文搞定面试中的二叉树问题 版权所有,转载请注明出处,谢谢! http://blog.csdn.net/walkinginthewind/article/details/7518888 树是一种比较重 ...

  9. 【全网最全】一文搞定 Linux 压缩、解压哪些事儿

    一文搞定 Linux 压缩.解压哪些事儿 Linux 常用的解压和压缩命令如下: 1..tar # 解包 tar xvf FileName.tar # 打包 tar cvf FileName.tar ...

最新文章

  1. GPTEE中定义的RSA的Algorithm Identifier详解
  2. cc2530定时器和捕获比较_STM32学习日志——输入捕获实验(20.06.26)
  3. python与java的比较_Python和Java两者有什么区别?
  4. 基准测试:Apache Ignite仍然领先于Hazelcast
  5. Jq将字符串复制粘贴到剪贴板
  6. 亚信安全发布《2022年网络安全发展趋势及十大威胁预测》
  7. 吴恩达|机器学习作业4.0神经网络反向传播(BP算法)
  8. meterpreter持久后门
  9. JZOJ.5264【NOIP2017模拟8.12】化学
  10. [BZOJ3110] [Zjoi2013]K大数查询
  11. HDU2500 做一个正气的杭电人【水题】
  12. python 特征选择方法_机器学习小窍门:Python 帮你进行特征选择
  13. mysql数据库文件查找网站后台密码_怎么查看数据库的密码?
  14. jar包扫描工具: gamma
  15. Android TextView带背景图片和自定义边框
  16. oracle日期转数值的函数,oracle数据库内置函数之数值函数、字符函数、日期函数、转换函数及其在查询语句中的运用...
  17. 安装服务器的win pe系统教程,u盘pe安装win7系统教程图解
  18. Ubuntu常用安装和卸载命令
  19. 计算两个时间戳型的时间差
  20. linux 下vi与vim区别以及vim的使用

热门文章

  1. 开源调度系统hello job
  2. Seata XA 模式示例分析
  3. 分布式事务解决方案:XA规范
  4. Golang 内存分配与逃逸分析
  5. 天气预报webservice
  6. 个人主权中文版pdf下载1.8 The Sovereign Individual+The Network State中文翻译网络国家+web3.0社区+DAO社区+NFT元宇宙+数字游民+移民润学
  7. JavaScript实现select下拉菜单省份和城市的级联菜单
  8. Android培训班(47)
  9. 【R】生成聊天记录词云
  10. countif和sum套用_SUM、SUMIF、COUNTIF函数的使用方法