C++20 协同调度原语:利用 std::atomic::wait/notify 实现低功耗自旋锁在高并发下的快速响应协议

各位同仁,女士们,先生们,

欢迎来到今天的技术讲座。在现代C++编程中,高性能与低功耗的追求从未停止。随着多核处理器的普及和异步编程模型的兴起,对并发原语的精细化控制变得尤为关键。C++20标准为我们带来了诸多激动人心的新特性,其中协程(coroutines)和原子操作的增强,为构建下一代高效并发系统提供了坚实的基础。

今天,我们将深入探讨C++20中如何利用std::atomic::waitstd::atomic::notify这两个强大的原语,来设计并实现一个在高并发场景下兼顾快速响应与低功耗的自旋锁。我们将剖析其内部机制,探讨其在协程调度中的潜在作用,并提供一个详尽的实现范例。

1. 传统自旋锁的困境与现代并发的需求

在并发编程中,锁是实现互斥访问共享资源的基本机制。自旋锁(Spinlock)是一种简单而高效的锁,其基本思想是当一个线程尝试获取锁但失败时,它不会立即放弃CPU,而是反复检查锁的状态,直到锁可用。这种“忙等待”(busy-waiting)的特性使其在临界区非常短、且预期锁竞争不激烈的场景下表现出色,因为它避免了操作系统上下文切换的开销。

然而,传统自旋锁的缺点也同样显著:

  • 高CPU占用率和功耗: 当锁竞争激烈时,大量线程在CPU上空转,持续消耗CPU周期,导致能源浪费和系统整体性能下降。
  • 缓存颠簸(Cache Thrashing): 多个CPU核心反复读取和修改同一个锁变量,可能导致缓存行在不同核心之间频繁迁移,增加内存访问延迟。
  • 优先级反转: 如果持有锁的低优先级线程被高优先级线程抢占,那么高优先级线程可能会长时间自旋,无法执行,导致系统响应变慢。

随着现代应用对并发性能和能效的要求日益提高,尤其是在构建高性能服务器、游戏引擎或实时系统中,我们需要一种能够结合自旋锁的低延迟优势与互斥锁的低功耗特性的混合方案。C++20的std::atomic::waitstd::atomic::notify正是解决这一问题的关键工具。

2. C++20 std::atomic::wait/notify 原理解析

std::atomic::waitstd::atomic::notify是C++20引入的原子操作,它们提供了一种高效的线程/协程停车(parking)和唤醒机制。与传统的std::condition_variable相比,它们直接作用于原子变量,减少了额外的对象和同步开销,尤其适用于构建底层同步原语。

2.1 std::atomic::wait

template< class T > void atomic<T>::wait( T old_val, std::memory_order order = std::memory_order::seq_cst ) const volatile noexcept;

  • 功能: 如果当前原子变量的值等于old_val,则当前线程/协程被阻塞,进入等待状态。当原子变量的值被notify唤醒,或者发生“虚假唤醒”(spurious wakeup)时,线程/协程会解除阻塞。
  • old_val 这是等待的谓词(predicate)。只有当原子变量的当前值与old_val相等时,线程才会被阻塞。这是为了避免丢失唤醒(lost wakeup)问题:如果在调用wait之前,原子变量的值已经改变,那么就不应该阻塞。
  • order 指定内存顺序。wait操作隐含地执行一个获取(acquire)操作。
  • 虚假唤醒:std::condition_variable一样,std::atomic::wait也可能发生虚假唤醒。因此,在wait返回后,必须重新检查条件

2.2 std::atomic::notify_onestd::atomic::notify_all

template< class T > void atomic<T>::notify_one() volatile noexcept;
template< class T > void atomic<T>::notify_all() volatile noexcept;

  • 功能:
    • notify_one():唤醒一个正在等待该原子变量的线程/协程。
    • notify_all():唤醒所有正在等待该原子变量的线程/协程。
  • 内存顺序: notify操作隐含地执行一个释放(release)操作。

2.3 wait/notify 的优势

  • 零开销或低开销: 当没有等待者时,notify操作通常是零开销的。当有等待者时,wait/notify的开销通常低于std::condition_variable,因为它不需要额外的互斥量来保护条件变量本身的状态。
  • 直接作用于原子变量: 这使得它们可以更容易地集成到自定义同步原语中,并与原子操作的内存模型无缝衔接。
  • 操作系统集成: wait操作最终会调用操作系统底层的“停车”(park)机制(例如Linux上的futex,Windows上的WaitOnAddress),将线程从调度队列中移除,从而实现真正的低功耗等待。

