多线程
mutex相关的锁保证互斥
conditional_variable保证顺序
不如直接将所有对同一个元素的修改全部扔进一个线程安全的队列里,然后再一个一个取出来处理
保证了互斥和顺序(?)
当一个变量会被多个线程访问的时候,就一定要使用上述的这些多线程的工具,否则,编译器优化可能会出错,因为他假定一切都在单线程下运行。
实际上,多线程的数据竞争包含两件事情。
一是互斥,通过mutex,atomic来保证操作是一起发生的,不被其他的事情打断。
二是顺序,通过conditional_variable,内存序来保证事情发生的顺序,如读后写或写后读,或双写。
内存序
编译器优化的时候,会将指令进行重排来进行优化,虽然有volatile这个关键词来避免优化,但实际上并不可靠,一般只用在嵌入式的IO上。
另外,在x86平台上,其限定了TSO的一种标准,保证了其内存序,因此在x86上内存序并不发生多少影响。然而在arm,riscv等平台上,内存序是弱的,仅有依赖相关方面的内存序,因此有一些未在程序中表现出来的顺序就会被优化出问题。因此就必须显式的告知编译器要对这方面注意。
一般有以下几种内存序,越松的内存序就性能越高,因为能疯狂优化。
std::memory_order_seq_cst,最强的一种内存序,被默认使用。
std::memory_order_acquire,保证该行前面的所有的内存相关内容已被执行。一般在读的时候使用,保证之前的写全部完成了。
std::memory_order_release,保证该行后面的所有内容都必须在自身后面执行。一般在写的时候使用,保证这之后的读都能看到自己。
一般上述的两个会配套使用。
std::memory_order_acq_rel,当一个操作即涉及读也涉及写的时候使用,就是上面两个的结合体。
std::memory_order_relax,没有对内存序的要求,当他的操作只涉及自己的时候使用。
关于原子变量,其功能实际上也是两方面的,一方面保证其自身的每一个操作都是原子性的,但不保证多个操作之间的事务性,及不被打断。另一方面是保证其前后的内存序,可以在构造和操作的时候指定内存序,用来保护其他非原子变量。
当我们自己要进行一个原子性的操作的时候,可以使用CAS来保证其原子性。
1 | |
其原理就是,先记录老值,再记录新值,如果老值和这个变量现在的值一样,那么就赋值,否则就按现在的新值操作一遍,当其改变的时候,返回false,使用第一个内存序,否则返回true,使用第二个内存序,因此第一个一般是relax,第二个一般是release。这一操作是编译器保证的。
通过内存序,也就能实现无锁的线程安全的数据结构。
关于atomic
原本等待atomic改变,也需要疯狂的while,来判断,现在也可以像条件变量一样wait了。在wait的时候,是直接挂起等notify的,不占用cpu。
一般来说,为了效率高,在wait的时候都不会直接wait,而是疯狂的尝试一会是否可行,如果不行的话才摆烂wait的。