《Head First C 中文版》審讀筆記(三)

黃志斌發表於2013-05-07

簡單的多執行緒程式

《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

看到沒有,後面的數字比前面的大。這種結果很少見。

相關文章