3. 构建低功耗自旋锁的协议设计

我们的目标是设计一个混合式自旋锁:在低竞争情况下,它表现为高效的自旋锁;在高竞争情况下,它能将等待线程/协程停车,避免忙等待,从而实现低功耗。

3.1 锁状态表示

我们使用一个std::atomic<bool>std::atomic<int>来表示锁的状态。为了简化,我们使用int

  • 0:锁未被占用。
  • 1:锁被占用,且没有等待者。
  • 2:锁被占用,且有等待者正在wait

使用int而不是bool的原因是,我们需要区分“锁被占用但无等待者”和“锁被占用且有等待者”这两种状态,以便在释放锁时决定是否需要调用notify

3.2 锁定(lock())协议

  1. 乐观尝试: 使用compare_exchange_weak(或strong)尝试将锁状态从0(未占用)原子性地设置为1(占用无等待)。如果成功,立即获取锁并返回。这是低竞争路径,开销最小。
  2. 自旋退让(Bounded Spin-Backoff): 如果乐观尝试失败(说明锁已被占用),线程进入一个有限的自旋循环。
    • 在每次自旋迭代中,再次尝试使用compare_exchange_weak将锁状态从0设置为1
    • 为了减少缓存颠簸和提高能效,在每次自旋迭代中,我们引入指数退让(exponential backoff)策略,并使用_mm_pause(x86/x64平台)或std::this_thread::yield
      • _mm_pause是一个CPU指令,它提示处理器当前线程处于自旋等待状态,允许处理器进行一些节能优化,并防止过度的乱序执行导致缓存失效。
      • std::this_thread::yield提示操作系统可以调度其他线程。
    • 自旋循环的次数应该有一个上限,以避免长时间忙等待。
  3. 进入等待状态: 如果经过了最大自旋次数后,仍未能获取锁,说明竞争非常激烈。此时,线程需要将自己停车。
    • 首先,它尝试将锁状态从1(占用无等待)原子性地设置为2(占用有等待)。如果成功,说明它成为第一个等待者,然后调用wait(2)
    • 如果设置失败(说明锁状态已经是2,即已有其他等待者),或者在自旋过程中没有成功获取锁,它直接调用wait(2)
    • wait(2)会阻塞线程,直到被notify唤醒,或者发生虚假唤醒。
    • 重要: wait返回后,必须重新回到步骤1,重新尝试获取锁,因为可能是虚假唤醒,或者唤醒后锁又被其他线程抢走了。这个循环会一直持续,直到成功获取锁。

3.3 解锁(unlock())协议

  1. 释放锁: 使用compare_exchange_strong尝试将锁状态从1(占用无等待)原子性地设置为0(未占用)。
  2. 检查等待者:
    • 如果步骤1成功,说明没有等待者,直接返回。
    • 如果步骤1失败,说明锁状态为2(占用有等待),此时需要将锁状态从2原子性地设置为0,并调用notify_one()(或notify_all())唤醒一个或所有等待的线程。
  3. 选择notify_one还是notify_all
    • 对于互斥锁,通常notify_one是更优的选择,因为一次只需要一个线程获取锁。notify_all可能导致“惊群效应”(thundering herd),即所有被唤醒的线程都去竞争锁,大部分会再次失败,从而浪费CPU周期。
    • 在某些特定场景(如屏障)下,notify_all可能更合适。对于自旋锁,我们倾向于notify_one

3.4 内存顺序(Memory Orderings)

正确使用内存顺序是确保并发程序正确性的关键。

  • lock()操作:
    • 乐观尝试和自旋尝试:compare_exchange_weak(0, 1, std::memory_order_acquire, std::memory_order_relaxed)。成功时使用acquire语义,确保在锁之前的所有内存写入对当前线程可见。失败时使用relaxed,因为只是读取状态,不涉及数据同步。
    • 更新状态为2compare_exchange_weak(1, 2, std::memory_order_relaxed, std::memory_order_relaxed)。这里只需要原子更新,不涉及数据同步。
    • wait(2)wait操作本身隐含acquire语义,因为它在被唤醒后,会看到notify之前的所有内存写入。
  • unlock()操作:
    • 释放锁:compare_exchange_strong(1, 0, std::memory_order_release, std::memory_order_relaxed)exchange(0, std::memory_order_release)release语义确保在锁内进行的所有内存写入在锁被释放时对其他线程可见。
    • notify_one()notify操作本身隐含release语义。

