线程创建

头文件#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. 对于处理结果的汇总

第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相关类)提供了对于资源的保护功能,但手动锁定(调用 locktry_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() 函数之前需要加锁,主要是因为:

如果没有加锁,多个线程可能在没有同步的情况下同时检查条件****,从而导致多个线程错误地认为条件已经满足并继续执行。

  1. 加锁操作 (unique_lock lock(mMoneyLock);)

    在第②行中,创建了一个 std::unique_lock 对象 lock,并且立即对 mMoneyLock 进行加锁。这是标准的加锁操作,它确保后续的临界区代码(即对共享资源 mMoney 的修改)是线程安全的。

  2. wait() 函数 (mConditionVar.wait(lock, ...);)

    在第③行,wait() 函数是用来等待某个条件满足的,在条件满足之前,当前线程会阻塞并释放 lock 持有的锁,以允许其他线程获得锁。

    • 锁释放wait() 函数在阻塞当前线程时,会自动释放 unique_lock 所持有的 mMoneyLock,从而避免其他线程无法获取锁并造成死锁。
    • 重新加锁:一旦条件满足(即 lambda 函数返回 true,条件为 mMoney + amount > 0),wait() 会重新加锁 mMoneyLock,然后继续执行后续的代码。
  3. 临界区代码

    在条件变量 wait() 成功返回之后,unique_lock 再次持有 mMoneyLock,这时线程进入临界区(即对 mMoney 的修改),并且执行对 mMoney 的操作。

  4. 通知操作 (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++线程--快速上手的更多相关文章

  1. 【Python五篇慢慢弹】快速上手学python

    快速上手学python 作者:白宁超 2016年10月4日19:59:39 摘要:python语言俨然不算新技术,七八年前甚至更早已有很多人研习,只是没有现在流行罢了.之所以当下如此盛行,我想肯定是多 ...

  2. Objective-C快速上手

    最近在开发iOS程序,这篇博文的内容是刚学习Objective-C时做的笔记,力图达到用最短的时间了解OC并使用OC.Objective-C是OS X 和 iOS平台上面的主要编程语言,它是C语言的超 ...

  3. 聊天系统Demo,增加文件传送功能(附源码)-- ESFramework 4.0 快速上手(14)

    本文我们将介绍在ESFramework 4.0 快速上手(08) -- 入门Demo,一个简单的IM系统(附源码)的基础上,增加文件传送的功能.如果不了解如何使用ESFramework提供的文件传送功 ...

  4. 可靠通信的保障 —— 使用ACK机制发送自定义信息——ESFramework 通信框架4.0 快速上手(12)

    使用ESPlus.Application.CustomizeInfo.Passive.ICustomizeInfoOutter接口的Send方法,我们已经可以给服务端或其它在线客户端发送自定义信息了, ...

  5. ESFramework 4.0 快速上手(01) -- Rapid引擎

    (在阅读该文之前,请先阅读 ESFramework 4.0 概述 ,会对本文的理解更有帮助.) ESFramework/ESPlatform 4.0 的终极目标是为百万级的用户同时在线提供支持,因为强 ...

  6. Java开发快速上手

    Java开发快速上手 前言 1.我的大学 2.对初学者的建议 3.大牛的三大特点 4.与他人的差距 第一章 了解Java开发语言 前言 基础常识 1.1 什么是Java 1.1.1 跨平台性 1.2 ...

  7. Flask入门和快速上手

    目录 Flask入门和快速上手 python三大主流框架对比 Flask安装 依赖 可选依赖 创建flask项目 flask最小应用--hello word 非法导入名称 调试模式 路由 唯一的 UR ...

  8. WebAPI调用笔记 ASP.NET CORE 学习之自定义异常处理 MySQL数据库查询优化建议 .NET操作XML文件之泛型集合的序列化与反序列化 Asp.Net Core 轻松学-多线程之Task快速上手 Asp.Net Core 轻松学-多线程之Task(补充)

    WebAPI调用笔记   前言 即时通信项目中初次调用OA接口遇到了一些问题,因为本人从业后几乎一直做CS端项目,一个简单的WebAPI调用居然浪费了不少时间,特此记录. 接口描述 首先说明一下,基于 ...

  9. 想要快速上手 Spring Boot?看这些教程就足够了!| 码云周刊第 81 期

    原文:https://blog.gitee.com/2018/08/19/weekly-81/ 想要快速上手 Spring Boot?看这些教程就足够了!| 码云周刊第 81 期 码云周刊 | 201 ...

  10. 一文快速上手Logstash

    原文地址:https://cloud.tencent.com/developer/article/1353068 Elasticsearch是当前主流的分布式大数据存储和搜索引擎,可以为用户提供强大的 ...

随机推荐

  1. 一键导入抓包数据生成HTTP请求

    Jmeter一键导入抓包数据生成HTTP请求.路径:工具->Import from cURL 在弹框里粘贴cURL,点击"Create Test Plan"会自动生成HTTP ...

  2. Jmeter函数助手34-digest

    digest函数用于返回特定哈希算法的加密值. 算法摘要:填入算法,如MD2.MD5.SHA-1.SHA-224.SHA-256.SHA-384.SHA-512 String to be hashed ...

  3. 【Git】Gitlab仓库访问拒绝,SSL校验影响

    更新代码失败,不可访问[XX]仓库 fatal: unable to access 'https://gitcyx.yycsy.com/dmscloud/dcs/dcs-vue-coordinate. ...

  4. 2018年视频,路径规划:层次化路径规划系统——hierarchical pathfinding system —— Hierarchical Dynamic Pathfinding for Large Voxel Worlds (续)

    前文: 2018年视频,路径规划:层次化路径规划系统--hierarchical pathfinding system -- Hierarchical Dynamic Pathfinding for ...

  5. 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 ...

  6. AI大模型 —— 国产大模型 —— 华为大模型

    有这么一句话,那就是AI大模型分两种,一种是大模型:另一种是华为大模型. 如果从技术角度来分析,华为的技术不论是在软件还是硬件都比国外的大公司差距极大,甚至有些技术评论者认为华为的软硬件技术至少落后2 ...

  7. aarch64/arm_v8 环境下编译Arcade-Learning-Environment —— ale-py —— gym[atari]的安装

    aarch64架构下不支持gym[atari]安装,因此我们只能在该环境下安装gym,对于atari环境的支持则需要源码上重新编译,也就是本文给出的下面的方法: 源码下载: https://githu ...

  8. java获取包下所有的类

    1.背景 给一个Java的包名,获取包名下的所有类.. 根据类上的注解,可以展开很多统一操作的业务 2.直接看代码-spring环境下 package com.qxnw.digit.scm.commo ...

  9. bat2exe

    https://blog.csdn.net/qq_23452385/article/details/109145151

  10. 2023 CCPC 哈尔滨游记

    board zsy 11.3 下了高代课跟教练聊了会,以为差点赶不上飞机了,结果还好.飞机上一直在看<笑傲江湖> 晚上本来想写作业的,还是摆了 拉 zsy 打雀魂,三人麻将到第二天了 11 ...