Openmp Runtime 庫函式彙總(下)——深入剖析鎖?原理與實現

一無是處的研究僧發表於2023-01-16

Openmp Runtime 庫函式彙總(下)——深入剖析鎖?原理與實現

前言

在本篇文章當中主要給大家介紹一下 OpenMP 當中經常使用到的鎖並且仔細分析它其中的內部原理!在 OpenMP 當中主要有兩種型別的鎖,一個是 omp_lock_t 另外一個是 omp_nest_lock_t,這兩個鎖的主要區別就是後者是一個可重入鎖,所謂可衝入鎖就是一旦一個執行緒已經拿到這個鎖了,那麼它下一次想要拿這個鎖的就是就不會阻塞,但是如果是 omp_lock_t 不管一個執行緒是否拿到了鎖,只要當前鎖沒有釋放,不管哪一個執行緒都不能夠拿到這個鎖。在後問當中將有仔細的例子來解釋這一點。本篇文章是基於 GNU OpenMP Runtime Library !

深入分析 omp_lock_t

這是 OpenMP 標頭檔案給我們提供的一個結構體,我們來看一下它的定義:

typedef struct
{
  unsigned char _x[4] 
    __attribute__((__aligned__(4)));
} omp_lock_t;

事實上這個結構體並沒有什麼特別的就是佔 4 個位元組,我們甚至可以認為他就是一個 4 位元組的 int 的型別的變數,只不過使用方式有所差異。與這個結構體相關的主要有以下幾個函式:

  • omp_init_lock,這個函式的主要功能是初始化 omp_lock_t 物件的,當我們初始化之後,這個鎖就處於一個沒有上鎖的狀態,他的函式原型如下所示:
void omp_init_lock(omp_lock_t *lock);
  • omp_set_lock,在呼叫這個函式之前一定要先呼叫函式 omp_init_lock 將 omp_lock_t 進行初始化,直到這個鎖被釋放之前這個執行緒會被一直阻塞。如果這個鎖被當前執行緒已經獲取過了,那麼將會造成一個死鎖,這就是上面提到了鎖不能夠重入的問題,而我們在後面將要分析的鎖 omp_nest_lock_t 是能夠進行重入的,即使當前執行緒已經獲取到了這個鎖,也不會造成死鎖而是會重新獲得鎖。這個函式的函式原型如下所示:
void omp_set_lock(omp_lock_t *lock);
  • omp_test_lock,這個函式的主要作用也是用於獲取鎖,但是這個函式可能會失敗,如果失敗就會返回 false 成功就會返回 true,與函式 omp_set_lock 不同的是,這個函式並不會導致執行緒被阻塞,如果獲取鎖成功他就會立即返回 true,如果失敗就會立即返回 false 。它的函式原型如下所示:
int omp_test_lock(omp_lock_t *lock); 
  • omp_unset_lock,這個函式和上面的函式對應,這個函式的主要作用就是用於解鎖,在我們呼叫這個函式之前,必須要使用 omp_set_lock 或者 omp_test_lock 獲取鎖,它的函式原型如下:
void omp_unset_lock(omp_lock_t *lock);
  • omp_destroy_lock,這個方法主要是對鎖進行回收處理,但是對於這個鎖來說是沒有用的,我們在後文分析他的具體的實現的時候會發現這是一個空函式。

我們現在使用一個例子來具體的體驗一下上面的函式:


#include <stdio.h>
#include <omp.h>

int main()
{
   omp_lock_t lock;
   // 對鎖進行初始化操作
   omp_init_lock(&lock);
   int data = 0;
#pragma omp parallel num_threads(16) shared(lock, data) default(none)
   {
      // 進行加鎖處理 同一個時刻只能夠有一個執行緒能夠獲取鎖
      omp_set_lock(&lock);
      data++;
      // 解鎖處理 執行緒在出臨界區之前需要解鎖 好讓其他執行緒能夠進入臨界區
      omp_unset_lock(&lock);
   }
   omp_destroy_lock(&lock);
   printf("data = %d\n", data);
   return 0;
}