下表总结了常用的内存顺序及其含义:

std::memory_order 语义 描述 适用场景
relaxed 宽松 不施加任何同步或排序约束。 仅需原子性操作,不关心其他线程的内存可见性。
consume 消费 确保依赖于被读取值的后续内存访问不会被重排。 读-写依赖链的消费者。
acquire 获取 确保此操作之后的内存访问不会被重排到此操作之前。获取锁时常用。 消费者端,确保在获取锁后看到生产者释放的所有内存修改。
release 释放 确保此操作之前的内存访问不会被重排到此操作之后。释放锁时常用。 生产者端,确保在释放锁前所有内存修改都已完成并对消费者可见。
acq_rel 获取-释放 兼具acquirerelease语义。 对同一个原子变量进行读-改-写操作,既要获取又要释放。
seq_cst 顺序一致 最强的内存顺序,保证所有线程看到的所有seq_cst操作都是以相同全局顺序执行的。开销最大。 默认内存顺序,适用于不确定或需要最强保证的场景,但应谨慎使用。

4. 协程调度中的应用

虽然std::atomic::wait/notify本身是线程级的同步原语,但它们是构建高效协程调度器的基石。在基于协程的异步编程模型中,当一个协程尝试获取一个锁并失败时,它不应该阻塞整个线程。相反,协程应该挂起(suspend)自身的执行,将控制权交还给调度器,让调度器去运行其他准备就绪的协程。当锁被释放时,调度器再唤醒并恢复之前挂起的协程。

我们的HybridSpinlock可以完美地融入这种模型:

  • 当协程调用lock()并进入wait(2)状态时,它实际上是将执行它的底层线程停车
  • 在一个协程调度器中,lock()方法可以被封装成一个awaitable对象。当lock()内部的wait(2)被调用时,协程会co_await这个awaitable,调度器会记录下这个协程的句柄,并将其所在的线程停车。
  • unlock()被调用,notify_one()唤醒一个线程时,调度器会接收到这个唤醒通知,然后将对应的协程重新加入到就绪队列中,等待被再次调度执行。

这种结合方式使得协程可以在不占用CPU资源的情况下等待锁,极大地提高了并发性能和资源利用率。

5. HybridSpinlock 完整实现示例

下面是一个HybridSpinlock的C++20实现。为了跨平台兼容性,我们使用std::this_thread::yield作为退让机制,同时提供_mm_pause的X86/X64特定优化。

#include <atomic>
#include <thread>
#include <chrono>
#include <vector>
#include <iostream>
#include <numeric> // For std::iota

// 平台特定的暂停指令
#if defined(_MSC_VER)
#include <intrin.h> // For _mm_pause on MSVC
#define PAUSE_INSTRUCTION _mm_pause()
#elif defined(__GNUC__) || defined(__clang__)
#define PAUSE_INSTRUCTION __builtin_ia32_pause()
#else
#define PAUSE_INSTRUCTION std::this_thread::yield() // Fallback for other platforms
#endif

// 缓存行大小,用于对齐
// 通常为64字节,但可以根据实际CPU架构调整
constexpr size_t CACHE_LINE_SIZE = 64;

class alignas(CACHE_LINE_SIZE) HybridSpinlock {
private:
    // 锁状态:
    // 0: 未锁定
    // 1: 已锁定,无等待者
    // 2: 已锁定,有等待者
    std::atomic<int> m_state;

    // 自旋退让的最大迭代次数
    static constexpr int MAX_SPIN_COUNT = 4000;
    // 在自旋结束后,进入等待状态前的退让次数
    static constexpr int MAX_YIELD_COUNT = 100;

public:
    HybridSpinlock() noexcept : m_state(0) {}

    // 删除拷贝构造和赋值操作符,防止意外复制
    HybridSpinlock(const HybridSpinlock&) = delete;
    HybridSpinlock& operator=(const HybridSpinlock&) = delete;

