12-3 资源的线程安全问题

1. 线程安全问题的来源之一 —— 对共享资源进行非原子操作的修改

        使用两个线程执行 Counter 函数,对 count 变量分别自加 100 万次,最后结果为 1049042,并非 200 万

        原因在于 count++ 操作并非原子操作。count++ 操作可拆解为 3 步:int temp = count,count = temp + 1,return temp。线程 1 执行完第一步,还没执行第二步时,线程 2 可能此时已经开始执行第一步,即 temp = count,故两个线程拿到的 temp 值相同。然后两个线程执行 count = temp + 1,加了两次,但 count 的值仅增加了 1

        对共享资源进行非原子操作的修改,是线程安全问题的来源之一。

#include <stdio.h>
#include <tinycthread.h>

int count = 0;

int Counter(void *arg){
  for (int i = 0; i < 1000000; ++i) {
    count++;
  }
  return 0;
}

void TestCounterByThread(){
  thrd_t t_1;
  thrd_t t_2;

  // create two threads to run the Counter function
  thrd_create(&t_1, Counter, NULL);
  thrd_create(&t_2, Counter, NULL);

  // free up resources
  thrd_join(t_1, NULL);
  thrd_join(t_2, NULL);

  printf("count: %d", count);  // count: 1049042
}

2. 线程安全问题的来源之二 —— 共享资源的可见性

        线程 A 对某个共享资源做了修改,线程 B 不一定能够立刻获取到修改的内容。修改内容需要通过 CPU 缓存同步到内存中,B 才能获取到该修改内容。

        在 T1 函数将 a 赋值为 2,flag 赋值为 1。在 T2 函数判断 flag 值并求解 a*a。理论上说,该代码可能陷入死循环,也可能计算出的 x 值为 0。原因在于可能 T1 函数执行 a = 2; flag=1; 后,改为 T2 函数执行,此时 T2 函数获取的 flag 和 a 值可能并没有改变,仍然为 0 和 0。但实际情况还与编译器内部实现有关,上述情况是理论结果

#include <stdio.h>
#include <tinycthread.h>

int flag = 0;

int a = 0;
int x = 0;

int T1(void *arg){
  a = 2;
  flag = 1;
  return 0;
}

int T2(void *arg){
  while (flag == 0){}
  x = a * a;
  return 0;
};

void TestVisibilityByThread(){
  thrd_t t_1;
  thrd_t t_2;

  // create two threads to run the Counter function
  thrd_create(&t_1, T1, NULL);
  thrd_create(&t_2, T2, NULL);

  // free up resources
  thrd_join(t_1, NULL);
  thrd_join(t_2, NULL);

  printf("a: %d ", x);  // count: 1049042
}

int main(){

  int count = 0;
  while (count < 1000000){
    TestVisibilityByThread();
    count++;
  }

  return 0;
}

3. 线程安全问题的来源之三 —— 代码重排序

        在 CMakeLists.txt 文件中添加 set(CMAKE_C_FLAGS "-O3"),可以让编译器对汇编指令进行优化。而这种优化是保证单个线程内部逻辑不变的情况下,对编译器进行的优化。而对多个线程间的逻辑,则无法保证

        查看代码的汇编指令进行验证。现修改 2 小节的部分代码,查看其汇编指令。

int T1(void *arg){
  a = 2;
  flag = 1;
  a = a + 3;
  return 0;
}

        未优化前的汇编指令如图所示,可以发现 a = 2, a = a + 3 两个操作对应的指令是分开的。

而优化后的汇编指令如下图所示, a = 2, a = a + 3 两个操作对应的指令被编译器优化为了一条指令,a 直接被赋值为 5,不存在赋值为 2 的可能性。

        此外,对 2 小节的 T2 函数代码,观察 while 循环对应的未优化前的汇编指令。首先获取 flag 的值,然后 test flag,如果为 0 就跳到 L4,如果不为 0 就继续向下执行。

        优化后的汇编指令如下图所示,首先获取 flag 的值,然后判断 flag 是否为 0,如果不是 0 继续执行,如果是 0 跳到 L5。注意此时指令中出现了死循环,和先前未优化前相比,优化后的指令只对 flag 判断了一次,而不是先前对 flag 循环进行判断

        此时修改 2 节的部分代码,可让程序执行结果为死循环。注意,修改后的代码使用未优化指令不会产生死循环的结果两者差异的原因在于 flag 是否被循环进行判断

// create two threads to run the Counter function
thrd_create(&t_2, T2, NULL);
thrd_create(&t_1, T1, NULL);

优化指令程序执行结果:

未优化指令程序执行结果:

4. 线程安全问题的产生

        1)对共享资源进行非原子的并发访问

        2)不同线程之间的代码可见性问题

        3)线程内部代码编译时的重排序问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值