在上面的函式我們定義了一個 omp_lock_t 鎖,並且在並行域內啟動了 16 個執行緒去執行 data ++ 的操作,因為是多執行緒環境,因此我們需要將上面的操作進行加鎖處理。

omp_lock_t 原始碼分析

  • omp_init_lock,對於這個函式來說最終在 OpenMP 動態庫內部會呼叫下面的函式:
typedef int gomp_mutex_t;
static inline void
gomp_mutex_init (gomp_mutex_t *mutex)
{
  *mutex = 0;
}

從上面的函式我們可以知道這個函式的作用就是將我們定義的 4 個位元組的鎖賦值為0,這就是鎖的初始化,其實很簡單。

  • omp_set_lock,這個函式最終會呼叫 OpenMP 內部的一個函式,具體如下所示:
static inline void
gomp_mutex_lock (gomp_mutex_t *mutex)
{
  int oldval = 0;
  if (!__atomic_compare_exchange_n (mutex, &oldval, 1, false,
				    MEMMODEL_ACQUIRE, MEMMODEL_RELAXED))
    gomp_mutex_lock_slow (mutex, oldval);
}

在上面的函式當中執行緒首先會呼叫 __atomic_compare_exchange_n 將鎖的值由 0 變成 1,還記得我們在前面對鎖進行初始化的時候將鎖的值變成0了嗎?

我們首先需要了解一下 __atomic_compare_exchange_n ,這個是 gcc 內嵌的一個函式,在這裡我們只關注前面三個引數,後面三個引數與記憶體模型有關,這並不是我們本篇文章的重點,他的主要功能是檢視 mutex 指向的地址的值等不等於 oldval ,如果等於則將這個值變成 1,這一整個操作能夠保證原子性,如成功將 mutex 指向的值變成 1 的話,那麼這個函式就返回 true 否則返回 false 對應 C 語言的資料就是 1 和 0 。如果 oldval 的值不等於 mutex 所指向的值,那麼這個函式就會將這個值寫入 oldval 。

如果這個操作不成功那麼就會呼叫 gomp_mutex_lock_slow 函式這個函式的主要作用就是如果使用不能夠使用原子指令獲取鎖的話,那麼就需要進入核心態,將這個執行緒掛起。在這個函式的內部還會測試是否能夠透過源自操作獲取鎖,因為可能在我們呼叫 gomp_mutex_lock_slow 這個函式的時候可能有其他執行緒釋放鎖了。如果仍然不能夠成功的話,那麼就會真正的將這個執行緒掛起不會浪費 CPU 資源,gomp_mutex_lock_slow 函式具體如下:

void
gomp_mutex_lock_slow (gomp_mutex_t *mutex, int oldval)
{
  /* First loop spins a while.  */
  // 先自旋 如果自旋一段時間還沒有獲取鎖 那就將執行緒刮掛起
  while (oldval == 1)
    {
      if (do_spin (mutex, 1))
	{
	  /* Spin timeout, nothing changed.  Set waiting flag.  */
	  oldval = __atomic_exchange_n (mutex, -1, MEMMODEL_ACQUIRE);
    // 如果獲得? 就返回
	  if (oldval == 0)
	    return;
    // 如果沒有獲得? 那麼就將執行緒颳起
	  futex_wait (mutex, -1);
    // 這裡是當掛起的執行緒被喚醒之後的操作 也有可能是 futex_wait 沒有成功
	  break;
	}
      else
	{
	  /* Something changed.  If now unlocked, we're good to go.  */
	  oldval = 0;
	  if (__atomic_compare_exchange_n (mutex, &oldval, 1, false,
					   MEMMODEL_ACQUIRE, MEMMODEL_RELAXED))
	    return;
	}
    }

  /* Second loop waits until mutex is unlocked.  We always exit this
     loop with wait flag set, so next unlock will awaken a thread.  */
  while ((oldval = __atomic_exchange_n (mutex, -1, MEMMODEL_ACQUIRE)))
    do_wait (mutex, -1);
}

