当前位置: 首页 > article >正文

C++并发编程指南07

文章目录

    • @[TOC]
      • 5.1 内存模型
        • 5.1.1 对象和内存位置
          • 图5.1 分解一个 `struct`,展示不同对象的内存位置
        • 5.1.2 对象、内存位置和并发
        • 5.1.3 修改顺序
          • 示例代码
      • 5.2 原子操作和原子类型
        • 5.2.1 标准原子类型
          • 标准库中的原子类型
          • 特殊的原子类型
          • 备选名称
          • 内存顺序参数
        • 5.2.2 `std::atomic_flag`
          • 初始化
          • 操作
          • 自旋锁实现
      • 5.2 原子操作和原子类型
        • 5.2.2 `std::atomic_flag`
          • 初始化
          • 操作
          • 自旋锁实现
        • 5.2.3 `std::atomic<bool>`
          • 初始化与赋值
          • 操作
          • “比较/交换”操作
          • 其他特性
        • 5.2.4 `std::atomic<T*>`
          • 初始化与赋值
          • 操作
          • 其他整型原子类型
      • 5.2 原子操作和原子类型
        • 5.2.5 标准原子整型的相关操作
          • 示例代码
        • 5.2.6 `std::atomic<>` 类模板
          • 示例代码
        • 5.2.7 原子操作的非成员函数
          • 常见的非成员函数
          • 内存顺序参数
          • 对 `std::shared_ptr<>` 的支持
          • 并行技术规范扩展

5.1 内存模型

内存模型在C++中分为两大部分:内存布局和并发。并发的基本结构非常重要,特别是低层原子操作。由于C++中的所有对象都与内存位置有关,我们将从基本结构开始讲解。


5.1.1 对象和内存位置

在C++程序中,数据都是由对象构成的。例如,可以创建一个 int 的衍生类,或者使用具有成员函数的基本类型,甚至像Smalltalk和Ruby那样——“一切都是对象”。对象是对C++数据构建块的声明,C++标准定义类对象为“存储区域”,但对象也可以将自己的特性赋予其他对象。

  • 基本类型:如 intfloat
  • 用户定义类的实例:如自定义的类。
  • 复杂对象:如数组、派生类的实例、具有非静态数据成员的类实例等。

无论是哪种类型,都会存储在一个或多个内存位置上。每个内存位置要么是一个标量类型的对象,要么是标量类型的子对象,例如 unsigned shortmy_class* 或序列中的相邻位域。当使用位域时需要注意:虽然相邻位域是不同的对象,但仍被视为相同的内存位置。

图5.1 分解一个 struct,展示不同对象的内存位置

在这里插入图片描述

struct MyStruct {
    int bf1 : 3;  // 位域bf1
    int bf2 : 5;  // 位域bf2
    int bf3 : 0;  // 位域bf3,宽度为0
    int bf4 : 7;  // 位域bf4
    std::string s;
};
  • 完整的 struct 是由多个子对象(每一个成员变量)组成的对象。
  • 位域 bf1bf2 共享同一个内存位置(假设 int 是4字节、32位类型)。
  • std::string 类型的对象 s 由内部多个内存位置组成。
  • 其他成员各自拥有自己的内存位置。
  • 宽度为0的位域 bf3 如何与 bf4 分离,并拥有各自的内存位置。

四个需要牢记的原则

  1. 每个变量都是对象,包括其成员变量的对象。
  2. 每个对象至少占有一个内存位置。
  3. 基本类型都有确定的内存位置(无论类型大小如何,即使它们是相邻的,或是数组的一部分)。
  4. 相邻位域是相同内存中的一部分。

你会奇怪,这些在并发中有什么作用?


5.1.2 对象、内存位置和并发

这部分对于C++的多线程编程至关重要。当两个线程访问不同的内存位置时,不会存在任何问题;当两个线程访问同一个内存位置时,就需要小心处理。

  • 只读访问:如果线程不更新数据,只读数据不需要保护或同步。
  • 写入访问:当线程对内存位置上的数据进行修改,就可能会产生条件竞争。

