预测执行又称推测执行、投机执行,是一类底层优化技术,包括分支预测、数值预测、预读内存和预读文件等,目的是在多级流水cpu上提高指令的并发度。做法是提前执行指令而不考虑师傅必要,若完成后发现没必要,则抛弃或修正预执行的结果
在c++环境中,多数情况下,我们需要通过原子类型实现原子操作。
标准原子类型的别名,和他们对应的std::atomic<>特化。
类型别名 | 定义 |
---|---|
std::atomic_bool | std::atomic |
std::atomic_char | std::atomic |
std::atomic_schar | std::atomic |
std::atomic_uchar | std::atomic |
std::atomic_short | std::atomic |
std::atomic_ushort | std::atomic |
std::atomic_int | std::atomic |
std::atomic_uint | std::atomic |
std::atomic_long | std::atomic |
std::atomic_ulong | std::atomic |
std::atomic_llong | std::atomic |
std::atomic_ullong | std::atomic |
std::atomic_char8_t (C++20) | std::atomic |
std::atomic_char16_t | std::atomic |
std::atomic_char32_t | std::atomic |
std::atomic_wchar_t | std::atomic |
还有一些其他的类型别名,可以参见std::atomic
原子指令用于在多个CPU之间维护同步关系。在一些科学计算问题中,通过并行算法把子问题分配到多个cpu上执行,但是各个子问题之间存在合作关系,因此需要硬件机制来实现多个cpu之间同步。
原子指令可以实现一个CPU独占执行时间。使用原子指令把连续多条指令包含起来,计算机保证只有一个cpu处于执行状态,其他cpu必须等待原子指令结束才能继续执行。(b)展示的就是实现“原子加1”的正确方法。
原子指令的实现机制一般是在cpu的互联网络中实现一个全局的原子寄存器,所有cpu对这个原子寄存器的访问是互斥的。cpu使用原子指令申请访问原子寄存器时,互联网络会对所有CPU进行仲裁,确保只有一个cpu可以获得对原子寄存器的访问权;如果有cpu获得了原子寄存器访问权,其他cpu必须等待该cpu释放权限才能继续执行。
原子指令详细介绍参见什么是原子指令
#include
#include using namespace std;int main()
{atomic x;bool flag = x.is_lock_free();cout << flag << endl;return 0;
}
输出
1
c++程序库专门为此提供了一组宏。他们的作用是,针对不同整数类型特化而成的各种原子类型,在编译期判定其是否属于无锁数据结构。
#if defined ATOMIC_BOOL_LOCK_FREE
#if defined ATOMIC_CHAR_LOCK_FREE
#if defined ATOMIC_CHAR16_T_LOCK_FREE
#if defined ATOMIC_CHAR32_T_LOCK_FREE
#if defined ATOMIC_WCHAR_T_LOCK_FREE
#if defined ATOMIC_SHORT_LOCK_FREE
#if defined ATOMIC_INT_LOCK_FREE
#if defined ATOMIC_LONG_LOCK_FREE
#if defined ATOMIC_LLONG_LOCK_FREE
#if defined ATOMIC_POINTER_LOCK_FREE
#include
//#include using namespace std;int main()
{
#if defined ATOMIC_BOOL_LOCK_FREEint a = 2;
#elseint a = 3;
#endifcout << a << endl;return 0;
}
输出
2
std::atomic_flag是最简单的标准原子类型,表示一个布尔标志。
该类型的对象只有两种状态:成立或置零。二者必居其一。
经过刻意设计,它相当简单,唯一用途是充当构建单元,因此我们认为普通开发者一般不会直接使用它。尽管这样,我们从std:atomic_flag切入,仍能借以说明原子类型的一些通用原则,方便进一步讨论其他原子类型。
1、atomic_flag类型的对象必须由宏 ATOMIC_FLAG_INIT 初始化,它把标志初始化为置零状态。
2、完成std::atomic_flag对象的初始化后,我们只能执行3种操作:销毁、置零、读取原有的值并设置标志成立。这分别对应于析构函数、成员函数clear()、成员函数test_and_set();
3、我们可以为clear()、test_and_set()指定内存次序。clear()是存储操作,而test_and_set()是”读-改-写“,因此能采用任何内存次序。对于上面两个原子操作,默认内存次序都是std::memory_order_seq_cst。
这个互斥非常简单,但已经能够配合lock_guard<>使用
class spinlock_mutex
{
public:spinlock_mutex() {}//spinlock_mutex(const spinlock_mutex& origin); // add this line~spinlock_mutex() {};void lock(){while (flag.test_and_set(memory_order_acquire));}void unlock(){flag.clear(memory_order_release);}
private:atomic_flag flag = ATOMIC_FLAG_INIT;
};
由于atomic_flag严格受限,甚至不支持单纯的无修改查值操作,无法用作普通的布尔标志,因此最好还是使用atomic
std::atomic b(true);
b=false;
- 原子地以 desr 的值替换 obj 所指向的值,并返回 obj 先前保有的值,如同以 obj->exchange(desr) 。
- 原子地以 desr 的值替换 obj 所指向的值,并返回 obj 先前保有的值,如同以 obj->exchange(desr, order) 。
参数
obj - 指向要修改的原子对象的指针
desr - 要存储于原子对象的值
order - 此操作所用的内存同步顺序:容许所有值。
#include
#include using namespace std;int main()
{atomic b;bool x = b.load(std::memory_order_acquire);b.store(true);x = b.exchange(false, std::memory_order_acq_rel);cout << "b=" << b << endl;cout << "x=" << x << endl;return 0;
}
输出
b=0
x=1
std::atomic还引入了一种操作,若原子对象当前的值符合预期,就赋予新值。它与exchage()一样,同为”读-改-写“操作。
比较交换操作是原子类型的编程基石。使用者给定一个期望值,原子变量将它和自身的值做比较,如果相等,就传入另一个既定的值;否则,更新期望值所属的变量,向它赋予原子变量的值。
比较交换函数返回布尔类型,如果完成了保存动作(前提是两值相等),则操作成功,函数返回ture,反之操作失败,函数返回false.
原子化的比较-交换必须由一条指令单独完成,而某些处理器没有这种指令,无法保证该操作按原子化方式完成。
要实现比较-交换,负责的线程则必须改为连续运行一些列指令,但在这些计算机上,只要出现线程数量多于处理器数量的情形,线程就有可能执行到中途因系统调度而切出,导致操作失败。
这种计算机最有可能引发上述的保存失败,我们称之为佯败(spurious failure)。其因败因不算变量值本身存在问题,而是函数执行时机不对。
因为compare_exchange_weak()可能佯败,所以它必须配合循环使用。
#include
#include using namespace std;int main()
{bool expected = false;extern atomic b; //由其他源文件的代码设定变量的值while(!b.compare_exchange_weak(expected, true) && !expected);{cout << "b=" << b << endl;}return 0;
}atomic b;
输出
b=1
这让我们得以明确知悉变量是否成功修改,或者是否存在另一线程抢先切入而导致佯败,从而能够摆脱上例所示的循环。
另外比较-交换函数中,关于内存次序这个参数我们就不再说了,感兴趣的可以看书籍。
#include
#include
#includeusing namespace std;class foo {};int main()
{foo foo_array[5];// 可以和boo类型类比,定义一个foo*指针,初始值是数组的第一个对象。atomic p(foo_array);foo* x = p.fetch_add(2); // 令p+2,返回旧址。assert(x==foo_array);assert(p.load() == &foo_array[2]);x = (p -= 1); //令p-1,返回新值assert(x == &foo_array[1]);assert(p.load() == &foo_array[1]);return 0;
}
在std::atomic
主模板的存在,在除了标准原子类型之外,允许用户使用自定义类型创建一个原子变量。不 是任何自定义类型都可以使用std::atomic<> 的:需要满足一定的标准才行。为了使用 std::atomic (UDT是用户定义类型),这个类型必须有拷贝赋值运算符。这就意味着这个类型不能有任何虚函数或虚基类,以及必须使用编译器创建的拷贝赋值操作。不仅仅是这 些,自定义类型中所有的基类和非静态数据成员也都需要支持拷贝赋值操作。这(基本上)就允 许编译器使用memcpy(),或赋值操作的等价操作,因为它们的实现中没有用户代码。
最后,这个类型必须是“位可比的”(bitwise equality comparable)。这与对赋值的要求差不多; 你不仅需要确定,一个UDT类型对象可以使用memcpy()进行拷贝,还要确定其对象可以使用 memcmp()对位进行比较。之所以要求这么多,是为了保证“比较/交换”操作能正常的工作。
内存次序共6种,分3种模式:
库中所有原子操作的默认行为提供先后一致次序(也叫序列一致顺序)。
至于每个的区别我们在这里就不讨论了,感兴趣的可以通过文后的参考链接和书籍,自行研究。
参见【并发编程十四】c++原子操作实现自旋锁
参考:
1、https://www.apiref.com/cpp-zh/cpp/thread.html
2、《c++并发编程实战(第二版)》安东尼.威廉姆斯 著;吴天明 译;