在上面的函式當中有三個依賴函式,他們的原始碼如下所示:


static inline void
futex_wait (int *addr, int val)
{
  // 在這裡進行系統呼叫,將執行緒掛起 
  int err = syscall (SYS_futex, addr, gomp_futex_wait, val, NULL);
  if (__builtin_expect (err < 0 && errno == ENOSYS, 0))
    {
      gomp_futex_wait &= ~FUTEX_PRIVATE_FLAG;
      gomp_futex_wake &= ~FUTEX_PRIVATE_FLAG;
    // 在這裡進行系統呼叫,將執行緒掛起 
      syscall (SYS_futex, addr, gomp_futex_wait, val, NULL);
    }
}

static inline void do_wait (int *addr, int val)
{
  if (do_spin (addr, val))
    futex_wait (addr, val);
}

static inline int do_spin (int *addr, int val)
{
  unsigned long long i, count = gomp_spin_count_var;

  if (__builtin_expect (__atomic_load_n (&gomp_managed_threads,
                                         MEMMODEL_RELAXED)
                        > gomp_available_cpus, 0))
    count = gomp_throttled_spin_count_var;
  for (i = 0; i < count; i++)
    if (__builtin_expect (__atomic_load_n (addr, MEMMODEL_RELAXED) != val, 0))
      return 0;
    else
      cpu_relax ();
  return 1;
}

static inline void
cpu_relax (void)
{
  __asm volatile ("" : : : "memory");
}

如果大家對具體的內部實現非常感興趣可以仔細研讀上面的程式碼,如果從 0 開始解釋上面的程式碼比較麻煩,這裡就不做詳細的分析了,簡要做一下概括:

  • 在鎖的設計當中有一個非常重要的原則:一個執行緒最好不要進入核心態被掛起,如果能夠在使用者態最好在使用者態使用原子指令獲取鎖,這是因為進入核心態是一個非常耗時的事情相比起原子指令來說。

  • 鎖(就是我們在前面討論的一個 4 個位元組的 int 型別的值)有以下三個值:

    • -1 表示現在有執行緒被掛起了。
    • 0 表示現在是一個無鎖狀態,這個狀態就表示鎖的競爭比較激烈。
    • 1 表示這個執行緒正在被一個執行緒用一個原子指令——比較並交換(CAS)獲得了,這個狀態表示現在鎖的競爭比較輕。
  • _atomic_exchange_n (mutex, -1, MEMMODEL_ACQUIRE); ,這個函式也是 gcc 內嵌的一個函式,這個函式的主要作用就是將 mutex 的值變成 -1,然後將 mutex 指向的地址的原來的值返回。

  • __atomic_load_n (addr, MEMMODEL_RELAXED),這個函式的作用主要作用是原子的載入 addr 指向的資料。

  • futex_wait 函式的功能是將執行緒掛起,將執行緒掛起的系統呼叫為 futex ,大家可以使用命令 man futex 去檢視 futex 的手冊。

  • do_spin 函式的功能是進行一定次數的原子操作(自旋),如果超過這個次數就表示現在這個鎖的競爭比較激烈為了更好的使用 CPU 的計算資源可以將這個執行緒掛起。如果在自旋(spin)的時候發現鎖的值等於 val 那麼就返回 0 ,如果在進行 count 次操作之後我們還沒有發現鎖的值變成 val 那麼就返回 1 ,這就表示鎖的競爭比較激烈。

  • 可能你會疑惑在函式 gomp_mutex_lock_slow 的最後一部分為什麼要用 while 迴圈,這是因為 do_wait 函式不一定會將執行緒掛起,這個和 futex 系統呼叫有關,感興趣的同學可以去看一下 futex 的檔案,就瞭解這麼設計的原因了。

  • 在上面的原始碼當中有兩個 OpenMP 內部全域性變數,gomp_throttled_spin_count_var 和 gomp_spin_count_var 用於表示自旋的次數,這個也是 OpenMP 自己進行設計的這個值和環境變數 OMP_WAIT_POLICY 也有關係,具體的數值也是設計團隊的經驗值,在這裡就不介紹這一部分的原始碼了。