为了避免条件竞争,线程需要以一定的顺序执行。有两种主要方式:

  1. 使用互斥量:通过同一互斥量在两个线程同时访问前锁住,确保在同一时间内只有一个线程能够访问对应的内存位置。
  2. 使用原子操作:决定两个线程的访问顺序,当多个线程访问同一个内存地址时,对每个访问者都需要设定顺序。

如果不规定对同一内存地址访问的顺序,那么访问就不是原子的。当两个线程都是“写入者”时,就会产生数据竞争和未定义行为。

未定义的行为:是C++中的黑洞。一旦应用中有任何未定义的行为,就很难预料会发生什么事情。数据竞争绝对是一个严重的错误,要不惜一切代价避免它。

另一个重点是:当程序对同一内存地址中的数据访问存在竞争时,可以使用原子操作来避免未定义行为。当然,这不会影响竞争的产生——原子操作并没有指定访问顺序——而原子操作会把程序拉回到定义行为的区域内。


5.1.3 修改顺序

C++程序中的对象都有一个由程序中的所有线程对象在初始化开始阶段确定好的修改顺序。大多数情况下,这个顺序不同于执行中的顺序,但在给定的程序中,所有线程都需要遵守这个顺序。

  • 非原子类型:必须确保有足够的同步操作,以确保线程都遵守了修改顺序。当不同线程在不同序列中访问同一个值时,可能会遇到数据竞争或未定义行为。
  • 原子类型:编译器有责任去做同步。

因为当线程按修改顺序访问一个特殊的输入时,所以投机执行是不允许的。之后的读操作必须由线程返回新值,并且之后的写操作必须发生在修改顺序之后。虽然所有线程都需要遵守程序中每个独立对象的修改顺序,但没有必要遵守在独立对象上的操作顺序。

示例代码
#include <atomic>
#include <thread>

std::atomic<int> counter(0);

