Concurrency

多线程编程

多线程并发执行时,由于处理器调度的不确定性,各线程指令会以任何可能的方式交叠执行。此时,当多个线程并发访问共享变量或资源时,由于缺乏操作的原子性、顺序性和一致性等问题,就可能产生预料之外的后果。

无法保证原子性

当两个线程一起调用 pay(100) 时,会对共享变量 balance 产生预料之外的影响 (代码的交替执行)。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

unsigned long balance = 100;

void *pay(void *arg) {
  int amount = *((int*) arg);
  if (balance >= amount) {
    usleep(1);  // some unexpected delays
    balance -= amount;
  }
  return NULL;
}

int main(void) {
  pthread_t card, phone;
  int num = 100;
  pthread_create(&card, NULL, buy_meals, (void*)&num);
  pthread_create(&phone, NULL, buy_meals, (void*)&num);

  pthread_join(card, NULL);
  pthread_join(phone, NULL);

  printf("Balance: %lu\n", balance);
  return 0;
}

当两个线程一起对共享变量 count 进行 count++ 操作时,最终输出的 count 不会如预期一样等于 2000000 (指令的交替执行)。 此时,即使把 count++ 替换为一条指令 asm volatile("incq %0" : "+m" (count)),也无法得到正确的预期结果 (在多核下多个线程的指令根本就是并行执行的)。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define NUM 1000000
int count = 0;

void* func(void *arg) {
  for (int i = 0; i < NUM; i++) { 
    count++;
  }  
  return NULL;
}

int main(void) {
  pthread_t thread1, thread2;
  pthread_create(&thread1, NULL, func, NULL);
  pthread_create(&thread2, NULL, func, NULL);

  pthread_join(thread1, NULL);
  pthread_join(thread2, NULL);

  printf("Count: %d\n", count);
  return 0;
}

无法保证顺序性

使用不同的编译优化选项会对代码行为产生影响。例如,使用 -O1 编译上述 count++ 代码会观察到 1000000;而使用 -O2 编译上述代码则会观察到 2000000 (此时程序行为一定是正确的吗?)。

此外,对于下述代码 (尝试实现一种 T1 等待 T2 的行为),在不进行编译优化和使用 -O2 进行优化时,程序也会表现出不同的行为。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

int flag = 0;
int x = 0;

void *T1(void* arg) {
  while (flag == 0)
    ;
  printf("%d\n", x);
  return NULL;
}

void *T2(void* arg) {
  x = 10;
  flag = 1;
  return NULL;
}

int main() {
  pthread_t thread1, thread2;
  pthread_create(&thread1, NULL, T1, NULL);
  pthread_create(&thread2, NULL, T2, NULL);

  pthread_join(thread1, NULL);
  pthread_join(thread2, NULL);  
  return 0;
}