Cpp Concurrency In Action(读书笔记4)——C++内存模型和原子类型操作

10 篇文章 0 订阅
9 篇文章 0 订阅

内存模型基础

  • 基本结构,这个结构奠定了与内存相关的基础
  • 并发

对象和内存位置

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

对象、内存位置和并发

       所有东西都在内存中。为了避免条件竞争,两个线程就需要一定的执行顺序。第一种方式,使用互斥量来确定访问的顺序;当同一互斥量在两个线程同时访问前被锁住,那么在同一时间内就只有一个线程能够访问到对应的内存位置,所以后一个访问必须在前一个访问之后。另一种方式是使用原子操作(atmic operations)同步机制,决定两个线程的访问顺序。当多于两个线程访问同一个内存地址时,对每个访问这都需要定义一个顺序。
    当程序中的对同一内存地址中的数据访问存在竞争,你可以使用原子操作来避免未定义行为。当然,这不会影响竞争的产生——原子操作并没有指定访问顺序——但原子操作把程序拉回了定义行为的区域内。

修改顺序 

        每一个在C++程序中的对象,都有(由程序中的所有线程对象)确定好的修改顺序(modification order),在初始化开始阶段确定。在大多数情况下,这个顺序不同于执行中的顺序,但是在给定的执行程序中,所有线程都需要遵守这顺序。如果对象不是一个原子类型,你必要确保有足够的同步操作,来确定每个线程都遵守了变量的修改顺序。当不同线程在不同序列中访问同一个值时,你可能就会遇到数据竞争或未定义行为。如果你使用原子操作,编译器就有责任去替你做必要的同步。
        这一要求意味着:投机执行是不允许的,因为当线程按修改顺序访问一个特殊的输入,之后的读操作,必须由线程返回较新的值,并且之后的写操作必须发生在修改顺序之后。同样的,在同一线程上允许读取对象的操作,要不返回一个已写入的值,要不在对象的修改顺序(也就是在读取后)再写入另一个值。虽然,所有线程都需要遵守程序中每个独立对象的修改顺序,但它们没有必要遵守在独立对象上的相对操作顺序。

C++中的原子操作和原子类型  

      原子操作是一类不可分割的操作,当这样操作在任意线程中进行一半的时候,你是不能查看的;它的状态要不就是完成,要不就是未完成。如果从对象中读取一个值的操作是原子的,并且对对象的所有修改也都是原子的话,那么加载操作要不就会检索对象初始化的值,要不就将值存在某一次修改中。另一方面,非原子操作可能会被视为由一个线程完成一半的操作。如果这种是一个存储操作,那么其他线程看到的,可能既不是存储前的值,也可能不是已存储的值。如果非原子操作是一个加载操作,那么它可能会去检索对象的部分成员,或是在另一个线程修改了对象的值后,对对象进行检索;所以,检索出来的值可能既不是第一个值,也不是第二个值,可能是某种两者结合的值。这就是一个简单的条件竞争(如第3章所描述),但是这种级别的竞争会构成数据竞争,且会伴有有未定义行为。  

标准原子类型 

        C++中(大多数情况下)你需要一个原子类型去执行一个原子操作。
        标准原子类型(atomic types)所有在这种类型上的操作都是原子的,虽然可以使用互斥量去达到原子操作的效果,但只有在这些类型上的操作是原子的(语言明确定义)。实际上,标准原子类型都很相似:它们大多数都有一个is_lock_free()成员函数,这个函数允许用户决定是否直接对一个给定类型使用原子指令(x.is_lock_free()返回true),或对编译器和运行库使用内部锁x.is_lock_free()返回false)。
         只用  std :: atomic_flag   类型不提供 is_lock_free()成员函数。 这种类型上的操作都需要是无锁的 ( lock-free )。 在  std :: atomic_flag 对象明确初始化后,做查询和设置 (使用 test_and_set()
成员函数),或清除(使用clear()成员函数)都很容易。这就是:无赋值,无拷贝,没有测试和清 除,没有其他任何操作。
         std :: atomic_flag   模板,使用对应的 T类型去特化模板的方式,要好于使用别名的方式。