void increment_counter() {
    for (int i = 0; i < 1000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::thread t1(increment_counter);
    std::thread t2(increment_counter);

    t1.join();
    t2.join();

    std::cout << "Counter: " << counter.load() << std::endl;
}

在这个示例中,std::atomic<int> 确保了对 counter 的访问是线程安全的,并且通过 fetch_addload 方法保证了修改顺序。

注意:虽然 memory_order_relaxed 不提供顺序保证,但它确保了操作的原子性,从而避免了数据竞争。


了解了对象和内存地址的概念后,接下来我们来看什么是原子操作以及如何规定顺序。


以上内容展示了如何使用对象、内存位置和并发来管理多线程程序中的同步问题。希望这些示例和解释能帮助你更好地理解和应用这些同步机制。

以下是经过优化排版后的5.2节内容,详细解释了C++中的原子操作和原子类型。每个部分都有详细的注释和结构化展示。


5.2 原子操作和原子类型

5.2.1 标准原子类型

原子操作是指不可分割的操作,系统的所有线程中不可能观察到原子操作完成了一半。如果读取对象的加载操作是原子的,那么这个对象的所有修改操作也是原子的,因此加载操作得到的值要么是对象的初始值,要么是某次修改操作存入的值。

另一方面,非原子操作可能会被另一个线程观察到只完成一半。如果这个操作是一个存储操作,那么其他线程看到的值可能既不是存储前的值,也不是存储的值。如果非原子操作是一个读取操作,可能先取到对象的一部分,然后值被另一个线程修改,然后再取到剩余的部分,所以它取到的既不是第一个值,也不是第二个值。这就构成了数据竞争(见5.1节),出现未定义行为。

标准库中的原子类型

标准原子类型定义在头文件 <atomic> 中。这些类型的操作都是原子的,语言定义中只有这些类型的操作是原子的,也可以用互斥锁来模拟原子操作。

  • is_lock_free() 成员函数:几乎所有的原子类型都有一个 is_lock_free() 成员函数,可以让用户查询某个原子类型的操作是否直接使用了原子指令(x.is_lock_free() 返回 true),还是内部使用了一个锁结构(x.is_lock_free() 返回 false)。

  • 无锁状态宏:C++17 中,所有原子类型有一个静态常量成员变量 is_always_lock_free,如果相应硬件上的原子类型是无锁类型,则返回 true。例如:

    std::atomic<int> counter;
    if (counter.is_always_lock_free) {
        // 该平台上的 std::atomic<int> 是无锁的
    }
    
  • 宏定义:编译时对各种整型原子操作是否无锁进行判别,如 ATOMIC_BOOL_LOCK_FREE, ATOMIC_CHAR_LOCK_FREE 等。如果原子类型是无锁结构,值为 2;如果是基于锁的实现,值为 0;如果无锁状态在运行时才能确定,值为 1。

特殊的原子类型
  • std::atomic_flag:这是一个简单的布尔标志,并且在这种类型上的操作都是无锁的。初始化后,可以使用 test_and_set()clear() 成员函数进行查询和设置。

    std::atomic_flag f = ATOMIC_FLAG_INIT;
    f.clear(std::memory_order_release);  // 清除标志
    bool x = f.test_and_set();           // 设置标志并获取旧值
    
  • 其他原子类型:可以通过特化 std::atomic<> 得到更多功能,但不一定都是无锁的。主流平台上,原子变量是无锁的内置类型(如 std::atomic<int>std::atomic<void*>)。

备选名称

为了历史兼容性,标准库提供了备选名称,如 atomic_bool 对应 std::atomic<bool>,具体见表5.1。

原子类型相关特化类
atomic_boolstd::atomic
atomic_charstd::atomic
atomic_scharstd::atomic
atomic_ucharstd::atomic
atomic_intstd::atomic
atomic_uintstd::atomic
内存顺序参数

每种原子类型的操作都有一个内存序参数,用于指定存储的顺序。常见的内存顺序选项包括:

  • Store 操作memory_order_relaxed, memory_order_release, memory_order_seq_cst
  • Load 操作memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_seq_cst
  • Read-modify-write 操作memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst

默认的内存序是 memory_order_seq_cst


5.2.2 std::atomic_flag

std::atomic_flag 是最简单的原子类型,只能在两个状态间切换:设置和清除。它作为构建块存在,通常用于实现自旋锁等简单同步机制。

初始化

std::atomic_flag 类型的对象必须使用 ATOMIC_FLAG_INIT 进行初始化:

std::atomic_flag f = ATOMIC_FLAG_INIT;
操作
  • clear() 成员函数:清除标志,使用释放语义。
  • test_and_set() 成员函数:设置标志并检索旧值,可以指定内存顺序。

示例代码:

f.clear(std::memory_order_release);  // 使用释放语义清除标志
bool x = f.test_and_set();           // 设置标志并获取旧值,默认内存序为 memory_order_seq_cst
自旋锁实现

std::atomic_flag 非常适合实现自旋锁。以下是一个简单的自旋锁实现:

class spinlock_mutex {
    std::atomic_flag flag;
public:
    spinlock_mutex():
        flag(ATOMIC_FLAG_INIT)
    {}

    void lock() {
        while(flag.test_and_set(std::memory_order_acquire));  // 等待直到获取锁
    }

    void unlock() {
        flag.clear(std::memory_order_release);  // 释放锁
    }
};

在这个例子中,spinlock_mutex 类使用 std::atomic_flag 来实现一个简单的自旋锁。lock() 方法会不断循环调用 test_and_set(),直到成功获取锁为止。unlock() 方法则通过调用 clear() 来释放锁。

由于 std::atomic_flag 的局限性,实际操作中最好使用 std::atomic<bool>,它提供了更多的功能和灵活性。


以上内容展示了如何使用原子操作和原子类型来管理多线程程序中的同步问题。希望这些示例和解释能帮助你更好地理解和应用这些同步机制。

以下是经过优化排版后的5.2.2至5.2.4节内容,详细解释了C++中的 std::atomic_flagstd::atomic<bool>std::atomic<T*> 的使用方法和特性。每个部分都有详细的注释和结构化展示。


5.2 原子操作和原子类型

5.2.2 std::atomic_flag

std::atomic_flag 是最简单的原子类型,用于在两个状态间切换:设置和清除。它通常作为构建块存在,适用于一些特别的情况。

初始化

std::atomic_flag 类型的对象必须通过 ATOMIC_FLAG_INIT 进行初始化:

std::atomic_flag f = ATOMIC_FLAG_INIT;
  • 初始化:标志总是初始化为“清除”状态。
  • 静态存储:如果 std::atomic_flag 是静态存储的,则保证其是静态初始化的,避免初始化顺序问题。
操作

初始化后,可以进行以下操作:

  • 销毁
  • 清除:使用 clear() 成员函数,并指定内存顺序。
  • 设置(查询之前的值):使用 test_and_set() 成员函数,并指定内存顺序。

示例代码:

f.clear(std::memory_order_release);  // 使用释放语义清除标志
bool x = f.test_and_set();           // 设置标志并获取旧值,默认内存序为 memory_order_seq_cst
  • clear():是一个存储操作,不能有 memory_order_acquirememory_order_acq_rel 语义。
  • test_and_set():是一个“读-改-写”操作,可以应用于任何内存顺序。
自旋锁实现

std::atomic_flag 非常适合实现自旋锁。以下是一个简单的自旋锁实现:

class spinlock_mutex {
    std::atomic_flag flag;
public:
    spinlock_mutex():
        flag(ATOMIC_FLAG_INIT)
    {}

    void lock() {
        while(flag.test_and_set(std::memory_order_acquire));  // 等待直到获取锁
    }

    void unlock() {
        flag.clear(std::memory_order_release);  // 释放锁
    }
};

在这个例子中,spinlock_mutex 类使用 std::atomic_flag 来实现一个简单的自旋锁。lock() 方法会不断循环调用 test_and_set(),直到成功获取锁为止。unlock() 方法则通过调用 clear() 来释放锁。

由于 std::atomic_flag 的局限性,实际操作中最好使用 std::atomic<bool>,它提供了更多的功能和灵活性。


5.2.3 std::atomic<bool>

std::atomic<bool> 是最基本的原子布尔类型,具有比 std::atomic_flag 更多的功能。

初始化与赋值

虽然不能拷贝构造和拷贝赋值,但可以通过非原子的 bool 类型进行构造和赋值:

std::atomic<bool> b(true);
b = false;
操作
  • store():写入 truefalse,类似于 std::atomic_flag 中的 clear()
  • test_and_set():可以替换为更通用的 exchange(),允许使用新值替换已存储的值,并检索原始值。
  • load():加载当前值。
  • compare_exchange_weak()compare_exchange_strong():比较当前值与期望值,当两值相等时存储新值;否则更新期望值为当前值。

示例代码:

std::atomic<bool> b;
bool x = b.load(std::memory_order_acquire);  // 加载当前值
b.store(true);                               // 存储 true
x = b.exchange(false, std::memory_order_acq_rel);  // 交换值并返回原始值
“比较/交换”操作
  • compare_exchange_weak():可能会伪失败,尤其是在缺少单条 CAS 操作的机器上。
  • compare_exchange_strong():保证不会伪失败,但在某些情况下可能需要额外的开销。

示例代码:

bool expected = false;
extern std::atomic<bool> b;  // 假设已经初始化
while (!b.compare_exchange_weak(expected, true) && !expected);
  • 内存顺序参数:可以在成功和失败的情况下分别指定不同的内存顺序。默认情况下,所有操作都使用 memory_order_seq_cst
其他特性
  • is_lock_free():检查操作是否无锁。这是除了 std::atomic_flag 外所有原子类型共有的特征。

5.2.4 std::atomic<T*>

std::atomic<T*> 是特化的原子指针类型,支持对指针的操作。

初始化与赋值

虽然不能拷贝构造和拷贝赋值,但可以通过合适的类型指针进行构造和赋值:

std::atomic<Foo*> p(some_array);  // some_array 是 Foo 类型的数组
操作
  • load():加载当前值。
  • store():存储新值。
  • exchange():交换值。
  • compare_exchange_weak()compare_exchange_strong():比较当前值与期望值,当两值相等时存储新值;否则更新期望值为当前值。
  • fetch_add()fetch_sub():在存储地址上做原子加法和减法,提供简易的封装。

示例代码:

class Foo {};
Foo some_array[5];
std::atomic<Foo*> p(some_array);

Foo* x = p.fetch_add(2);  // p 加 2,并返回原始值
assert(x == some_array);
assert(p.load() == &some_array[2]);

x = (p -= 1);  // p 减 1,并返回原始值
assert(x == &some_array[1]);
assert(p.load() == &some_array[1]);

p.fetch_add(3, std::memory_order_release);  // 使用释放语义增加指针
  • 内存顺序参数fetch_add()fetch_sub() 是“读-改-写”操作,可以使用任意的内存顺序。
其他整型原子类型

剩下的原子类型基本上都是整型原子类型,并且拥有类似的接口(除了内置类型不同)。例如:

  • std::atomic<int>
  • std::atomic<unsigned int>
  • std::atomic<long>

这些类型的接口和操作方式与 std::atomic<bool>std::atomic<T*> 类似。


以上内容展示了如何使用 std::atomic_flagstd::atomic<bool>std::atomic<T*> 来管理多线程程序中的同步问题。希望这些示例和解释能帮助你更好地理解和应用这些同步机制。

以下是经过优化排版后的5.2.5至5.2.7节内容,详细解释了C++中的标准原子整型操作、std::atomic<> 类模板以及原子操作的非成员函数。每个部分都有详细的注释和结构化展示。


5.2 原子操作和原子类型

5.2.5 标准原子整型的相关操作

除了基本的操作集合(如 load()store()exchange()compare_exchange_weak()compare_exchange_strong()),标准原子整型(如 std::atomic<int>std::atomic<unsigned long long>)还提供了一套完整的操作:

  • fetch_add():原子加法操作,并返回旧值。
  • fetch_sub():原子减法操作,并返回旧值。
  • fetch_and():按位与操作,并返回旧值。
  • fetch_or():按位或操作,并返回旧值。
  • fetch_xor():按位异或操作,并返回旧值。

此外,还支持复合赋值方式(如 +=, -=, &=, |=, ^=)以及前缀和后缀的自增和自减操作(如 ++x, x++, --x, x--)。

示例代码
std::atomic<int> counter(0);

counter.fetch_add(1);          // 原子加法操作
counter.fetch_sub(1);          // 原子减法操作
counter.fetch_and(0xFF);       // 按位与操作
counter.fetch_or(0x100);       // 按位或操作
counter.fetch_xor(0x80);       // 按位异或操作

counter += 5;                  // 复合赋值操作
++counter;                     // 前缀自增
counter++;                     // 后缀自增

这些操作通常用于计数器或掩码等场景。如果需要更复杂的操作(如除法、乘法或移位操作),可以使用 compare_exchange_weak() 或其他同步机制来实现。


5.2.6 std::atomic<> 类模板

std::atomic<> 类模板允许用户定义自己的原子类型,但有一些限制条件:

  • 拷贝赋值运算符:类型必须有编译器生成的拷贝赋值运算符,不能有任何虚函数或虚基类。
  • 所有基类和非静态数据成员:也必须支持拷贝赋值操作。
  • 比较操作:比较-交换操作类似于 memcmp,而不是为用户定义类型(UDT)定义的比较操作符。
示例代码
struct MyType {
    int x;
    double y;
};

std::atomic<MyType> atomic_var(MyType{1, 2.0});

// 使用 load() 和 store()
MyType old_val = atomic_var.load();
MyType new_val{3, 4.0};
atomic_var.store(new_val);

// 使用 exchange()
MyType exchanged_val = atomic_var.exchange(MyType{5, 6.0});

// 使用 compare_exchange_weak()
MyType expected = atomic_var.load();
MyType desired{7, 8.0};
bool success = atomic_var.compare_exchange_weak(expected, desired);

对于大多数平台,当 UDT 的大小等于或小于一个 intvoid* 类型时,std::atomic<UDT> 会使用原子指令。某些平台可能支持双字节比较和交换(DWCAS)指令,适用于两倍于 intvoid* 大小的类型。

需要注意的是,复杂的数据结构(如 std::vector<int>)不适合用作原子类型,因为它们包含多个操作,而不仅仅是赋值和比较。在这种情况下,最好使用 std::mutex 来保护数据。


在这里插入图片描述

5.2.7 原子操作的非成员函数

除了成员函数外,C++ 标准库还提供了非成员函数来操作原子类型。这些非成员函数通常以 atomic_ 作为前缀,并且可以重载不同的原子类型。

常见的非成员函数
  • std::atomic_load:加载原子变量的值。
  • std::atomic_store:存储新值到原子变量。
  • std::atomic_exchange:交换原子变量的值,并返回旧值。
  • std::atomic_compare_exchange_weakstd::atomic_compare_exchange_strong:比较并交换值。

示例代码:

std::atomic<int> a(10);

// 成员函数形式
int val1 = a.load();                // 加载值
a.store(20);                        // 存储新值
int val2 = a.exchange(30);          // 交换值并返回旧值
bool success = a.compare_exchange_weak(val1, 40);  // 比较并交换值

// 非成员函数形式
int val3 = std::atomic_load(&a);    // 加载值
std::atomic_store(&a, 50);          // 存储新值
int val4 = std::atomic_exchange(&a, 60);  // 交换值并返回旧值
success = std::atomic_compare_exchange_weak(&a, &val3, 70);  // 比较并交换值
内存顺序参数

非成员函数可以通过 _explicit 后缀指定内存顺序参数。例如:

std::atomic_store_explicit(&a, 80, std::memory_order_release);
bool success_explicit = std::atomic_compare_exchange_weak_explicit(
    &a, &val3, 90, std::memory_order_acquire, std::memory_order_relaxed);
std::shared_ptr<> 的支持

C++ 标准库还为 std::shared_ptr<> 提供了原子操作的非成员函数:

std::shared_ptr<my_data> p;
void process_global_data() {
    std::shared_ptr<my_data> local = std::atomic_load(&p);
    process_data(local);
}

void update_global_data() {
    std::shared_ptr<my_data> local(new my_data);
    std::atomic_store(&p, local);
}

这些函数打破了“只有原子类型才能提供原子操作”的原则,使得 std::shared_ptr<> 也能进行原子操作。

并行技术规范扩展

并行技术规范扩展提供了一种原子类型 std::experimental::atomic_shared_ptr<T>,声明在 <experimental/atomic> 头文件中。它支持无锁实现,并提供了 loadstoreexchangecompare-exchange 等操作。

示例代码:

#include <experimental/atomic>

std::experimental::atomic_shared_ptr<my_data> atomic_p;

void process_global_data() {
    std::shared_ptr<my_data> local = atomic_p.load();
    process_data(local);
}

void update_global_data() {
    std::shared_ptr<my_data> local(new my_data);
    atomic_p.store(local);
}

通过 is_lock_free() 函数可以确定在对应的硬件平台上是否无锁。


以上内容展示了如何使用标准原子整型操作、std::atomic<> 类模板以及原子操作的非成员函数来管理多线程程序中的同步问题。希望这些示例和解释能帮助你更好地理解和应用这些同步机制。


http://www.kler.cn/a/526032.html

相关文章:

  • [EAI-027] RDT-1B,目前最大的用于机器人双臂操作的机器人基础模型
  • Ubuntu安装VMware17
  • Swing使用MVC模型架构
  • Ubuntu全面卸载mysql
  • 步进电机加减速公式推导
  • spring中解决循环依赖的方法
  • CMAKE工程编译好后自动把可执行文件传输到远程开发板
  • 深度学习探索:ChatGPT数据分析精髓 梯度下降优化方法深度剖析
  • 微软官方工具箱 PowerToys,提升工作效率
  • Conditional DETR for Fast Training Convergence论文学习
  • 负荷预测算法模型
  • 新月智能护甲系统CMIA--未来战场的守护者
  • 从巫师求雨说起
  • 如何使用formlinker,重构微软表单创建的数字生产力法则?
  • 提供算法模型管理、摄像头管理、告警管理、数据统计等功能的智慧园区开源了
  • MySQL(高级特性篇) 14 章——MySQL事务日志
  • 【memgpt】letta 课程4:基于latta框架构建MemGpt代理并与之交互
  • 如何把一个python文件打包成一步一步安装的可执行程序
  • 基于Android Studio开发平台使用CNN和LSTM的人工智能家居监控系统
  • 物业管理软件引领社区智能化转型提升服务效率与居民生活质量
  • 网站快速收录:利用新闻源的优势
  • 【外文原版书阅读】《机器学习前置知识》2.用看电影推荐的例子带你深入了解向量点积在机器学习的作用
  • AI大模型开发原理篇-8:Transformer模型
  • 走近科学IT版:Windows里Ollama关闭后还在占用11434端口
  • 2.文件IO
  • 国产SiC碳化硅功率器件技术成为服务器电源升级的核心引擎