c++线程--快速上手
线程创建
头文件#include thread 是在 C++11 标准中引入的。
C++11 标准引入了对多线程编程的标准化支持,其中包括了线程的创建、管理和同步机制。
头文件提供了基本的线程支持库,允许开发者直接使用c++线程进行并行编程,而无需依赖操作系统特定的 API
#include <iostream>
#include <thread>
using namespace std;
void hello() {
cout << "Hello World from new thread." << endl;
}
int main() {
thread t(hello);
t.join();
return 0;
}
为了使用多线程的接口,我们需要#include 头文件。新建线程的入口是一个普通的函数,它并没有什么特别的地方。
创建线程的方式就是构造一个thread对象,并指定入口函数。与普通对象不一样的是,此时编译器便会为我们创建一个新的操作系统线程,并在新的线程中执行我们的入口函数。
int main() {
thread t([] {
cout << "Hello World from lambda thread." << endl;
});
t.join();
return 0;
}
- 也可以直接使用lambda表达式
join & detach
API | 说明 |
---|---|
join | 等待线程完成其执行 |
detach | 不等待,允许线程独立执行 |
join:调用此接口时,当前线程会一直阻塞,直到目标线程执行完成(当然,很可能目标线程在此处调用之前就已经执行完成了,不过这不要紧)。因此,如果目标线程的任务非常耗时,你就要考虑好是否需要在主线程上等待它了,因此这很可能会导致主线程卡住。
detach:detach是让目标线程成为守护线程(daemon threads)。一旦detach之后,目标线程将独立执行,即便其对应的thread对象销毁也不影响线程的执行。并且,你无法再与之通信。
joinable() 是 C++11 中 std::thread 类的一个成员函数,用于检查一个线程是否可以被 join()。如果线程可以被 join(),则返回 true,否则返回 false。
joinable() 返回 true 的条件:线程对象必须表示一个有效的、尚未被 join() 或 detach() 的线程。
管理当前线程
API | C++标准 | 说明 |
---|---|---|
yield | C++11 | 让出处理器,重新调度各执行线程 |
get_id | C++11 | 返回当前线程的线程 id |
sleep_for | C++11 | 使当前线程的执行停止指定的时间段 |
sleep_until | C++11 | 使当前线程的执行停止直到指定的时间点 |
上面是一些在线程内部使用的API,它们用来对当前线程做一些控制。
- yield 通常用在自己的主要任务已经完成的时候,此时希望让出处理器给其他任务使用。
- get_id 返回当前线程的id,可以以此来标识不同的线程。
- sleep_for 是让当前线程停止一段时间。
- sleep_until 和sleep_for类似,但是是以具体的时间点为参数。这两个API都以chrono API(由于篇幅所限,这里不展开这方面内容)为基础。
下面是一个代码示例:
void print_time() {
auto now = chrono::system_clock::now();
auto in_time_t = chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << put_time(localtime(&in_time_t), "%Y-%m-%d %X");
cout << "now is: " << ss.str() << endl;
}
void sleep_thread() {
this_thread::sleep_for(chrono::seconds(3));
cout << "[thread-" << this_thread::get_id() << "] is waking up" << endl;
}
void loop_thread() {
for (int i = 0; i < 10; i++) {
cout << "[thread-" << this_thread::get_id() << "] print: " << i << endl;
}
}
int main() {
print_time();
thread t1(sleep_thread);
thread t2(loop_thread);
t1.join();
t2.detach();
print_time();
return 0;
}
这段代码应该还是比较容易理解的,这里创建了两个线程。它们都会有一些输出,其中一个会先停止3秒钟,然后再输出。主线程调用join会一直卡住等待它运行结束。
这段程序的输出如下:
now is: 2019-10-13 10:17:48
[thread-0x70000cdda000] print: 0
[thread-0x70000cdda000] print: 1
[thread-0x70000cdda000] print: 2
[thread-0x70000cdda000] print: 3
[thread-0x70000cdda000] print: 4
[thread-0x70000cdda000] print: 5
[thread-0x70000cdda000] print: 6
[thread-0x70000cdda000] print: 7
[thread-0x70000cdda000] print: 8
[thread-0x70000cdda000] print: 9
[thread-0x70000cd57000] is waking up
now is: 2019-10-13 10:17:51
线程如何一次调用?
主要API
API | C++标准 | 说明 |
---|---|---|
call_once | C++11 | 即便在多线程环境下,也能保证只调用某个函数一次 |
once_flag | C++11 | 与call_once配合使用 |
在一些情况下,我们有些任务需要执行一次,并且我们只希望它执行一次,例如资源的初始化任务。
这个时候就可以用到上面的接口。这个接口会保证,即便在多线程的环境下,相应的函数也只会调用一次。
下面就是一个示例:有三个线程都会使用init函数,但是只会有一个线程真正执行它。
void init() {
cout << "Initialing..." << endl;
// Do something...
}
void worker(once_flag* flag) {
call_once(*flag, init);
}
int main() {
once_flag flag;
thread t1(worker, &flag);
thread t2(worker, &flag);
thread t3(worker, &flag);
t1.join();
t2.join();
t3.join();
return 0;
}
我们无法确定具体是哪一个线程会执行init。而事实上,我们也不关心,因为只要有某个线程完成这个初始化工作就可以了。
- 请思考一下,为什么要在main函数中创建once_flag flag。如果是在worker函数中直接声明一个once_flag并使用行不行?为什么?
竞争条件与临界区
当多个进程或者线程同时访问共享数据时,只要有一个任务会修改数据,那么就可能会发生问题。
此时结果依赖于这些任务执行的相对时间,这种场景称为竞争条件(race condition)。
访问共享数据的代码片段称之为临界区(critical section)。要避免竞争条件,就需要对临界区进行数据保护。
mutex
mutex
是 mutual exclusion(互斥)的简写。
开发并发系统的目的主要是为了提升性能:将任务分散到多个线程,然后在不同的处理器上同时执行。这些分散开来的线程通常会包含两类任务:
- 独立的对于划分给自己的数据的处理
- 对于处理结果的汇总
第1项任务因为每个线程是独立的,不存在竞争条件的问题。
而第2项任务,由于所有线程都可能往总结果(例如 sum
变量)汇总,这就需要做保护了。
在某一个具体的时刻,只应当有一个线程更新总结果,即:保证每个线程对于共享数据的访问是“互斥”的。mutex
就提供了这样的功能。
mutex
是 mutual exclusion(互斥)的简写。
API | C++标准 | 说明 |
---|---|---|
mutex |
C++11 | 提供基本互斥设施 |
timed_mutex |
C++11 | 提供互斥设施,带有超时功能 |
recursive_mutex |
C++11 | 提供能被同一线程递归锁定的互斥设施 |
recursive_timed_mutex |
C++11 | 提供能被同一线程递归锁定的互斥设施,带有超时功能 |
shared_timed_mutex |
C++14 | 提供共享互斥设施并带有超时功能 |
shared_mutex |
C++17 | 提供共享互斥设施 |
在这些类中,mutex
是最基础的 API。其他类都是在它的基础上的改进。
这些类都提供了下面三个方法,并且它们的功能是一样的:
基本方法
方法 | 说明 |
---|---|
lock |
锁定互斥体,如果不可用,则阻塞 |
try_lock |
尝试锁定互斥体,如果不可用,直接返回 |
unlock |
解锁互斥体 |
这三个方法提供了基础的锁定和解除锁定的功能。
使用 lock
意味着你有很强的意愿一定要获取到互斥体,而使用 try_lock
则是进行一次尝试。
在这些基础功能之上,其他的类分别在下面三个方面进行了扩展:
超时:timed_mutex,recursive_timed_mutex,shared_timed_mutex的名称都带有timed,这意味着它们都支持超时功能。
它们都提供了try_lock_for和try_lock_until方法,这两个方法分别可以指定超时的时间长度和时间点。如果在超时的时间范围内没有能获取到锁,则直接返回,不再继续等待。
可重入:recursive_mutex和recursive_timed_mutex的名称都带有recursive。可重入或者叫做可递归,是指在同一个线程中,同一把锁可以锁定多次。这就避免了一些不必要的死锁。
共享:shared_timed_mutex和shared_mutex提供了共享功能。对于这类互斥体,实际上是提供了两把锁:一把是共享锁,一把是互斥锁。一旦某个线程获取了互斥锁,任何其他线程都无法再获取互斥锁和共享锁;但是如果有某个线程获取到了共享锁,其他线程无法再获取到互斥锁,但是还有获取到共享锁。这里互斥锁的使用和其他的互斥体接口和功能一样。而共享锁可以同时被多个线程同时获取到(使用共享锁的接口见下面的表格)。共享锁通常用在读者写者模型上。
方法 | 说明 |
---|---|
lock_shared |
获取互斥体的共享锁,如果无法获取则阻塞 |
try_lock_shared |
尝试获取共享锁,如果不可用,直接返回 |
unlock_shared |
解锁共享锁 |
使用 mutex
的并发系统
通过将锁保护的范围缩小到只在汇总时锁定,可以提升性能:
#include <iostream>
#include <cmath>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
static const int MAX = 10e8;
static double sum = 0;
static std::mutex exclusive;
void concurrent_worker(int min, int max) {
double tmp_sum = 0;
for (int i = min; i <= max; i++) {
tmp_sum += std::sqrt(i); // ① 计算单独结果
}
exclusive.lock(); // ② 只在汇总时加锁
sum += tmp_sum;
exclusive.unlock();
}
输出结果:
hardware_concurrency: 16
Concurrent task finish, 451 ms consumed, Result: 2.10819e+13
可以看到,性能得到了极大的提升。
锁的粒度
- 细粒度(fine-grained):锁保护较小的范围,性能更好。
- 粗粒度(coarse-grained):锁保护较大的范围,容易导致性能瓶颈。
最佳实践:尽量减少锁的范围,不要在持有锁的情况下执行耗时操作。
> In general, a lock should be held for only the minimum possible time needed to perform the required operations.
> –《C++ Concurrency in Action》
通用锁定算法
主要API
API | C++标准 | 说明 |
---|---|---|
lock | C++11 | 锁定指定的互斥体,若任何一个不可用则阻塞 |
try_lock | C++11 | 试图通过重复调用 try_lock 获得互斥体的所有权 |
要避免死锁,需要仔细思考和设计业务逻辑。
有一个比较简单的原则可以避免死锁:对所有的锁进行排序,每次一定要按照顺序来获取锁,不允许乱序。
例如:要获取某个玩具,一定要先拿到锁A,再拿到锁B,才能玩玩具。这样就不会死锁了。
这个原则虽然简单,但却不容易遵守,因为数据常常是分散在很多地方的。
不过好消息是,C++11标准中为我们提供了一些工具来避免因为多把锁而导致的死锁。我们只要直接调用这些接口就可以了。
这个就是上面提到的两个函数。它们都支持传入多个 Lockable 对象。
接下来我们用它来改造之前死锁的转账系统:
// 10_improved_bank_transfer.cpp
bool transferMoney(Account* accountA, Account* accountB, double amount) {
lock(*accountA->getLock(), *accountB->getLock()); // ①
lock_guard lockA(*accountA->getLock(), adopt_lock); // ②
lock_guard lockB(*accountB->getLock(), adopt_lock); // ③
if (amount > accountA->getMoney()) {
return false;
}
accountA->changeMoney(-amount);
accountB->changeMoney(amount);
return true;
}
这里只改动了3行代码。
- 这里通过lock函数来获取两把锁,标准库的实现会保证不会发生死锁。
- lock_guard在下面我们还会详细介绍。这里只要知道它会在自身对象生命周期的范围内锁定互斥体即可。
创建lock_guard的目的是为了在transferMoney结束的时候释放锁,lockB也是一样。
但需要注意的是,这里传递了 adopt_lock表示:现在是已经获取到互斥体了的状态了,不用再次加锁(如果不加adopt_lock就是二次锁定了)。
通用互斥管理
互斥体(mutex相关类)提供了对于资源的保护功能,但手动锁定(调用 lock
或 try_lock
)和解锁(调用 unlock
)互斥体需要耗费较大的精力。
我们需要精心设计代码以确保解锁和加锁配对,因为如果某个路径导致获取锁后没有正常释放,会影响整个系统。此外,异常抛出也会使代码复杂化。
鉴于此,标准库提供了上述这些API。它们使用了RAII编程技巧,简化了手动加锁和解锁的过程。
RAII 可总结如下:
将每个资源封装入一个类,其中:
构造函数请求资源,并建立所有类不变式,或在它无法完成时抛出异常,
析构函数释放资源并决不抛出异常;
始终经由 RAII 类的实例使用满足要求的资源,该资源自身拥有自动存储期或临时生存期,或具有与自动或临时对象的生存期绑定的生存期
API | C++标准 | 说明 |
---|---|---|
lock_guard |
C++11 | 实现严格基于作用域的互斥体所有权包装器 |
unique_lock |
C++11 | 实现可移动的互斥体所有权包装器 |
锁定策略 | C++标准 | 说明 |
---|---|---|
try_to_lock |
C++11 | 类型为 try_to_lock_t ,尝试获得互斥的所有权而不阻塞 |
#include <thread>
#include <mutex>
#include <iostream>
int g_i = 0;
std::mutex g_i_mutex; // ①
void safe_increment()
{
std::lock_guard<std::mutex> lock(g_i_mutex); // ②
++g_i;
std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
// ③
}
int main()
{
std::cout << "main: " << g_i << '\n';
std::thread t1(safe_increment); // ④
std::thread t2(safe_increment);
t1.join();
t2.join();
std::cout << "main: " << g_i << '\n';
}
这段代码中:
全局的互斥体g_i_mutex
用来保护全局变量g_i
这是一个设计为可以被多线程环境使用的方法。因此需要通过互斥体来进行保护。
这里没有调用lock
方法,而是直接使用lock_guard
来锁定互斥体。在方法结束的时候,局部变量std::lock_guard<std::mutex> lock
会被销毁,它对互斥体的锁定也就解除了。
条件变量
API | C++标准 | 说明 |
---|---|---|
condition_variable | C++ 11 | 提供与 std::unique_lock 关联的条件变量 |
条件变量提供了一个可以让多个线程间同步协作的功能。这对于生产者-消费者模型很有意义。在这个模型下:
生产者和消费者共享一个工作区。这个区间的大小是有限的。
生产者总是产生数据放入工作区中,当工作区满了。它就停下来等消费者消费一部分数据,然后继续工作。
消费者总是从工作区中拿出数据使用。当工作区中的数据全部被消费空了之后,它也会停下来等待生产者往工作区中放入新的数据。
从上面可以看到,无论是生产者还是消费者,当它们工作的条件不满足时,它们并不是直接报错返回,而是停下来等待,直到条件满足。
void changeMoney(double amount) {
unique_lock lock(mMoneyLock); // ②
mConditionVar.wait(lock, [this, amount] { // ③
return mMoney + amount > 0; // ④
});
mMoney += amount;
mConditionVar.notify_all(); // ⑤
}
在调用 wait() 函数之前需要加锁,主要是因为:
如果没有加锁,多个线程可能在没有同步的情况下同时检查条件****,从而导致多个线程错误地认为条件已经满足并继续执行。
加锁操作 (
unique_lock lock(mMoneyLock);
):
在第②行中,创建了一个std::unique_lock
对象lock
,并且立即对mMoneyLock
进行加锁。这是标准的加锁操作,它确保后续的临界区代码(即对共享资源mMoney
的修改)是线程安全的。wait()
函数 (mConditionVar.wait(lock, ...);
):
在第③行,wait()
函数是用来等待某个条件满足的,在条件满足之前,当前线程会阻塞并释放lock
持有的锁,以允许其他线程获得锁。- 锁释放:
wait()
函数在阻塞当前线程时,会自动释放unique_lock
所持有的mMoneyLock
,从而避免其他线程无法获取锁并造成死锁。 - 重新加锁:一旦条件满足(即 lambda 函数返回
true
,条件为mMoney + amount > 0
),wait()
会重新加锁mMoneyLock
,然后继续执行后续的代码。
- 锁释放:
临界区代码:
在条件变量wait()
成功返回之后,unique_lock
再次持有mMoneyLock
,这时线程进入临界区(即对mMoney
的修改),并且执行对mMoney
的操作。通知操作 (
notify_all()
):
在第⑤行,执行mConditionVar.notify_all()
通知其他等待线程,告诉它们某个条件可能已经发生了变化,让它们重新检查条件。
- 加锁解锁的行为:
std::condition_variable::wait()
函数在等待期间会释放锁,只有在条件满足时才会重新加锁并继续执行。因此,不存在重复上锁的情况。 - 锁的管理:
std::unique_lock
管理了整个过程的加锁和解锁行为,避免了手动管理锁的复杂性,并确保在需要时锁是正确的被持有。
异步机制
API | C++标准 | 说明 |
---|---|---|
async |
C++11 | 异步运行一个函数,并返回保有其结果的 std::future |
future |
C++11 | 等待被异步设置的值 |
很多语言都提供了异步的机制。异步使得耗时的操作不影响当前主线程的执行流。 |
std::async
和 线程 (std::thread
) 都是 C++ 中的并发机制,但它们有不同的使用方式、灵活性和处理方式。让我们来详细比较一下它们之间的区别。
- 抽象层级
std::async
:更高层次的抽象。它封装了线程的创建和管理,并且返回一个std::future
,允许用户轻松地获取异步任务的结果。async
不需要直接与线程打交道,主要关注任务的执行和结果。std::thread
:较低层次的抽象。它直接代表一个独立的线程。你需要手动创建线程,并决定如何管理它的生命周期(通过join()
或detach()
等操作)。它更灵活,但需要更多的管理工作。
c++线程--快速上手的更多相关文章
- 【Python五篇慢慢弹】快速上手学python
快速上手学python 作者:白宁超 2016年10月4日19:59:39 摘要:python语言俨然不算新技术,七八年前甚至更早已有很多人研习,只是没有现在流行罢了.之所以当下如此盛行,我想肯定是多 ...
- Objective-C快速上手
最近在开发iOS程序,这篇博文的内容是刚学习Objective-C时做的笔记,力图达到用最短的时间了解OC并使用OC.Objective-C是OS X 和 iOS平台上面的主要编程语言,它是C语言的超 ...
- 聊天系统Demo,增加文件传送功能(附源码)-- ESFramework 4.0 快速上手(14)
本文我们将介绍在ESFramework 4.0 快速上手(08) -- 入门Demo,一个简单的IM系统(附源码)的基础上,增加文件传送的功能.如果不了解如何使用ESFramework提供的文件传送功 ...
- 可靠通信的保障 —— 使用ACK机制发送自定义信息——ESFramework 通信框架4.0 快速上手(12)
使用ESPlus.Application.CustomizeInfo.Passive.ICustomizeInfoOutter接口的Send方法,我们已经可以给服务端或其它在线客户端发送自定义信息了, ...
- ESFramework 4.0 快速上手(01) -- Rapid引擎
(在阅读该文之前,请先阅读 ESFramework 4.0 概述 ,会对本文的理解更有帮助.) ESFramework/ESPlatform 4.0 的终极目标是为百万级的用户同时在线提供支持,因为强 ...
- Java开发快速上手
Java开发快速上手 前言 1.我的大学 2.对初学者的建议 3.大牛的三大特点 4.与他人的差距 第一章 了解Java开发语言 前言 基础常识 1.1 什么是Java 1.1.1 跨平台性 1.2 ...
- Flask入门和快速上手
目录 Flask入门和快速上手 python三大主流框架对比 Flask安装 依赖 可选依赖 创建flask项目 flask最小应用--hello word 非法导入名称 调试模式 路由 唯一的 UR ...
- WebAPI调用笔记 ASP.NET CORE 学习之自定义异常处理 MySQL数据库查询优化建议 .NET操作XML文件之泛型集合的序列化与反序列化 Asp.Net Core 轻松学-多线程之Task快速上手 Asp.Net Core 轻松学-多线程之Task(补充)
WebAPI调用笔记 前言 即时通信项目中初次调用OA接口遇到了一些问题,因为本人从业后几乎一直做CS端项目,一个简单的WebAPI调用居然浪费了不少时间,特此记录. 接口描述 首先说明一下,基于 ...
- 想要快速上手 Spring Boot?看这些教程就足够了!| 码云周刊第 81 期
原文:https://blog.gitee.com/2018/08/19/weekly-81/ 想要快速上手 Spring Boot?看这些教程就足够了!| 码云周刊第 81 期 码云周刊 | 201 ...
- 一文快速上手Logstash
原文地址:https://cloud.tencent.com/developer/article/1353068 Elasticsearch是当前主流的分布式大数据存储和搜索引擎,可以为用户提供强大的 ...
随机推荐
- 一键导入抓包数据生成HTTP请求
Jmeter一键导入抓包数据生成HTTP请求.路径:工具->Import from cURL 在弹框里粘贴cURL,点击"Create Test Plan"会自动生成HTTP ...
- Jmeter函数助手34-digest
digest函数用于返回特定哈希算法的加密值. 算法摘要:填入算法,如MD2.MD5.SHA-1.SHA-224.SHA-256.SHA-384.SHA-512 String to be hashed ...
- 【Git】Gitlab仓库访问拒绝,SSL校验影响
更新代码失败,不可访问[XX]仓库 fatal: unable to access 'https://gitcyx.yycsy.com/dmscloud/dcs/dcs-vue-coordinate. ...
- 2018年视频,路径规划:层次化路径规划系统——hierarchical pathfinding system —— Hierarchical Dynamic Pathfinding for Large Voxel Worlds (续)
前文: 2018年视频,路径规划:层次化路径规划系统--hierarchical pathfinding system -- Hierarchical Dynamic Pathfinding for ...
- conda报错、anconda报错:requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
anconda报错,报错信息: requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0) 不能使用c ...
- AI大模型 —— 国产大模型 —— 华为大模型
有这么一句话,那就是AI大模型分两种,一种是大模型:另一种是华为大模型. 如果从技术角度来分析,华为的技术不论是在软件还是硬件都比国外的大公司差距极大,甚至有些技术评论者认为华为的软硬件技术至少落后2 ...
- aarch64/arm_v8 环境下编译Arcade-Learning-Environment —— ale-py —— gym[atari]的安装
aarch64架构下不支持gym[atari]安装,因此我们只能在该环境下安装gym,对于atari环境的支持则需要源码上重新编译,也就是本文给出的下面的方法: 源码下载: https://githu ...
- java获取包下所有的类
1.背景 给一个Java的包名,获取包名下的所有类.. 根据类上的注解,可以展开很多统一操作的业务 2.直接看代码-spring环境下 package com.qxnw.digit.scm.commo ...
- bat2exe
https://blog.csdn.net/qq_23452385/article/details/109145151
- 2023 CCPC 哈尔滨游记
board zsy 11.3 下了高代课跟教练聊了会,以为差点赶不上飞机了,结果还好.飞机上一直在看<笑傲江湖> 晚上本来想写作业的,还是摆了 拉 zsy 打雀魂,三人麻将到第二天了 11 ...