其實上面的加鎖過程是非常複雜的,大家可以自己自行去好好分析一下這其中的設計,其實是非常值得學習的,上面的加鎖程式碼貫徹的宗旨就是:能不進核心態就別進核心態。

  • omp_unset_lock,這個函式的主要功能就是解鎖了,我們再來看一下他的原始碼設計。這個函式最終呼叫的 OpenMP 內部的函式為 gomp_mutex_unlock ,其原始碼如下所示:
static inline void
gomp_mutex_unlock (gomp_mutex_t *mutex)
{
  int wait = __atomic_exchange_n (mutex, 0, MEMMODEL_RELEASE);
  if (__builtin_expect (wait < 0, 0))
    gomp_mutex_unlock_slow (mutex);
}

在上面的函式當中呼叫一個函式 gomp_mutex_unlock_slow ,其原始碼如下:

void
gomp_mutex_unlock_slow (gomp_mutex_t *mutex)
{
  // 表示喚醒 1 個執行緒
  futex_wake (mutex, 1);
}

static inline void
futex_wake (int *addr, int count)
{
  int err = syscall (SYS_futex, addr, gomp_futex_wake, count);
  if (__builtin_expect (err < 0 && errno == ENOSYS, 0))
    {
      gomp_futex_wait &= ~FUTEX_PRIVATE_FLAG;
      gomp_futex_wake &= ~FUTEX_PRIVATE_FLAG;
      syscall (SYS_futex, addr, gomp_futex_wake, count);
    }
}

在函式 gomp_mutex_unlock 當中首先呼叫原子操作 __atomic_exchange_n,將鎖的值變成 0 也就是無鎖狀態,這個其實是方便被喚醒的執行緒能夠不被阻塞(關於這一點大家可以好好去分分析 gomp_mutex_lock_slow 最後的 while 迴圈,就能夠理解其中的深意了),然後如果 mutex 原來的值(這個值會被賦值給 wait )小於 0 ,我們在前面已經談到過,這個值只能是 -1,這就表示之前有執行緒進入核心態被掛起了,因此這個執行緒需要喚醒之前被阻塞的執行緒,好讓他們能夠繼續執行。喚醒之前執行緒的函式就是 gomp_mutex_unlock_slow,在這個函式內部會呼叫 futex_wake 去真正的喚醒一個之前被鎖阻塞的執行緒。

  • omp_test_lock,這個函式主要是使用原子指令看是否能夠獲取鎖,而不嘗試進入核心,如果成功獲取鎖返回 1 ,否則返回 0 。這個函式在 OpenMP 內部會最終呼叫下面的函式。

int
gomp_test_lock_30 (omp_lock_t *lock)
{
  int oldval = 0;

  return __atomic_compare_exchange_n (lock, &oldval, 1, false,
				      MEMMODEL_ACQUIRE, MEMMODEL_RELAXED);
}

從上面原始碼來看這函式就是做了原子的比較並交換操作,如果成功就是獲取鎖並且返回值為 1 ,反之沒有獲取鎖那麼就不成功返回值就是 0 。

總的說來上面的鎖的設計主要有一下的兩個方向:

  • Fast path : 能夠在使用者態解決的事兒就別進核心態,只要能夠透過原子指令獲取鎖,那麼就使用原子指令,因為進入核心態是一件非常耗時的事情。
  • Slow path : 當經歷過一定數目的自旋操作之後發現還是不能夠獲得鎖,那麼就能夠判斷此時鎖的競爭比較激烈,如果這個時候還不將執行緒掛起的話,那麼這個執行緒好就會一直消耗 CPU ,因此這個時候我們應該要進入核心態將執行緒掛起以節省 CPU 的計算資源。