    void lock() noexcept {
        int spin_count = 0;
        int yield_count = 0;

        // 1. 乐观尝试获取锁 (低竞争路径)
        // 尝试将0设置为1 (未锁定 -> 已锁定,无等待者)
        if (m_state.compare_exchange_strong(0, 1, std::memory_order_acquire, std::memory_order_relaxed)) {
            return; // 成功获取锁
        }

        // 2. 进入自旋退让循环 (中等竞争路径)
        while (spin_count < MAX_SPIN_COUNT) {
            // 如果锁当前未被占用 (0),则尝试获取它
            if (m_state.load(std::memory_order_relaxed) == 0 &&
                m_state.compare_exchange_strong(0, 1, std::memory_order_acquire, std::memory_order_relaxed)) {
                return; // 成功获取锁
            }

            // 引入指数退让,减少CPU忙等待和缓存颠簸
            // 每次循环暂停一段时间,从1到32
            // 见 intel 64 and IA-32 Architectures Optimization Reference Manual
            // 章节 8.5.6 Software-Controlled Pre-fetch
            if (spin_count < 16) {
                // 短暂停,例如使用 PAUSE 指令
                for (int i = 0; i < (1 << spin_count); ++i) {
                    PAUSE_INSTRUCTION;
                }
            } else {
                // 超过一定次数后,让出CPU时间片
                std::this_thread::yield();
            }
            spin_count++;
        }

        // 3. 自旋失败,进入等待状态 (高竞争路径)
        while (true) {
            // 尝试将锁状态从0 (未锁定) 设置为 1 (已锁定,无等待者)
            // 这种情况发生在其他线程释放锁,但还没有唤醒等待者时
            if (m_state.compare_exchange_strong(0, 1, std::memory_order_acquire, std::memory_order_relaxed)) {
                return; // 成功获取锁
            }

            // 如果锁被持有 (无论是1还是2),并且当前没有等待者 (状态为1)
            // 尝试将其标记为2 (已锁定,有等待者)
            int expected_state = 1;
            if (m_state.compare_exchange_strong(expected_state, 2, std::memory_order_relaxed, std::memory_order_relaxed)) {
                // 成功将状态从1变为2,说明我们是第一个等待者
                // 此时,原子变量的值是2。我们将等待它变为其他值 (通常是0)
                m_state.wait(2, std::memory_order_relaxed); // wait 会隐含 acquire 语义
            } else if (expected_state == 2) {
                // 锁已经是2 (已锁定,有等待者),直接等待
                m_state.wait(2, std::memory_order_relaxed); // wait 会隐含 acquire 语义
            } else { // expected_state == 0
                // 锁已经被释放(状态为0),但我们没有在上面成功CAS
                // 可能是由于其他线程竞争或时序问题,继续循环尝试
                // 这里为了避免无限自旋,可以加入短暂的 yield
                if (yield_count < MAX_YIELD_COUNT) {
                    std::this_thread::yield();
                    yield_count++;
                } else {
                    // 超过一定退让次数仍未获取,直接进入 wait 状态
                    // 这里可以重新尝试 CAS(0,1) 或者直接 wait(0)
                    // 为了简化,我们继续通过 wait(2) 来处理
                    // 但需要确保 m_state.load() == 0 时,我们能够被唤醒
                    // 实际上,如果 m_state.load() == 0,我们将通过外层循环的 CAS(0,1) 成功获取
                    // 所以这里更倾向于 wait(2) 或者 wait(load_value_if_not_0_or_1)
                }
            }
            // 每次wait返回后,都必须重新检查条件,因为可能是虚假唤醒
            // 外层while(true)循环会处理这个重新检查
        }
    }

    void unlock() noexcept {
        // 尝试将锁从1 (已锁定,无等待者) 设为 0 (未锁定)
        // 如果成功,说明没有等待者,直接返回
        if (m_state.compare_exchange_strong(1, 0, std::memory_order_release, std::memory_order_relaxed)) {
            return;
        }

        // 如果状态不是1,那么它一定是2 (已锁定,有等待者)
        // 尝试将锁从2 (已锁定,有等待者) 设为 0 (未锁定)
        // 确保这个 CAS 是原子性的,并且在此操作之前的所有写入都已对其他线程可见
        // 如果 CAS 失败,说明在我们检查m_state后,它又变回了1 (例如,新的等待者尝试获取锁)
        // 这种情况下,我们仍需要通知一个等待者
        int expected_state = 2;
        m_state.compare_exchange_strong(expected_state, 0, std::memory_order_release, std::memory_order_relaxed);

        // 唤醒一个等待者
        m_state.notify_one();
    }
};