std::atomic_flag的相关操作  

        最简单的标准原子类型,它表示了一个布尔标志。这个类型的对象可以 在两个状态间切换:设置和清除。它就是那么的简单,只作为一个构建块存在。我从未期待 这个类型被使用,除非在十分特别的情况下。
        使用 std::atomic_flag实现自旋互斥锁
    
    
  1. #include <atomic>
  2. class spinlock_mutex
  3. {
  4. std::atomic_flag flag;// = ATOMIC_FLAG_INIT;
  5. public:
  6. //初始化标志是“清除”,并且互斥量处于解锁状态。
  7. //std::atomic_flag 类型的对象必须被ATOMIC_FLAG_INIT初始化,总是初始化为清除。
  8. spinlock_mutex()
  9. :flag{ ATOMIC_FLAG_INIT } //此处使用新标准统一的初始化方式,否则就是调用复制构造函数(已删除)
  10. {}
  11. void lock()
  12. {
  13. while (flag.test_and_set(std::memory_order_acquire));
  14. //循环运行test_and_set(),直到旧值为false,当前线程设置为true
  15. }
  16. void unlock()
  17. {
  18. flag.clear(std::memory_order_release);
  19. }
  20. };
        spinlock_mutex使用测试:
    
    
  1. #include <atomic>
  2. #include <iostream>
  3. #include <thread>
  4. #include <mutex>
  5. #include <vector>
  6. class spinlock_mutex;//使用上面的定义
  7. struct test {
  8. spinlock_mutex m;
  9. int i;
  10. };
  11. void fun(test &t)
  12. {
  13. std::lock_guard<spinlock_mutex> lk(t.m);
  14. ++t.i;
  15. std::cout << "thread : fun # " << t.i << std::endl;
  16. }
  17. int main()
  18. {
  19. test t;
  20. std::lock_guard<spinlock_mutex> lk(t.m);
  21. t.i = 0;
  22. lk.~lock_guard();
  23. std::vector<std::thread> vtds;
  24. for (int i = 0;i < 10;++i)
  25. {
  26. vtds.push_back(std::thread{ fun,std::ref(t) });
  27. }
  28. for (auto & th : vtds)
  29. {
  30. th.join();
  31. }
  32. system("pause");
  33. return 0;
  34. }
        由于 std :: atomic_flag 局限性太强,因为它没有非修改查询操作,它甚至不能像普通的布尔 标志那样使用。所以,你最好使用 std :: atomic  。

std::atomic的相关操作 

         std :: atomic  : 最基本的原子整型类型就。 它有着比 std :: atomic_flag 更加 齐全的布尔标志特性。虽然它依旧不能拷贝构造和拷贝赋值,但是你可以使用一个非原子的 bool类型构造它,所以它可以被初始化为 rue或 false,并且你也可以从一个非原子 bool变量赋 值给 std :: atomic   的实例。
     
     
  1. std::atomic<bool> f(true);
  2. f = false;
        store()是一个存储操作,而load()是一个加载操作。exchange()是一个“读-改-写”操作:
      
      
  1. std::atomic<bool> b;
  2. bool x = b.load(std::memory_order_acquire);
  3. b.store(true);
  4. x = b.exchange(false, std::memory_order_acq_rel);//x:true;b:false
  5. /*std::memory_order_acq_rel:Reads as an acquire operation
  6. *and writes as a release operation (as described above).
  7. */
        std :: atomic <bool> 提供的exchange(),不仅仅是一个“读-改-写”的操作;它还介绍了一种新的 存储方式:当当前值与预期值一致时,存储新值的操作。
        存储一个新值或旧值取决于当前值:
        这是一种新型操作,叫做“比较交换”,它的形式表现为compare_exchange_weak()(会发生 伪失败”( spurious failure )) compare_exchange_strong() 成员函数。 比较-交换 操作是原子类型编程的基石;它比较原子 变量的当前值和提供的预期值,当两值相等时,存储预期值。当两值不等,预期值就会被更 新为原子变量中的值。 比较 交换 函数值是一个 bool 变量,当返回 true 时执行存储操作,当 false则更新期望值。(此处不是很懂)
        std :: atomic 和  std :: atomic_flag 的不同之处在于, std :: atomic 不是无锁的; 为了保证操作的原子性,其实现中需要一个内置的互斥量。当处于特殊情况时,你可以使用 is_lock_free()成员函数,去检查 std :: atomic 上的操作是否无锁。这是另一个,除 了  std :: atomic_flag 之外,所有原子类型都拥有的特征。