雜談:

  • 其實上面的鎖的設計是非公平的我們可以看到在 gomp_mutex_unlock 函式當中,他是直接將 mutex 和 0 進行交換,根據前面的分析現在的鎖處於一個沒有執行緒獲取的狀態,如果這個時候有其他執行緒進來那麼就可以直接透過原子操作獲取鎖了,而這個執行緒如果將之前被阻塞的執行緒喚醒,那麼這個被喚醒的執行緒就會處於 gomp_mutex_lock_slow 最後的那個迴圈當中,如果這個時候 mutex 的值不等於 0 (因為有新來的執行緒透過原子指令將 mutex 的值由 0 變成 1 了),那麼這個執行緒將繼續阻塞,而且會將 mutex 的值設定成 -1。

  • 上面的鎖設計加鎖和解鎖的互動情況是非常複雜的,因為需要確保加鎖和解鎖的操作不會造成死鎖,大家可以使用各種順序去想象一下程式碼的執行就能夠發現其中的巧妙之處了。

  • 不要將獲取鎖和執行緒的喚醒關聯起來,執行緒被喚醒不一定獲得鎖,而且 futex 系統呼叫存在虛假喚醒的可能(關於這一點可以檢視 futex 的手冊)。

深入分析 omp_nest_lock_t

在介紹可重入鎖(omp_nest_lock_t)之前,我們首先來介紹一個需求,看看之前的鎖能不能夠滿足這個需求。


#include <stdio.h>
#include <omp.h>

void echo(int n, omp_nest_lock_t* lock, int * s)
{
   if (n > 5)
   {
      omp_set_nest_lock(lock);
      // 在這裡進行遞迴呼叫 因為在上一行程式碼已經獲取鎖了 遞迴呼叫還需要獲取鎖
      // omp_lock_t 是不能滿足這個要求的 而 omp_nest_lock_t 能
      echo(n - 1, lock, s);
      *s += 1;
      omp_unset_nest_lock(lock);
   }
   else
   {
      omp_set_nest_lock(lock);
      *s += n;
      omp_unset_nest_lock(lock);
   }
}

int main()
{
   int n = 100;
   int s = 0;
   omp_nest_lock_t lock;
   omp_init_nest_lock(&lock);
   echo(n, &lock, &s);
   printf("s = %d\n", s);
   omp_destroy_nest_lock(&lock);

   printf("%ld\n", sizeof (omp_nest_lock_t));
   return 0;
}

在上面的程式碼當中會呼叫函式 echo,而在 echo 函式當中會進行遞迴呼叫,但是在遞迴呼叫之前執行緒已經獲取鎖了,如果進行遞迴呼叫的話,因為之前這個鎖已經被獲取了,因此如果再獲取鎖的話就會產生死鎖,因為執行緒已經被獲取了。

如果要解決上面的問題就需要使用的可重入鎖了,所謂可重入鎖就是當一個執行緒獲取鎖之後,如果這個執行緒還想獲取鎖他仍然能夠獲取到鎖,而不會產生死鎖的現象。如果將上面的鎖改成可重入鎖 omp_nest_lock_t 那麼程式就會正常執行完成,而不會產生死鎖。


#include <stdio.h>
#include <omp.h>

void echo(int n, omp_nest_lock_t* lock, int * s)
{
   if (n > 5)
   {
      omp_set_nest_lock(lock);
      echo(n - 1, lock, s);
      *s += 1;
      omp_unset_nest_lock(lock);
   }
   else
   {
      omp_set_nest_lock(lock);
      *s += n;
      omp_unset_nest_lock(lock);
   }
}

int main()
{
   int n = 100;
   int s = 0;
   omp_nest_lock_t lock;
   omp_init_nest_lock(&lock);
   echo(n, &lock, &s);
   printf("s = %d\n", s);
   omp_destroy_nest_lock(&lock);
   return 0;
}

上面的各個函式的使用方法和之前的 omp_lock_t 的使用方法是一樣的:

  • 鎖的初始化 —— init 。
  • 加鎖 —— set_lock。
  • 解鎖 —— unset_lock 。
  • 鎖的釋放 —— destroy 。

我們現在來分析一下 omp_nest_lock_t 的實現原理,首先需要了解的是 omp_nest_lock_t 這個結構體一共佔用 16 個位元組,這 16個位元組的欄位如下所示:

typedef struct { 
  int lock; 
  int count; 
  void *owner; 
} omp_nest_lock_t;

上面的結構體一共佔 16 個位元組現在我們來仔細分析以上面的三個欄位的含義:

  • lock,這個欄位和上面談到的 omp_lock_t 是一樣的作用都是佔用 4 個位元組,主要是用於原子操作。
  • count,在前面我們已經談到了 omp_nest_lock_t 同一個執行緒在獲取鎖之後仍然能夠獲取鎖,因此這個欄位的含義就是表示執行緒獲取了多少次鎖。
  • owner,這個欄位的含義就比較簡單了,我們需要記錄是哪個執行緒獲取的鎖,這個欄位的意義就是執行獲取到鎖的執行緒。
  • 這裡大家只需要稍微瞭解一下這幾個欄位的含義,在後面分析原始碼的時候大家就能夠體會到這其中設計的精妙之處了。

omp_nest_lock_t 原始碼分析

  • omp_init_nest_lock,這個函式的作用主要是進行初始化操作,將 omp_nest_lock_t 中的資料中所有的位元全部變成 0 。在 OpenMP 內部中最終會呼叫下面的函式:
void
gomp_init_nest_lock_30 (omp_nest_lock_t *lock)
{
  // 字元 '\0' 對應的數值就是 0 這個就是將 lock 指向的 16 個位元組全部清零
  memset (lock, '\0', sizeof (*lock));
}
  • omp_set_nest_lock,這個函式的主要作用就是加鎖,在 OpenMP 內部最終呼叫的函式如下所示:
void
gomp_set_nest_lock_30 (omp_nest_lock_t *lock)
{
  // 首先獲取當前執行緒的指標
  void *me = gomp_icv (true);
	// 如果鎖的所有者不是當前執行緒,那麼就呼叫函式 gomp_mutex_lock 去獲取鎖
  // 這裡的 gomp_mutex_lock 函式和我們之前在 omp_lock_t 當中所分析的函式
  // 是同一個函式
  if (lock->owner != me)
    {
      gomp_mutex_lock (&lock->lock);
    	// 當獲取鎖成功之後將當前執行緒的所有者設定成自己
      lock->owner = me;
    }
	// 因為獲取鎖了所以需要將當前執行緒獲取鎖的次數加一
  lock->count++;
}

在上面的程式當中主要的流程如下:

  • 如果當前鎖的所有者是自己,也就是說如果當前執行緒之前已經獲取到鎖了,那麼久直接將 count 進行加一操作。

  • 如果當執行緒還還沒有獲取到鎖,那麼就使用 gomp_mutex_lock 去獲取鎖,如果當前已經有執行緒獲取到鎖了,那麼執行緒就會被掛起。

  • omp_unset_nest_lock

void
gomp_unset_nest_lock_30 (omp_nest_lock_t *lock)
{
  if (--lock->count == 0)
    {
      lock->owner = NULL;
      gomp_mutex_unlock (&lock->lock);
    }
}

在由了 omp_lock_t 的分析基礎之後上面的程式碼也是比較容易分析的,首先會將 count 的值減去一,如果 count 的值變成 0,那麼就可以進行解鎖操作,將鎖的所有者變成 NULL ,然後使用 gomp_mutex_unlock 函式解鎖,喚醒之前被阻塞的執行緒。

  • omp_test_nest_lock
int
gomp_test_nest_lock_30 (omp_nest_lock_t *lock)
{
  void *me = gomp_icv (true);
  int oldval;

  if (lock->owner == me)
    return ++lock->count;

  oldval = 0;
  if (__atomic_compare_exchange_n (&lock->lock, &oldval, 1, false,
				   MEMMODEL_ACQUIRE, MEMMODEL_RELAXED))
    {
      lock->owner = me;
      lock->count = 1;
      return 1;
    }

  return 0;
}