// 示例:使用 HybridSpinlock 保护一个共享计数器
std::atomic<long long> shared_counter = 0;
HybridSpinlock my_spinlock;
const int NUM_THREADS = 8;
const long long ITERATIONS_PER_THREAD = 1000000;

void increment_thread_func() {
    for (long long i = 0; i < ITERATIONS_PER_THREAD; ++i) {
        std::lock_guard<HybridSpinlock> lock(my_spinlock);
        shared_counter++;
    }
}

int main() {
    std::cout << "Starting HybridSpinlock test with " << NUM_THREADS << " threads and "
              << ITERATIONS_PER_THREAD << " iterations per thread." << std::endl;

    auto start_time = std::chrono::high_resolution_clock::now();

    std::vector<std::thread> threads;
    for (int i = 0; i < NUM_THREADS; ++i) {
        threads.emplace_back(increment_thread_func);
    }

    for (auto& t : threads) {
        t.join();
    }

    auto end_time = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsed = end_time - start_time;

    std::cout << "Final counter value: " << shared_counter << std::endl;
    std::cout << "Expected counter value: " << NUM_THREADS * ITERATIONS_PER_THREAD << std::endl;
    std::cout << "Time taken: " << elapsed.count() << " seconds" << std::endl;

    if (shared_counter == NUM_THREADS * ITERATIONS_PER_THREAD) {
        std::cout << "Test PASSED: Counter value matches expected." << std::endl;
    } else {
        std::cout << "Test FAILED: Counter value mismatch." << std::endl;
    }

    // 简单对比 std::mutex
    std::cout << "nStarting std::mutex test for comparison..." << std::endl;
    std::mutex std_mutex;
    std::atomic<long long> mutex_counter = 0;

    auto mutex_start_time = std::chrono::high_resolution_clock::now();

    std::vector<std::thread> mutex_threads;
    for (int i = 0; i < NUM_THREADS; ++i) {
        mutex_threads.emplace_back([&]() {
            for (long long i = 0; i < ITERATIONS_PER_THREAD; ++i) {
                std::lock_guard<std::mutex> lock(std_mutex);
                mutex_counter++;
            }
        });
    }

    for (auto& t : mutex_threads) {
        t.join();
    }

    auto mutex_end_time = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> mutex_elapsed = mutex_end_time - mutex_start_time;

    std::cout << "Final mutex counter value: " << mutex_counter << std::endl;
    std::cout << "Time taken (std::mutex): " << mutex_elapsed.count() << " seconds" << std::endl;

    return 0;
}

代码解释:

  1. alignas(CACHE_LINE_SIZE) 确保m_state变量位于独立的缓存行中,防止假共享(false sharing),这在高并发场景下对性能至关重要。
  2. MAX_SPIN_COUNTMAX_YIELD_COUNT 定义了在进入wait状态前,自旋和std::this_thread::yield的最大次数。这些值需要根据具体的CPU架构、系统负载和临界区长度进行调优。
  3. lock()方法:
    • 首先尝试compare_exchange_strong(0, 1),这是最快的路径。
    • 如果失败,进入while (spin_count < MAX_SPIN_COUNT)循环进行自旋退让。这里使用了指数退让和PAUSE_INSTRUCTION,在X86/X64架构上,_mm_pause是比std::this_thread::yield更轻量级的CPU指令,能有效降低功耗和减少缓存冲突。
    • 如果自旋退让后仍未能获取锁,进入外部while(true)循环。在这个循环中,线程会尝试将状态从1变为2(标记有等待者),然后调用m_state.wait(2)将自身停车。wait返回后,会重新从头开始尝试获取锁。
  4. unlock()方法:
    • 首先尝试compare_exchange_strong(1, 0),如果没有等待者,直接释放锁。
    • 如果m_state2(有等待者),则将m_state设置为0,并调用m_state.notify_one()唤醒一个等待者。
  5. PAUSE_INSTRUCTION宏: 根据编译器和平台,选择_mm_pause__builtin_ia32_pause,否则回退到std::this_thread::yield
  6. main函数中的测试: 启动多个线程并发递增一个共享计数器,并与std::mutex进行简单对比,以展示其性能特征。

