0%

《C++ 并发编程实战》读书笔记(2)

《C++ 并发编程实战》读书笔记(2)

等待一个具有超时条件的条件变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <condiiton_variable>
#include <mutex>
#include <chrono>
std::condition_variable cv;
bool done;
std::mutex m;

bool wait_loop(){
auto const timeout = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
std::unique_lock<std::mutex> lk(m);
while(!done) {
if(cv.wait_until(lk, timeout) == std::cv_status::timeout) break;
}
return done;
}

一个简单的启动线程的实现,不推荐实际使用, 因为创建一个线程时间成本很高。

1
2
3
4
5
6
7
8
9
10
template<typename F, typename A>
std::future<std::result_of<F(A&&)>::type>
spawn_task(F&& f, A&& a) {
typedef std::result_of<F(A&&)>::type result_type;
std::packaged_task<result_type(A&&)> task(std::move(f));
std::future<result_type> res(task.get_future());
std::thread t(std::move(task), std::move(a));
t.detach();
return res;
}

内存模型

C++程序中的所有数据均是对象(object)组成的。这并不是说你可以创建一个派生自int的新类,或是基本类型具有成员函数。这只是一句关于C++中数据的构造块的一种陈述。C++标准定义对象那个为存储区域,尽管它会为这些对象分配属性,如它们的类型和生存周期。
无论什么类型,对象均被存储与一个或多个内存位置中。每个内存位置要么是一个标量类型的对象,比如unsigned shortmy_class*, 要么是相邻位域的序列。如果使用位域,有非常重要的一点必须注意:虽然相邻的位域是不同的对象,但他们仍然算作相同的内存位置。

  • 每个变量都是一个对象,包括其他对象的成员。
  • 每个对象占据至少一个内存位置。
  • intchar这样的基本类型的变量恰好一个内存位置,不论其大小,即使它们相邻或是数组的一部分。
  • 相邻的位域是相同内存位置的一部分。

原子操作

原子操作是一个不可分割的操作。从系统中的任何一个线程中,你都无法观察到一个完成到一半的这种操作,他要么做完了要么没做完。

传统意义上,标准原子类型是不可复制且不可赋值的,因为它们没有拷贝构造函数和拷贝赋值运算符。但是,它们确实支持从相应的内置类型的赋值进行隐式转换并赋值。由于他是一个泛型类模板,操作只限为load()store()exchange()compare_exchange_weak()compare_exchange_strong()
在原子类型上的每一个操作均具有一个可选的内存顺序参数,它可以用来指定所需的内存顺序语义。顺序运算分为三种类型:

  • 存储(store)操作, 可以包括memory_order_relaxedmemory_order_releasememory_order_seq_cst顺序
  • 载入(load)操作,可以包括memory_order_relaxedmemory_order_consumememory_order_acquirememory_order_seq_cst顺序。
  • 读-修改-写(read_modify_write)操作, 可以包括memory_order_relaxedmemory_order_consumememory_order_acquirememory_order_releasememory_order_acq_relmemory_order_seq_cst顺序。
    所有操作的默认顺序为memory_order_seq_cst

使用std::atomic_flag实现一个自旋锁

1
2
3
4
5
6
7
8
9
10
11
12
13
class spinlock_mutex
{
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);
}
private:
std::atomic_flag flag;
};

根据当前值存储一个新值

这个新的操作被称为比较/交换,它以compare_exchange_weak()compare_exchange_strong()成员函数形式出现。
比较/交换操作是是使用原子类型编程的基石,它比较原子变量值和所提供的期望值,
如果两者相等,则存储提供的期望值。
如果两者不等,则期望值更新为原子变量的实际值。
比较/交换函数的返回类型为bool, 如果执行了存储即为true, 反之则为false

对于compare_exchange_weak(),
即使原始值等于期望值也可能出现存储不成功,在这种情况下变量的值是不变的, compare_exchange_weak()的返回值为false
这最有可能发生在缺少单个的比较并交换指令的机器上,此时处理器无法保证该操作被完成–这就是所谓的伪失败,因为失败的原因是时间的函数而不是变量的值。
由于compare_exchange_weak()可能会伪失败,它通常必须用在循环中。

另一方面,仅当实际值不等于expected值时compare_exchange_strong()才保证返回false。这样可以消除对循环的需求。

如果你想要改变变量,无论其初始值是多少, expected的更新就变得很有用,每次经过循环时,excepted被重新载入,因此如果没有其他线程在此期间修改其值,那么compare_exchange_weak()compare_exchange_strong()的调用在下一次循环中应该是成功的。

如果计算待存储的值很简单,为了避免在compare_exchange_weak()可能会伪失败的平台上的双重循环,(于是compare_exchange_strong包含一个循环), 则使用compare_exchange_weak()可能是有好处的。另一方面,
如果计算待存储的值本身是耗时的,当expected值没有变化时,使用compare_exchange_strong()来避免被迫重新计算待存储的值可能时有意义的。对于std::atomic<bool>而言这并不重要,毕竟只有两个值,但对于较大的原子类型这会有所不同。

比较/交换函数还有一点非同寻常,他们可以接受两个内存顺序参数。这就允许内存顺序的语义在成功和失败的情况下有所区别。对于一次成功调用具有memory_order_acq_rel语义而一次失败的调用有着memory_order_relaxed语义,这想必是极好的。一次失败的比较/交换并不进行存储,因此它无法具有memory_order_releasememory_order_acq_rel语义。因此再失败时禁止提供这些值作为顺序。你也不应为失败提供比成功更严格的内存顺序。如果你希望memory_order_acquire或者memory_order_seq_cst作为失败的语义,你也必须为成功指定这些语义。

如果你没有为失败指定一个顺序,就会假定它与成功是相同的,除了顺序的release部分被除去:memory_order_release变成memory_order_relaxed, memory_order_acq_rel变成memory_order_acquire。如果你都没有指定,他们它们通常默认为memory_order_seq_cst, 这为成功和失败都提供了完整的序列顺序。以下对compare_exchange_weak()的两个调用时等价的。

1
2
3
4
std::atomic<bool> b;
bool expected;
b.compare_exchange_weak(expected, true, memory_order_acq_rel, memory_order_acquire);
b.compare_exchange_weak(exprected, true, memory_order_acq_rel);