为什么c++11需要针对语言层面引入线程

整理自这篇文档,好文档

1. c++11之前引入多线程的方式

​ 通过引入线程库的方式,一般是Pthread库的方式引入线程库,注意的是编译器并不知道有线程的存在,编程语言也不知道,仅仅库支持线程的一些操作,这种方式脱离语言不保证语言的安全性

2. Pthread标准

1.  应用程序应该确保一个或者两个以上的进程、线程对任何内存区域的访问收到限制,为了确保当一个线程正在修改内存位置,没有线程可以读取或者修改该位置,可以通过同步线程执行和同步内存的函数(pthread_mutex_lock())达到限制其他线程的操作
2.  禁止竞争使用变量,一个全局变量,A线程同时修改变量,B线程同时读取读取变量,不允许这样,值得注意的是,这种情况的出现不仅仅是相邻字段,在相同内存位置的任何字段都可能出现读取内存位置字段和修改内存位置字段
3.  建议使用Pthread互斥语句防止并发修改共享变量

3. Pthread并发带来的问题1 -- 编译器和硬件对代码进行重排序、优化

​ 除了多线程程序中可见存在的并发读写、重入。还有编译器和硬件的内存操作重排序也会引入变量竞争

1. 编译器可能会对内存操作进行重排序(在不影响线程内部依赖的前提下),这样不会改变每个线程的本身含义,可以达到性能提升的效果

2. 硬件可能会对内存操作重排序建立在一些约束条件下,例如那些常见的硬件平台,对于x86处理器,可以在加载后对存储结构重新排序,一般来说,存储接口会立即产生一个写的缓冲区,之后再写入cache区域,之后其他线程将对这个加载后的变量存储信息可见

​ 例子1:

​ 针对于x和y初始化为0,两个线程分别执行一句:

if (x == 1) ++y;
if (y == 1) ++x;

​ 因为顺序执行原则,所以结果就是x和y都是0,没啥改变的

​ 当编译器可能对代码进行重排序:

++y; if (x != 1) --y;
++x; if (y != 1) --x;

​ 那么当编译器转换为这种形式就出现了变量的竞争

例子2:

​ 再比如下面c的位域操作

​ struct { int a:17; int b:15 } x;

​ 在对a进行更改的时候,大致有下面的操作

1. 保存 x的值
  1. 进行位操作赋值

  2. 将值写回到x

    如果是一个顺序执行一个线程执行对a的修改,那没问题,但是如果在步骤1和步骤2之间,另一个线程对b进行修改,那么将会引发一个竞争操作,并且另一个线程对b的更新操作可能失效,尽管他们不在一个位域操作

例子3:

​ 对程序的性能优化带来的问题

​ 下面的例子:

​ 在一个for循环中对于全局变量mt重复更新,使用了pthread_mutex_lock进行锁保护,但是锁是有条件获得的

for (...) {
...
if (mt) pthread_mutex_lock(...);
x = ... x ...
if (mt) pthread_mutex_unlock(...);
}

​ 编译器可能因为性能优化和速度把代码进行优化为:

r = x;
for (...) {
...
if (mt) {
x = r; pthread_mutex_lock(...); r = x;
}
r = ... r ...
if (mt) {
x = r; pthread_mutex_unlock(...); r = x;
}
}
x = r;

​ 这就有额外引出了竞争,多线程就会有问题

4. Pthread并发带来的问题2 -- 性能上的影响

​ Pthread 中的 thread mutex lock()和pthread mutex unlock()等都需要一条可以使用原子方式更新内存位置的硬件指令,这个操作隐式地防止在调用周围对内存引用进行硬件重新排序否则还需要单独的硬件屏障指令

1. **以原子方式更新内存位置的硬件指令需要超过100个处理器周期**
2. **硬件指令本身的高成本和构建在其上的pthread原语的更高成本**

性能上 thread mutex lock()> 自旋锁 > volatile > 无锁操作

5. 问题原因

1. 当竞争出现的时候  c语言模型中没有规定变量可以如何操作,不像java:japan:
2.  c语言的规范在多线程场景下没有限制,比如对相邻字段的隐士写入
3. 当出现并发更新,最终结果取决于语言规范给出它们的语义,以及编译器本身进行的实现
4. c编译器不知道线程的存在,所以优化的时候不会考虑线程
5. 比如标准库的引用计数直接使用原子操作

6. 建议操作

  1. 使用现代语言进行并发操作(java c++11)
  2. 仔细和直接地使用硬件原语比如(原子变量、volatile、自旋锁),以及更少地限制共享变量的使用
  3. double check lock本身就是一个极大耗费性能的操作,不应该出现这种事情
  4. 除了简单的原子加载和存储之外,还涉及直接依赖于原子内存操作的编程技术,来避免锁
  5. 其实当今软件已经出现了很多针对并发的解决方案 比如 CAS等

posted @ 2021-01-25 22:52  make_wheels  阅读(90)  评论(0编辑  收藏  举报