6. 性能考量与调优

  • MAX_SPIN_COUNT的选择: 这是一个关键参数。过小会导致过早进入wait状态,增加上下文切换开销;过大则可能导致不必要的忙等待,浪费CPU资源。通常,它应该与临界区的预期执行时间成比例。
  • PAUSE_INSTRUCTION的利用: 在支持_mm_pause的平台上,务必使用它。它是一个“处理器提示”,可以显著降低自旋等待时的功耗和缓存压力。
  • 内存顺序: acquirerelease语义是确保内存可见性的核心。在我们的实现中,compare_exchange_strong在成功时使用acquire,在unlock时使用release,这符合标准的锁实现模式。waitnotify也隐含了相应的内存顺序。
  • 缓存行对齐: alignas(CACHE_LINE_SIZE)对于防止假共享至关重要。如果锁变量与不相关的其他数据共享同一个缓存行,那么对这些不相关数据的修改也可能导致缓存行在不同核心间频繁迁移,从而降低锁的性能。
  • notify_one vs notify_all 对于互斥锁,notify_one通常是最佳选择,因为它避免了“惊群效应”。只有在需要唤醒所有等待者才能继续的场景(如屏障)才考虑notify_all
  • 公平性: 这种自旋锁是不公平的。被唤醒的线程或在自旋中恰好成功的线程,不一定是等待时间最长的线程。在大多数高性能场景中,公平性通常不如吞吐量重要。

7. 与其他同步原语的比较

理解HybridSpinlock的特点,有助于我们选择合适的同步机制。

特性 HybridSpinlock (本文实现) std::mutex 纯自旋锁 (std::atomic_flag) std::condition_variable
基础机制 短自旋 + atomic::wait/notify 操作系统互斥量 (futex, WaitForSingleObject) 忙等待 (busy-waiting) mutex + 条件等待 (wait/notify)
适用场景 临界区短,竞争可能剧烈,需低功耗和低延迟 临界区长,竞争剧烈,或需线程停车 临界区极短,竞争不激烈,追求极致低延迟 复杂条件等待,生产者-消费者模型,非互斥同步
CPU 占用 低 (高竞争时停车) 低 (总是停车) 高 (始终忙等待) 低 (与 mutex 结合,停车)
功耗
上下文切换 高竞争时可能发生 总是发生 从不发生 总是发生
延迟 低 (低竞争时与自旋锁相当) 中高 极低 高 (包含 mutex 和条件变量自身开销)
虚假唤醒 是 (需循环检查) 否 (操作系统管理) 是 (需循环检查)
API 复杂性 中等 (需要手动实现) 简单 (标准库提供) 简单 (原子操作) 中等 (需要 mutexcondition_variable)

选择建议:

  • std::mutex 默认选择。如果临界区执行时间较长,或者不确定竞争模式,std::mutex是最安全、最易用的选择。
  • 纯自旋锁: 仅在临界区极其短(几个CPU指令)、且几乎无竞争的场景下考虑。例如,一些低级调度器或硬件交互。
  • HybridSpinlock 当你需要在低竞争时保持极低延迟,同时在高竞争时避免CPU忙等待时,本文实现的这种混合式自旋锁是理想选择。它在性能和能耗之间取得了很好的平衡。尤其是在构建高性能协程调度器时,它能提供比std::mutex更细粒度的控制。
  • std::condition_variable 用于实现比简单互斥更复杂的线程间通信和协调逻辑,例如生产者-消费者队列、任务调度等。

8. 构建高效并发系统的基石

C++20的std::atomic::waitstd::atomic::notify为C++程序员提供了前所未有的底层同步原语,使得我们能够以更精细的方式控制并发行为。通过将它们与传统的自旋退让策略相结合,我们能够构建出像HybridSpinlock这样既能快速响应又低功耗的同步机制。

这种技术不仅适用于实现互斥锁,还可以作为构建更复杂并发数据结构和协程调度器的基石。在追求极致性能和能效的现代软件开发中,掌握并善用这些C++20的新特性,将是构建高效、健壮系统的关键能力。

今天的讲座就到这里。感谢大家的聆听!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注