天天看點

83-自己實作互斥鎖

前面已經講過兩種鎖了,現在我們先停一停,思考一下如果我們自己要實作一把鎖該怎麼做?

1. 思路

通常的一種思路就是設定一個共享标記,假設通過指針 lock 傳遞。然後去檢查該 lock 指向的共享記憶體的值是否為 1. 代碼像下面的樣子:

void mylock(int *lock) {
  int tmp = 1;
  while (tmp == 1) {
    tmp = *lock; // 如果 *lock 為 1,說明鎖被占用,這時候會死循環。
    *lock = 1; // 無論鎖有沒有被占用,這裡都将共享記憶體的值置 1.      

上面這段代碼存在的問題就是競争未加鎖的 lock 的時候,如果兩個線程同時競争一把未上鎖的 lock,此時 *lock == 0,如果其中一個線程執行完 tmp = *lock 的時候,切換到了另一個線程,另一個線程也執行了 tmp = *lock,這樣,兩邊的線程都認為自己拿到了鎖 (tmp == 0) 進而結束循環,于是産生錯誤。

所有,除非有一種辦法,讓 ​

​tmp = *lock; *lock = 1​

​ 一次性執行完畢而不會打斷,那麼上面的代碼就完美了。

事實上,這種手段是存在的,隻不過在 C 語言中沒有辦法,但是有彙編指令可以幫助我們做到,這條指令非常簡單——xchg.

2. xchg 指令

它的格式如下:

xchg a, b      

主要用于交換 a 和 b 的值。在單核 cpu 中,彙編指令都是原子的,是以要麼指令一次執行成功,要麼不執行。在多核 cpu 中,為了防止競争,隻要在彙編指令前加一條字首 lock 即可:

lock      

這樣就可以防止這一條指令同時被多個 cpu 同時執行。

3. 解決問題

3.1 mylock 實作

​tmp = *lock; *lock = 1​

​ 如果能寫成

tmp = 1;
xchg(tmp, *lock);      

那就相當美好了,可是 C 語言是禁止這種寫法的。為了能完成同樣的功能,不得不把第 1 節中的代碼改成彙編的樣子:

void mylock(int *lock) {
  __asm__ __volatile__ ("1:\n\t"
      "movl $1, %%eax\n\t"
      "lock xchg %%eax, %0\n\t" /* 将 lock 中的值和 eax 進行交換, 相當于 eax = lock, lock = 1 */
      "test %%eax, %%eax\n\t"/* 判斷 eax 是否為 1 */
      "jnz 1b" /* 如果為 1 則跳到标記 1 的地方繼續執行 */
      ::"m"(*lock)
      :"%eax"      

上面代碼中的 eax,就相當于前面代碼中的 tmp 變量,隻不過換成了寄存器。

這些代碼看起來可能會有暈,一點一點解釋:

  • asm表示在 C 語言中内聯彙編。
  • volatile表示記憶體中的值可能産生變化,在編譯的時候重新從該記憶體擷取值而不是從寄存器中取該記憶體的備份。
  • “1:\n\t” 表示位置标記,類似 C 語言中的 goto 語句的标記。這是友善指令”jnz 1b” 能夠在滿足條件的時候跳轉到這裡。
  • $1 表示立即數 1,是以 “mov $1, %%eax\n\t” 表示置 eax 的内容為 1.
  • %0 表示把後面的 “m”(*lock) 中的 *lock 的值放到這個位置。 “lock xchg %%eax, %0\n\t” 的功能前面已經講過了,就是交換 eax 和 *lock 的值。
  • test %%eax, %%eax 用于測試 eax 的值是否為 0,如果為 0,為将結果儲存到 eflags 寄存器的 ZF 位。
  • “jnz 1b” 表示如果 test 指令的結果為 1,就跳到 “1:\n\t” 這個地方。

3.2 myunlock 解鎖實作

解鎖的代碼相當簡單,一行就可以搞定:

void myunlock(int *lock) {
  *lock = 0;
}      

這個沒什麼好解釋的了……

4. 功能測試

我們将之前的 luffy 和 allen 搶火車票的例子拿來修改修改,将互斥鎖換成我們實作的鎖。

程式 myspinlock 通過指令行傳參數,傳 0 表示不使用鎖,傳 1 表示使用我們自己寫的鎖。

4.1 程式清單

請原諒我很不要臉的把鎖的名字取為 myspinlock,事實上,這就是後面我們要講的自旋鎖,雖然遠遠沒有系統提供的自旋鎖有那麼多機制,那麼複雜。相信自己,你已經知道自旋鎖大概是什麼東西了,下一篇文章我會正式的講。

// myspinlock.c
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>

int uselock = 0;
int lock;
int tickets = 3;

void myspin_lock(int *lock) {
  __asm__ __volatile__ ("1:\n\t"
      "movl $1, %%eax\n\t"
      "lock xchg %%eax, %0\n\t" /* 将 lock 中的值和 eax 進行交換, 相當于 eax = lock, lock = 1 */
      "test %%eax, %%eax\n\t"/* 判斷 eax 是否為 1 */
      "jnz 1b" /* 如果為 1 則跳到标記 1 的地方繼續執行 */
      ::"m"(*lock)
      :"%eax" /* 表示在内聯彙編中我用過了 eax 寄存器,通知一下編譯器做相關處理以免沖突 */
      );
}

void myspin_unlock(int *lock) {
  *lock = 0;
}

void* allen(void* arg) {
  int flag = 1;
  while(flag) {
    if (uselock)
      myspin_lock(&lock);
    int t = tickets;
    usleep(1000*20);// 20ms
    if (t > 0) {
      printf("allen buy a ticket\n");
      --t;
      usleep(1000*20);// 20ms
      tickets = t;
    }
    else flag = 0;
    if (uselock)
      myspin_unlock(&lock);
    usleep(1000*20);// 20ms
  }
  return NULL;
}

void* luffy(void* arg) {
  int flag = 1;
  while(flag) {
    if (uselock)
      myspin_lock(&lock);
    int t = tickets;
    usleep(1000*20);
    if (t > 0) {
      printf("luffy buy a ticket\n");
      --t;
      usleep(1000*20);// 20ms
      tickets = t;
    }
    else flag = 0;
    if (uselock)
      myspin_unlock(&lock);
    usleep(1000*20);// 20ms
  }
  return NULL;
}
int main(int argc, char* argv[]) {
  if (argc >= 2) uselock = atoi(argv[1]);

  pthread_t tid1, tid2;
  pthread_create(&tid1, NULL, allen, NULL);
  pthread_create(&tid2, NULL, luffy, NULL);
  pthread_join(tid1, NULL);
  pthread_join(tid2, NULL);
  return 0;
}      

4.2 編譯和運作

  • 編譯
$ gcc myspinlock.c -o myspinlock -lpthread      
  • 兩種運作結果
83-自己實作互斥鎖

圖1 不使用鎖和使用鎖的結果

5.總結

  • 了解實作鎖的關鍵步驟
  • 了解 xchg 指令以及 lock 字首的作用

繼續閱讀