std::atomic指针运算  

         fetch_add() 和fetch_sub() 的返回值与算数符号运算的返回值略有不同( 如果 x是 std :: atomic < Foo *> 类型的数组的首地址, x.ftech_add(3) 指向第四个元素,并且函 数返回指向第一个元素的地址) 。这种操作也被称为 交换-相加” ,并且这是一个原子的“ 读- 改- 写”操作,如同exchange()和compare_exchange_weak()/compare_exchange_strong()一样。 正像其他操作那样,返回值是一个普通的  T* 值,而非是std::atomic<Foo*> 对象的引用,所以 调用代码可以基于之前的值进行操作:
        
        
  1. #include <atomic>
  2. #include <cassert>
  3. class Foo {};
  4. int main()
  5. {
  6. Foo some_array[5];
  7. std::atomic<Foo*> p(some_array);
  8. Foo* x = p.fetch_add(2); // p加2,并返回原始值
  9. assert(x == some_array);
  10. assert(p.load() == &some_array[2]);
  11. x = (p -= 1); // p减1,并返回p值
  12. assert(x == &some_array[1]);
  13. assert(p.load() == &some_array[1]);
  14. system("pause");
  15. return 0;
  16. }
       两个 函数也允许内存顺序语义( http://www.cplusplus.com/reference/atomic/atomic_flag/test_and_set/作为给定函数的参数:   
        
        
  1. p.fetch_add(3,std::memory_order_release);

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

        std :: atomic <int> 和  std :: atomic <unsigned long long > fetch_add(), fetch_sub()(返回旧值,非引用), fetch_and(), fetch_or(), fetch_xor() ,还有复合赋值方式 ((+=, -=(返回新值 ,非引用 ), &=, |= ^=) ,以及 ++ --(++x, x++, --x x--) 只有除法、乘法 和移位操作不在其中。因为,整型原子值通常用来作计数器,或者是掩码,所以以上操作的 缺失显得不是那么重要;如果需要,额外的操作可以将 compare_exchange_weak() 放入循环 中完成。

std::atomic<>主要类的模板

        主模板的存在,在除了标准原子类型之外,允许用户使用自定义类型创建一个原子变量。
        std :: atomic < UDT > 使用要求 (UDT 是用户定义类型, 保证“比较交换”操作能正常的工作 )
  • 必须有拷贝赋值运算符 。即不能有任何虚函数或虚基类,以及必须使用编译器创建的拷贝赋值操作 。可以使用memcpy()进行拷贝。
  • 必须是“位可比的”(bitwise equality comparable) 。可以使用memcmp()对位进行比较。
        编译器可以对 std :: atomic   直接使用原 子指令(因此实例化一个特殊无锁结构)。
        双字节比较和交 换”( double-word-compare-and-swap DWCAS ) 指令: 如果你的 UDT类型的大小如同 (或小于 )一个 int或  void*  类型时,大多数平台将会 std :: atomic < UDT >   使用原子指令,有些平台可能会对用户自定义类型 (两倍于int或 void* 大小)特化的 std :: atomic < > 使用原子指令。
       限制:
  • 浮点数的特化,存在相等却表达式不同的情况。
  • UDT定义了与memcmp()不同的比较操作。
  • 不能使用包含有计数器,标志指针和简单数组的类型,作为特化类型。  

原子操作的释放函数 

       在不同的原子类型中 也有等价的非成员函数存在。大多数非成员函数的命名与对应成员函数有关,但是需 “atomic_”作为前缀( 比如,  std::atomic_load() ) 。这些函数都会被不同的原子类型所重载。 在指定一个内存序列标签时,他们会分成两种:一种没有标签,另一种将“_explicit”作为后 ,并且需要一个额外的参数,或将内存顺序作为标签,亦或只有标签 如,  std::atomic_store(&atomic_var,new_value) 与  std::atomic_store_explicit(&atomic_var,ne w_value,std::memory_order_release  )。不过,原子对象被成员函数隐式引用,所有释放函数都 持有一个指向原子对象的指针 作为第一个参数
        std :: atomic_flag的操作: std :: atomic_flag_test_and_set () std :: atomic_flag_test_and_set_explicit ()
        C++标准库也对在一个原子类型中的 std::shared_ptr<> 智能指针类型提供释放函数。这打破 了“ 只有原子类型,才能提供原子操作” 的原则 这里  std::shared_ptr<>  肯定不是原子类型。 但是,C++标准委员会感觉对此提供额外的函数是很重要的。可使用的原子操作有:load, store, exchange 和compare/exchange ,这些操作重载了标准原子类型的操作,并且获取一 个  std::shared_ptr<>*  作为第一个参数:
     
     
  1. std::shared_ptr<my_data> p;
  2. void process_global_data()
  3. {
  4. std::shared_ptr<my_data> local = std::atomic_load(&p);
  5. process_data(local);
  6. }
  7. void update_global_data()
  8. {
  9. std::shared_ptr<my_data> local(new my_data);
  10. std::atomic_store(&p, local);
  11. }

同步操作和强制排序  

        不同线程对数据的读写:  
     
     
  1. #include <atomic>
  2. #include <vector>
  3. #include <iostream>
  4. #include <chrono>
  5. #include <thread>
  6. std::vector<int> data;
  7. std::atomic<bool> data_ready(false);
  8. void reader_thread()
  9. {
  10. while (!data_ready.load()) // 1
  11. {
  12. std::this_thread::sleep_for(std::chrono::milliseconds(5));
  13. }
  14. std::cout << "The answer=" << data[0] << std::endl; // 2
  15. }
  16. void writer_thread()
  17. {
  18. data.push_back(42); // 3
  19. data_ready = true; // 4
  20. }
  21. int main()
  22. {
  23. std::thread t_reader{ reader_thread }, t_writer{ writer_thread };
  24. t_reader.detach();
  25. t_writer.detach();
  26. system("pause");
  27. return 0;
  28. }
        强制访问顺序是由对 std :: atomic <bool> 类型的data_ready变量进行操作完成的; 这些操作通 过“ 先行发生 ”( happens-before )和“ 同步发生 ”( synchronizes-with )确定必要的顺序。3先于4,1先于2,读写同步,于是强制顺序:写先于读。

同步发生 

        同步发生” 关系是指:只能在原子类型之间进行的操作。 :在变量 x进行适当标记的原子写操作 W,同步与对 进行适当标记的 原子读操作,读取的是 W操作写入的内容;或是在 W之后,同一线程上的原子写操作对 写入 的值;亦或是任意线程对 的一系列原子读 -写操作 (例如, fetch_add()或 compare_exchange_weak())。

先行发生  

        先行发生” 关系是一个程序中,基本构建块的操作顺序;它指定了某个操作去影响另一个操 作。
        对于参数中的函数调用顺序是未指定顺序的(结果一般为2,1,与环境相关): 
     
     
  1. void foo(int a, int b)
  2. {
  3. std::cout << a << " , " << b << std::endl;
  4. }
  5. int get_num()
  6. {
  7. static int i = 0;
  8. return ++i;
  9. }
  10. int main()
  11. {
  12. foo(get_num(), get_num()); // 无序调用get_num()
  13. system("pause");
  14. return 0;
  15. }

原子操作的内存顺序

        三种内存模型(内存序列): 排序一致序列 ( sequentially consistent ),获取 -释放序列 ( memory_order_consume, memory_order_acquire, memory_order_release memory_order_acq_rel ),和自由序列( memory_order_relaxed )。
         排序一致队列: 程序中的行为从任意角度 去看,序列顺序都保持一致。
         全序——序列一致:
       
       
  1. #include <atomic>
  2. #include <thread>
  3. #include <cassert>
  4. std::atomic<bool> x, y;
  5. std::atomic<int> z;
  6. void write_x()
  7. {
  8. x.store(true, std::memory_order_seq_cst); // 1
  9. }
  10. void write_y()
  11. {
  12. y.store(true, std::memory_order_seq_cst); // 2
  13. }
  14. void read_x_then_y()
  15. {
  16. while (!x.load(std::memory_order_seq_cst));
  17. if (y.load(std::memory_order_seq_cst)) // 3
  18. ++z;
  19. }
  20. void read_y_then_x()
  21. {
  22. while (!y.load(std::memory_order_seq_cst));
  23. if (x.load(std::memory_order_seq_cst)) // 4
  24. ++z;
  25. }
  26. int main()
  27. {
  28. x = false;
  29. y = false;
  30. z = 0;
  31. std::thread a(write_x);
  32. std::thread b(write_y);
  33. std::thread c(read_x_then_y);
  34. std::thread d(read_y_then_x);
  35. a.join();
  36. b.join();
  37. c.join();
  38. d.join();
  39. assert(z.load() != 0); // 5:可能是1、2,所以永远都不会触发
  40. system("pause");
  41. return 0;
  42. }
        序列一致是最简单、直观的序列,但是他也是最昂贵的内存序列,因为它需要对所有线程进 行全局同步。在一个多处理系统上,这就需要处理期间进行大量并且费时的信息交换。
        非排序一致内存模型: 线程没必要去保证一致性。 在没 有明确的顺序限制下,唯一的要求就是,所有线程都要统一对每一个独立变量的修改顺序 对不同变量的操作可以体现在不同线程的不同序列上,提供的值要与任意附加顺序限制保持 一致。 踏出排序一致世界后,最好的示范就是使用 memory_order_relaxed对所有操作进行约束。
        自由序列: 在原子类型上的操作以自由序列执行,没有任何同步关系。在同一线程中对于同一变量的操 作还是服从先发执行的关系,但是这里不同线程几乎不需要相对的顺序。唯一的要求是, 访问同一线程中的单个原子变量不能重排序;当一个给定线程已经看到一个原子变量的特定 值,线程随后的读操作就不会去检索变量较早的那个值。当使用 memory_order_relaxed,就 不需要任何额外的同步,对于每个变量的修改顺序只是线程间共享的事情。
        非限制操作只有非常少的顺序要求(relaxed——任意)
      
      
  1. #include <atomic>
  2. #include <thread>
  3. #include <cassert>
  4. std::atomic<bool> x, y;
  5. std::atomic<int> z;
  6. void write_x_then_y()
  7. {
  8. x.store(true, std::memory_order_relaxed); // 1
  9. y.store(true, std::memory_order_relaxed); // 2
  10. }
  11. void read_y_then_x()
  12. {
  13. while (!y.load(std::memory_order_relaxed)); // 3
  14. if (x.load(std::memory_order_relaxed)) // 4
  15. ++z;
  16. }
  17. int main()
  18. {
  19. x = false;
  20. y = false;
  21. z = 0;
  22. std::thread a(write_x_then_y);
  23. std::thread b(read_y_then_x);
  24. a.join();
  25. b.join();
  26. assert(z.load() != 0); // 5,可能会触发
  27. system("pause");
  28. return 0;
  29. }
        书上说可能会触发(虽然我循环跑了一次,没有触发,待研究,要与之后的计数员的笔记一块阅读),这里的关键点: x和 y是两个不同的变量,所以这里没有顺序 去保证每个操作产生相关值的可见性。
         非限制操作 ——多线程版:
       
       
  1. #include <thread>
  2. #include <atomic>
  3. #include <iostream>
  4. std::atomic<int> x(0), y(0), z(0); // 1
  5. std::atomic<bool> go(false); // 2
  6. unsigned const loop_count = 10;
  7. struct read_values
  8. {
  9. int x, y, z;
  10. };
  11. read_values values1[loop_count];
  12. read_values values2[loop_count];
  13. read_values values3[loop_count];
  14. read_values values4[loop_count];
  15. read_values values5[loop_count];
  16. void increment(std::atomic<int>* var_to_inc, read_values* values)
  17. {
  18. while (!go)
  19. std::this_thread::yield(); // 3 自旋,等待信号
  20. for (unsigned i = 0;i<loop_count;++i)
  21. {
  22. values[i].x = x.load(std::memory_order_relaxed);
  23. values[i].y = y.load(std::memory_order_relaxed);
  24. values[i].z = z.load(std::memory_order_relaxed);
  25. var_to_inc->store(i + 1, std::memory_order_relaxed); // 4
  26. std::this_thread::yield();
  27. /*Provides a hint to the implementation to
  28. reschedule the execution of threads,
  29. allowing other threads to run*/
  30. }
  31. }
  32. void read_vals(read_values* values)
  33. {
  34. while (!go)
  35. std::this_thread::yield(); // 5 自旋,等待信号
  36. for (unsigned i = 0;i<loop_count;++i)
  37. {
  38. values[i].x = x.load(std::memory_order_relaxed);
  39. values[i].y = y.load(std::memory_order_relaxed);
  40. values[i].z = z.load(std::memory_order_relaxed);
  41. std::this_thread::yield();
  42. }
  43. }
  44. void print(read_values* v)
  45. {
  46. for (unsigned i = 0;i<loop_count;++i)
  47. {
  48. if (i)
  49. std::cout << ",";
  50. std::cout << "(" << v[i].x << "," << v[i].y << "," << v[i].z << ")";
  51. }
  52. std::cout << std::endl;
  53. }
  54. int main()
  55. {
  56. std::thread t1(increment, &x, values1);
  57. std::thread t2(increment, &y, values2);
  58. std::thread t3(increment, &z, values3);
  59. std::thread t4(read_vals, values4);
  60. std::thread t5(read_vals, values5);
  61. go = true; // 6 开始执行主循环的信号
  62. t5.join();
  63. t4.join();
  64. t3.join();
  65. t2.join();
  66. t1.join();
  67. print(values1); // 7 打印最终结果
  68. print(values2);
  69. print(values3);
  70. print(values4);
  71. print(values5);
  72. system("pause");
  73. return 0;
  74. }
         强烈建议避免自由的原子操作,除非 它们是硬性要求的,并且在使用它们的时候需要十二分的谨慎。上面程序结果多种。由于给出的不直观的结果,复杂性增强。
         获取 - 释放序列: 原子加载就是“获取”(acquire)操作 (memory_order_acquire)原子存储就是 “释放”操作 (memory_order_release),原子读- -写 操作 (例如 fetch_add()或 exchange())在这里,不是 “获取 ”,就是 “释放 ”,或者两者兼有的操作 (memory_order_acq_rel)。这里,同步在线程释放和获取间,是成对的 (pairwise )。释放操作 与获取操作同步,这样就能读取已写入的值。这意味着不同线程看到的序列虽还是不同,但 这些序列都是受限的。
         获取(acquire——加载)-释放(release——存储)不意味着统一操作顺序:
        
        
  1. #include <atomic>
  2. #include <thread>
  3. #include <assert.h>
  4. std::atomic<bool> x, y;
  5. std::atomic<int> z;
  6. void write_x()
  7. {
  8. x.store(true, std::memory_order_release);
  9. }
  10. void write_y()
  11. {
  12. y.store(true, std::memory_order_release);
  13. }
  14. void read_x_then_y()
  15. {
  16. while (!x.load(std::memory_order_acquire));
  17. if (y.load(std::memory_order_acquire)) // 1
  18. ++z;
  19. }
  20. void read_y_then_x()
  21. {
  22. while (!y.load(std::memory_order_acquire));
  23. if (x.load(std::memory_order_acquire))
  24. ++z;
  25. }
  26. int main()
  27. {
  28. x = false;
  29. y = false;
  30. z = 0;
  31. std::thread a(write_x);
  32. std::thread b(write_y);
  33. std::thread c(read_x_then_y);
  34. std::thread d(read_y_then_x);
  35. a.join();
  36. b.join();
  37. c.join();
  38. d.join();
  39. assert(z.load() != 0); // 3,可能触发
  40. }
        因为x和y是由不同线程写入,所以序列中的每一次释放到获取都不会影响到 其他线程的操作(书中所述:结果类似自由序列)。至于分析,理解能力有限,感觉是一次对应后再次获取就可能不是对应的值了( https://www.zhihu.com/question/24301047),仍然待研究。
        获取-释放操作会影响序列中的释放操作:
        
        
  1. #include <atomic>
  2. #include <thread>
  3. #include <assert.h>
  4. std::atomic<bool> x, y;
  5. std::atomic<int> z;
  6. void write_x_then_y()
  7. {
  8. x.store(true, std::memory_order_relaxed); // 1
  9. y.store(true, std::memory_order_release); // 2
  10. }
  11. void read_y_then_x()
  12. {
  13. while (!y.load(std::memory_order_acquire)); // 3,自旋,等待y被设置为true
  14. if (x.load(std::memory_order_relaxed)) // 4
  15. ++z;
  16. }
  17. int main()
  18. {
  19. x = false;
  20. y = false;
  21. z = 0;
  22. std::thread a(write_x_then_y);
  23. std::thread b(read_y_then_x);
  24. a.join();
  25. b.join();
  26. assert(z.load() != 0); // 5,不会触发
  27. }
        读到的y和写入是一样的, 因为存储y使用的是 memory_order_release,读取y使用的是 memory_order_acquire,存储就与读取就同步了。1先于2,则1先于3,故1先于4,所以不会触发。
         与同步传递相关的获取 - 释放序列:通过获取和释放来控制同步。
         使用获取和释放顺序进行同步传递:
       
       
  1. #include <atomic>
  2. #include <cassert>
  3. #include <thread>
  4. std::atomic<int> data[5];
  5. std::atomic<bool> sync1(false), sync2(false);
  6. void thread_1()
  7. {
  8. data[0].store(42, std::memory_order_relaxed);
  9. data[1].store(97, std::memory_order_relaxed);
  10. data[2].store(17, std::memory_order_relaxed);
  11. data[3].store(-141, std::memory_order_relaxed);
  12. data[4].store(2003, std::memory_order_relaxed);
  13. sync1.store(true, std::memory_order_release); // 1.设置sync1
  14. }
  15. void thread_2()
  16. {
  17. while (!sync1.load(std::memory_order_acquire)); // 2.直到sync1设置后,循环结束
  18. sync2.store(true, std::memory_order_release); // 3.设置sync2
  19. }
  20. void thread_3()
  21. {
  22. while (!sync2.load(std::memory_order_acquire)); // 4.直到sync1设置后,循环结束
  23. assert(data[0].load(std::memory_order_relaxed) == 42);
  24. assert(data[1].load(std::memory_order_relaxed) == 97);
  25. assert(data[2].load(std::memory_order_relaxed) == 17);
  26. assert(data[3].load(std::memory_order_relaxed) == -141);
  27. assert(data[4].load(std::memory_order_relaxed) == 2003);
  28.    //以上实现了同步,不会触发
  29. }
  30. int main()
  31. {
  32. std::thread t1{thread_1}, t2{thread_2}, t3{thread_3};
  33. t1.join();
  34. t2.join();
  35. t3.join();
  36. return 0;
  37. }
         更改:
        
        
  1. #include <atomic>
  2. #include <cassert>
  3. #include <thread>
  4. std::atomic<int> data[5];
  5. std::atomic<int> sync(0);
  6. void thread_1()
  7. {
  8. data[0].store(42, std::memory_order_relaxed);
  9. data[1].store(97, std::memory_order_relaxed);
  10. data[2].store(17, std::memory_order_relaxed);
  11. data[3].store(-141, std::memory_order_relaxed);
  12. data[4].store(2003, std::memory_order_relaxed);
  13. sync.store(1, std::memory_order_release);
  14. }
  15. void thread_2()
  16. {
  17. int expected = 1;
  18. while (!sync.compare_exchange_strong(expected, 2,//保证thread_1对变量只进行一次更新
  19. std::memory_order_acq_rel))//同时进行获取和释放的语义
  20. expected = 1;
  21. /*当expected与sync相等时候,sync=2,返回true,结束程序;
  22. *当expected与sync不等的时候,expected=sync,返回false,
  23. 进入循环,expected=1;
  24. */
  25. }
  26. void thread_3()
  27. {
  28. while (sync.load(std::memory_order_acquire)<2);
  29. assert(data[0].load(std::memory_order_relaxed) == 42);
  30. assert(data[1].load(std::memory_order_relaxed) == 97);
  31. assert(data[2].load(std::memory_order_relaxed) == 17);
  32. assert(data[3].load(std::memory_order_relaxed) == -141);
  33. assert(data[4].load(std::memory_order_relaxed) == 2003);
  34. }
  35. int main()
  36. {
  37. std::thread t1{thread_1}, t2{thread_2}, t3{thread_3};
  38. t1.join();
  39. t2.join();
  40. t3.join();
  41. return 0;
  42. }
        关于 compare_exchange_weak、 compare_exchange_strong: http://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange
         获取 - 释放序列和 memory_order_consume 的数据相关性: memory_order_consume 是“ 获取- 释放” 序列模型的一部分, 它完 全依赖于数据,并且其展示了与线程间先行关系 的不同之处。数据依赖: 前序依赖 dependency-ordered-before )和携带依赖 carries-a-dependency-to )。
         使用  std :: memory_order_consume 同步数据:
        
        
  1. #include <string>
  2. #include <atomic>
  3. #include <thread>
  4. #include <cassert>
  5. struct X
  6. {
  7. int i;
  8. std::string s;
  9. };
  10. std::atomic<X*> p;
  11. std::atomic<int> a;
  12. void create_x()
  13. {
  14. X *x = new X;
  15. x->i = 42;
  16. x->s = "hello";
  17. a.store(99, std::memory_order_relaxed); // 1
  18. p.store(x, std::memory_order_release); // 2
  19. }
  20. void use_x()
  21. {
  22. X* x;
  23. while (!(x = p.load(std::memory_order_consume))) // 3
  24. std::this_thread::sleep_for(std::chrono::microseconds(1));
  25. assert(x->i == 42); // 4,不会触发
  26. assert(x->s == "hello"); // 5,不会触发
  27. assert(a.load(std::memory_order_relaxed) == 99); // 6,可能触发
  28. }
  29. int main()
  30. {
  31. std::thread t1(create_x);
  32. std::thread t2(use_x);
  33. t1.join();
  34. t2.join();
  35. return 0;
  36. }
         存储p 的操作标记为 emory_order_release,加载p 的操作标记为 emory_order_consume,这就意味着p存储 仅先行那些需要加载 的操作。 x变量操作 的表达式对加载p 的操作携带有依赖。
         显式打破依赖链
       
       
  1. int global_data[] = {...};
  2. std::atomic<int> index;
  3. void f()
  4. {
  5. int i = index.load(std::memory_order_consume);
  6. do_something_with(global_data[std::kill_dependency(i)]);
  7. //让编译器知道这里不需要重新读取该数组的内容
  8. }
         std :: kill_dependency ( ) 是一个简单的函数模 板,其会复制提供的参数给返回值,但是依旧会打破依赖链。 你必须记住,这是为了优化,所以这 种方式必须谨慎使用,并且需要性能数据证明其存在的意义(待研究)。

释放队列与同步 

         使用原子操作从队列中读取数据:
        
        
  1. #include <atomic>
  2. #include <thread>
  3. #include <vector>
  4. #include <iostream>
  5. std::vector<int> queue_data;
  6. std::atomic<int> count;
  7. void populate_queue()
  8. {
  9. unsigned const number_of_items = 20;
  10. queue_data.clear();
  11. for (unsigned i = 0;i<number_of_items;++i)
  12. {
  13. queue_data.push_back(i);
  14. }
  15. count.store(number_of_items, std::memory_order_release); // 1 初始化存储
  16. }
  17. void consume_queue_items()
  18. {
  19. while (true)
  20. {
  21. int item_index;
  22. if ((item_index = count.fetch_sub(1, std::memory_order_acquire)) <= 0)
  23. // 2 一个“读-改-写”操作
  24. {
  25. wait_for_more_items(); // 3 等待更多元素
  26. break;
  27. }
  28. process(queue_data[item_index - 1]); // 4 安全读取queue_data
  29. }
  30. }
  31. int main()
  32. {
  33. std::thread a(populate_queue);
  34. std::thread b(consume_queue_items);
  35. std::thread c(consume_queue_items);
  36. a.join();
  37. b.join();
  38. c.join();
  39. system("pause");
  40. return 0;
  41. }

栅栏  

        栅栏操作会对内存序列进行约束, 使其无法对任何数据进行修改,典型的做法是与使用 memory_order_relaxed约束序的原子操 作一起使用。栅栏属于全局操作,执行栅栏操作可以影响到在线程中的其他原子操作。因为 这类操作就像画了一条任何代码都无法跨越的线一样,所以栅栏操作通常也被称为“内存栅”(memory barriers)。
        栅栏可以让自由操作变的有序:
       
       
  1. #include <atomic>
  2. #include <thread>
  3. #include <cassert>
  4. std::atomic<bool> x, y;
  5. std::atomic<int> z;
  6. void write_x_then_y()
  7. {
  8. x.store(true, std::memory_order_relaxed); // 1
  9. std::atomic_thread_fence(std::memory_order_release); // 2,释放栅栏
  10. y.store(true, std::memory_order_relaxed); // 3
  11. }
  12. void read_y_then_x()
  13. {
  14. while (!y.load(std::memory_order_relaxed)); // 4
  15. std::atomic_thread_fence(std::memory_order_acquire); // 5,获取栅栏
  16. if (x.load(std::memory_order_relaxed)) // 6
  17. ++z;
  18. }
  19. int main()
  20. {
  21. x = false;
  22. y = false;
  23. z = 0;
  24. std::thread a(write_x_then_y);
  25. std::thread b(read_y_then_x);
  26. a.join();
  27. b.join();
  28. assert(z.load() != 0); // 7,不会触发
  29. }
        这两个栅栏都是必要 的:你需要在一个线程中进行释放,然后在另一个线程中进行获取,这样才能构建出同步关 系。 栅栏同步依赖于读取 /写入的操作发生于栅栏之前 /后,但是这里有一点很重要:同步 点,就是栅栏本身
       如果采用如下方式,则也许会触发,失去了栅栏将两个释放操作隔开。
        
        
  1. void write_x_then_y()
  2. {
  3. std::atomic_thread_fence(std::memory_order_release);
  4. x.store(true, std::memory_order_relaxed);
  5. y.store(true, std::memory_order_relaxed);
  6. }

原子操作对非原子的操作排序

        使用非原子操作执行序列:
     
     
  1. #include <atomic>
  2. #include <thread>
  3. #include <assert.h>
  4. bool x = false; // x现在是一个非原子变量
  5. std::atomic<bool> y;
  6. std::atomic<int> z;
  7. void write_x_then_y()
  8. {
  9. x = true; // 1 在栅栏前存储x
  10. std::atomic_thread_fence(std::memory_order_release);
  11. y.store(true, std::memory_order_relaxed); // 2 在栅栏后存储y
  12. }
  13. void read_y_then_x()
  14. {
  15. while (!y.load(std::memory_order_relaxed)); // 3 在#2写入前,持续等待
  16. std::atomic_thread_fence(std::memory_order_acquire);
  17. if (x) // 4 这里读取到的值,是#1中写入
  18. ++z;
  19. }
  20. int main()
  21. {
  22. x = false;
  23. y = false;
  24. z = 0;
  25. std::thread a(write_x_then_y);
  26. std::thread b(read_y_then_x);
  27. a.join();
  28. b.join();
  29. assert(z.load() != 0); // 5 断言将不会触发
  30. }
       其中y必须是原子操作,否则会数据竞争,而对x不用(栅栏作用)。
        不仅是栅栏可对非原子操作排序。你在上面 看到 memory_order_release/memory_order_consume对,也可以用来排序非原子访问,为的是可 以动态分配对象,并且本章中的许多例子都可以使用普通的非原子操作,去替代标记为m emory_order_relaxed的操作。

后记

        在本章的学习中,可以说是举步维艰,我觉得要先看《深度探索C++对象模型》等书,对内存模型有一个初步的认识,看来还要努力学习。同时,书中部分代码没有完全明白,今后需要重温。文中文字部分摘抄至原翻译作品以及自己的部分感想总结。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值