简单的多线程程序

《Head First C 中文版》第 510 页给出了一个非常简单的多线程程序 beer.c:

派对开始了,倒计数啤酒瓶数。下面这段代码运行了 20 个线程,总共有 200 万瓶啤酒。

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

int beers = 2000000;

void *drink_lots(void *a)
{
  int i;
  for (i = 0; i < 100000; i++) {
    beers = beers - 1;
  }
  return NULL;
}

int main()
{
  pthread_t threads[20];
  int t;
  printf("%i bottles of beer on the wall\n%i bottles of beer\n", beers, beers);
  for (t = 0; t < 20; t++) pthread_create(&threads[t], NULL, drink_lots, NULL);
  void *result;
  for (t = 0; t < 20; t++) pthread_join(threads[t], &result);
  printf("There are now %i bottles of beer on the wall\n", beers);
  return 0;
}

仔细观察刚才那个程序,当多次运行程序时会发生:

> gcc beer.c -lpthread -o beer
> ./beer
2000000 bottles of beer on the wall
2000000 bottles of beer
There are now 353860 bottles of beer on the wall
> ./beer
2000000 bottles of beer on the wall
2000000 bottles of beer
There are now 888389 bottles of beer on the wall
> ./beer
2000000 bottles of beer on the wall
2000000 bottles of beer
There are now 684100 bottles of beer on the wall

大多数情况下,代码没有把 beers 变量减为 0。

奇怪, beers 变量的初始值是 200 万,每个线程都把它的值减去 10 万,一共有 20 个线程,beers 变量不应该每次都减到 0 吗?

这是由于在多线程程序中,多个线程对共享变量 beers 同时写入的结果。

假设某个时刻 beers 的值为 37,我们想要这么执行:

  1. 线程 1 读 beers,得 37;
  2. 线程 1 将 beers 减一,得 36;
  3. 线程 1 将 36 写回 beers;
  4. 线程 2 读 beers,得 36;
  5. 线程 2 将 beers 减一,得 35;
  6. 线程 2 将 35 写回 beers。

但在多线程并行的情况下,很可能是:

  1. 线程 1 读 beers,得 37;
  2. 线程 2 读 beers,得 37;
  3. 线程 2 将 beers 减一,得 36;
  4. 线程 2 将 36 写回 beers;
  5. 线程 1 将 beers 减一,得 36;
  6. 线程 1 将 36 写回 beers。

用互斥锁来管理交通

第 518~519 页的练习解答,使用互斥锁来解决这个问题。

找到上锁的位置实非易事,而锁的位置会改变代码的运行方式。下面有两个不同版本的drink_lots()函数,它们以不同方式为代码上了锁。

版本一

版本一 beer_fixed_strategy_1.c 如下所示:

pthread_mutex_t beers_lock = PTHREAD_MUTEX_INITIALIZER;
void *drink_lots(void *a)
{
  int i;
  pthread_mutex_lock(&beers_lock);
  for (i = 0; i < 100000; i++) {
    beers = beers - 1;
  }
  pthread_mutex_unlock(&beers_lock);
  printf("beers = %i\n", beers);
  return NULL;
}

编译和运行:

> gcc beer_fixed_strategy_1.c -lpthread -o beer_fixed_strategy_1
> ./beer_fixed_strategy_1
2000000 bottles of beer on the wall
2000000 bottles of beer
beers = 1900000
beers = 1800000
beers = 1700000
beers = 1600000
beers = 1500000
beers = 1400000
beers = 1300000
beers = 1200000
beers = 1100000
beers = 1000000
beers = 900000
beers = 800000
beers = 700000
beers = 600000
beers = 500000
beers = 400000
beers = 300000
beers = 200000
beers = 100000
beers = 0
There are now 0 bottles of beer on the wall
>

版本二

版本一是使用互斥锁给整个循环上锁。而版本二 beer_fixed_strategy_2.c 是在循环内部使用互斥锁对单个语句上锁:

pthread_mutex_t beers_lock = PTHREAD_MUTEX_INITIALIZER;
void *drink_lots(void *a)
{
  int i;
  for (i = 0; i < 100000; i++) {
    pthread_mutex_lock(&beers_lock);
    beers = beers - 1;
    pthread_mutex_unlock(&beers_lock);
  }
  printf("beers = %i\n", beers);
  return NULL;
}

编译和运行:

> gcc beer_fixed_strategy_2.c -lpthread -o beer_fixed_strategy_2
> ./beer_fixed_strategy_2
2000000 bottles of beer on the wall
2000000 bottles of beer
beers = 1708955
beers = 1455422
beers = 1432694
beers = 1080733
beers = 822639
beers = 610817
beers = 432912
beers = 413783
beers = 388791
beers = 370305
beers = 334197
beers = 256264
beers = 216787
beers = 189881
beers = 164612
beers = 143293
beers = 54990
beers = 18085
beers = 3175
beers = 0
There are now 0 bottles of beer on the wall
>

两段代码都用互拆锁来保护beers变量的安全,并在退出前显示了beers值。由于它们在不同的位置使用了锁,因此在屏幕上输出了不同结果。

多次运行

由于版本一是给整个循环上锁,多次运行,其结果始终是一样的。而版本二是给单个语句上锁,在循环执行过程中可能被其他线程抢占,所以多次运行的结果一般来说是不同的。再次运行版本二:

> ./beer_fixed_strategy_2
2000000 bottles of beer on the wall
2000000 bottles of beer
beers = 1066452
beers = 951407
beers = 929222
beers = 817108
beers = 805376
beers = 748046
beers = 738577
beers = 594522
beers = 447356
beers = 332950
beers = 280823
beers = 221191
beers = 204440
beers = 177260
beers = 147865
beers = 119168
beers = 70104
beers = 67381
beers = 11243
beers = 0
There are now 0 bottles of beer on the wall
> 

不断运行版本二的程序,多次之后,还出现了:

> ./beer_fixed_strategy_2
2000000 bottles of beer on the wall
2000000 bottles of beer
beers = 1135299
beers = 917185
beers = 640456
beers = 617477
beers = 466726
beers = 379176
beers = 289287
beers = 288456
beers = 259522
beers = 211679
beers = 190408
beers = 181443
beers = 144808
beers = 141295
beers = 121684
beers = 110626
beers = 63618
beers = 37551
beers = 0
beers = 10179
There are now 0 bottles of beer on the wall

看到没有,后面的数字比前面的大。这种结果很少见。