這個不進入核心態獲取鎖的程式碼也比較容易,首先分析當前鎖的擁有者是不是當前執行緒,如果是那麼就將 count 的值加一,否則就使用原子指令看看能不能獲取鎖,如果能夠獲取鎖就返回 1 ,否則就返回 0 。

原始碼函式名稱不同的原因揭秘

在上面的原始碼分析當中我們可以看到我們真正分析的程式碼並不是在 omp.h 的標頭檔案當中定義的,這是因為在 OpenMP 內部做了很多的重新命名處理:

# define gomp_init_lock_30 omp_init_lock
# define gomp_destroy_lock_30 omp_destroy_lock
# define gomp_set_lock_30 omp_set_lock
# define gomp_unset_lock_30 omp_unset_lock
# define gomp_test_lock_30 omp_test_lock
# define gomp_init_nest_lock_30 omp_init_nest_lock
# define gomp_destroy_nest_lock_30 omp_destroy_nest_lock
# define gomp_set_nest_lock_30 omp_set_nest_lock
# define gomp_unset_nest_lock_30 omp_unset_nest_lock
# define gomp_test_nest_lock_30 omp_test_nest_lock

在 OponMP 當中一個跟鎖非常重要的檔案就是 lock.c,現在檢視一下他的原始碼,你的疑惑就能夠揭開了:

#include <string.h>
#include "libgomp.h"

/* The internal gomp_mutex_t and the external non-recursive omp_lock_t
   have the same form.  Re-use it.  */

void
gomp_init_lock_30 (omp_lock_t *lock)
{
  gomp_mutex_init (lock);
}

void
gomp_destroy_lock_30 (omp_lock_t *lock)
{
  gomp_mutex_destroy (lock);
}

void
gomp_set_lock_30 (omp_lock_t *lock)
{
  gomp_mutex_lock (lock);
}

void
gomp_unset_lock_30 (omp_lock_t *lock)
{
  gomp_mutex_unlock (lock);
}

int
gomp_test_lock_30 (omp_lock_t *lock)
{
  int oldval = 0;

  return __atomic_compare_exchange_n (lock, &oldval, 1, false,
				      MEMMODEL_ACQUIRE, MEMMODEL_RELAXED);
}

void
gomp_init_nest_lock_30 (omp_nest_lock_t *lock)
{
  memset (lock, '\0', sizeof (*lock));
}

void
gomp_destroy_nest_lock_30 (omp_nest_lock_t *lock)
{
}

void
gomp_set_nest_lock_30 (omp_nest_lock_t *lock)
{
  void *me = gomp_icv (true);

  if (lock->owner != me)
    {
      gomp_mutex_lock (&lock->lock);
      lock->owner = me;
    }

  lock->count++;
}

void
gomp_unset_nest_lock_30 (omp_nest_lock_t *lock)
{
  if (--lock->count == 0)
    {
      lock->owner = NULL;
      gomp_mutex_unlock (&lock->lock);
    }
}

int
gomp_test_nest_lock_30 (omp_nest_lock_t *lock)
{
  void *me = gomp_icv (true);
  int oldval;

  if (lock->owner == me)
    return ++lock->count;

  oldval = 0;
  if (__atomic_compare_exchange_n (&lock->lock, &oldval, 1, false,
				   MEMMODEL_ACQUIRE, MEMMODEL_RELAXED))
    {
      lock->owner = me;
      lock->count = 1;
      return 1;
    }

  return 0;
}

總結

在本篇文章當中主要給大家分析了 OpenMP 當中兩種主要的鎖的實現,分別是 omp_lock_t 和 omp_nest_lock_t,一種是簡單的鎖實現,另外一種是可重入鎖的實現。其實 critical 子句在 OpenMP 內部的也是利用上面的鎖實現的。整個鎖的實現還是非常複雜的,裡面有很多耐人尋味的細節,這些程式碼真的很值得一讀,看看能操刀 OpenMP Runtime Library 這些程式設計大師的作品,真的很有收穫。

更多精彩內容合集可訪問專案:https://github.com/Chang-LeHung/CSCore

關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演演算法與資料結構)知識。

相關文章