隱式執行緒和基於語言的執行緒


隱式執行緒

解決這些難題並更好地支援多執行緒應用程式設計的一種方法是將執行緒的建立和管理從應用程式開發人員轉移到編譯器和執行時庫。這種被稱為隱式執行緒的技術是當今的一種流行趨勢。

**隱式執行緒**主要指使用庫或其他語言支援來隱藏執行緒的管理。在 C 語言環境中,最常見的隱式執行緒庫是 OpenMP。

**OpenMP** 是一組編譯器指令以及用於 C、C++ 或 FORTRAN 語言編寫的程式的 API,它為共享記憶體環境中的並行程式設計提供支援。OpenMP 將並行區域識別為可以並行執行的程式碼塊。應用程式開發人員在其程式碼的並行區域插入編譯器指令,這些指令指示 OpenMP 執行時庫並行執行該區域。下面的 C 程式演示了在包含 printf() 語句的並行區域上方的一個編譯器指令

示例

 線上演示

#include <omp.h>
#include <stdio.h>
int main(int argc, char *argv[]){
   /* sequential code */
   #pragma omp parallel{
      printf("I am a parallel region.");
   }
   /* sequential code */
   return 0;
}

輸出

I am a parallel region.

當 OpenMP 遇到指令時

#pragma omp parallel

它建立與系統中處理核心數量相同的執行緒。因此,對於雙核系統,將建立兩個執行緒;對於四核系統,將建立四個執行緒;以此類推。然後,所有執行緒同時執行並行區域。當每個執行緒退出並行區域時,它將被終止。OpenMP 提供了幾個其他指令來並行執行程式碼區域,包括並行化迴圈。

除了提供並行化指令外,OpenMP 還允許開發人員在多個並行級別之間進行選擇。例如,他們可以手動設定執行緒數。它還允許開發人員識別資料是線上程之間共享還是對執行緒私有。OpenMP 可用於 Linux、Windows 和 Mac OS X 系統的多個開源和商業編譯器。

Grand Central Dispatch (GCD)

Grand Central Dispatch (GCD)——蘋果 Mac OS X 和 iOS 作業系統的一項技術——是 C 語言擴充套件、API 和執行時庫的組合,允許應用程式開發人員指定程式碼的哪些部分並行執行。與 OpenMP 一樣,GCD 也管理大部分執行緒細節。它識別 C 和 C++ 語言的擴充套件,稱為塊。塊只是一個自包含的工作單元。它由插入在一對花括號 { } 前面的脫字元 ^ 指定。下面顯示了一個簡單的塊示例:

{
   ˆprintf("This is a block");
}

它透過將塊放入排程佇列中來安排塊的執行時執行。當 GCD 從佇列中移除一個塊時,它會將塊分配給它管理的執行緒池中的可用執行緒。它識別兩種型別的排程佇列:序列和併發。放入序列佇列中的塊按照 FIFO 順序移除。一旦塊從佇列中移除,它必須完成執行才能移除另一個塊。每個程序都有自己的序列佇列(稱為主佇列)。開發者可以建立附加的序列佇列,這些佇列特定於程序。序列佇列對於確保多個任務的順序執行非常有用。放入併發佇列中的塊也按 FIFO 順序移除,但可以一次移除多個塊,從而允許多個塊並行執行。有三個系統範圍的併發排程佇列,它們按優先順序區分:低、預設和高。優先順序表示塊相對重要性的估計。簡單來說,較高優先順序的塊應放在高優先順序排程佇列中。以下程式碼段演示瞭如何獲取預設優先順序的併發佇列,以及使用 dispatch_async() 函式將塊提交到佇列

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch async(queue, ˆ{ printf("This is a block."); });

在內部,GCD 的執行緒池由 POSIX 執行緒組成。GCD 積極管理池,允許執行緒數量根據應用程式需求和系統容量而增長和縮小。

執行緒作為物件

在其他語言中,傳統的面嚮物件語言透過將執行緒作為物件來提供顯式多執行緒支援。在這些型別的語言中,類被編寫為擴充套件執行緒類或實現相應的介面。這種風格類似於 Pthread 方法,因為程式碼是用顯式執行緒管理編寫的。但是,類中資訊的封裝和額外的同步選項會改變任務。

Java 執行緒

Java 提供了一個 Thread 類和一個 Runnable 介面,可以用來實現。每個都需要實現一個公共 void run() 方法,該方法定義執行緒的入口點。一旦物件的例項被分配,執行緒就會透過呼叫該例項上的 start() 方法來啟動。與 Pthreads 一樣,啟動執行緒是非同步的,即執行的時間安排是不確定的。

Python 執行緒

Python 還提供了兩種多執行緒機制。一種方法類似於 Pthread 樣式,其中函式名傳遞給庫方法 thread.start_new_thread()。這種方法非常基礎,並且一旦執行緒啟動就缺乏加入或終止執行緒的靈活性。一種更靈活的技術是使用 threading 模組來定義擴充套件 threading.Thread 的類。與 Java 方法類似,該類應該有一個 run() 方法,該方法提供執行緒的入口點。一旦從該類例項化了一個物件,它就可以被顯式啟動,並在稍後加入。

併發作為語言設計

較新的程式語言透過將併發執行的假設直接構建到語言設計本身來避免競爭條件。例如,Go 將簡單的隱式執行緒機制(goroutine)與通道(一種定義明確的訊息傳遞通訊方式)相結合。Rust 採用與 Pthreads 相同的明確執行緒方法。但是,Rust 具有非常強大的記憶體保護,軟體工程師無需額外的工作。

Goroutine

Go 語言包含一個簡單的隱式執行緒機制:在呼叫之前放置關鍵字 go。新執行緒會傳遞到訊息傳遞通道的關聯。然後,主執行緒呼叫 success := <-messages,這會在通道上執行阻塞掃描。一旦使用者輸入正確的猜測 7,鍵盤監聽執行緒就會寫入通道,從而允許主執行緒繼續。

通道和 goroutine 是 Go 語言的核心元件,該語言是在假設大多數程式將是多執行緒的情況下設計的。這種設計選擇簡化了事件模型,允許語言本身承擔管理執行緒和程式設計的責任。

Rust 併發

近年來建立的另一種語言是 Rust,其併發性是其核心設計功能。以下示例演示瞭如何使用 thread::spawn() 建立一個新執行緒,該執行緒稍後可以透過在其上呼叫 join() 來加入。thread::spawn() 的引數從 || 開始,稱為閉包,可以認為是匿名函式。也就是說,這裡的子執行緒將列印 a 的值。

示例

use std::thread;
fn main() {
   /* Initialize a mutable variable a to 7 */
   let mut a = 7;
   /* Spawn a new thread */
   let child_thread = thread::spawn(move || {
      /* Make the thread sleep for one second, then print a */
      a -= 1;
      println!("a = {}", a)
   });
   /* Change a in the main thread and print it */
   a += 1;
   println!("a = {}", a);
   /* Join the thread and print a again */
   child_thread.join();
}

但是,這段程式碼中有一個微妙的點,它是 Rust 設計的核心。在新執行緒(執行閉包中的程式碼)中,a 變數與這段程式碼其他部分中的 a 不同。它強制執行非常嚴格的記憶體模型(稱為“所有權”),這可以防止多個執行緒訪問相同的記憶體。在此示例中,move 關鍵字表示生成的執行緒將接收 a 的單獨副本以供其自身使用。無論兩個執行緒(主執行緒和子執行緒)的排程如何,它們都不能干擾彼此對 a 的修改,因為它們是不同的副本。這兩個執行緒不可能共享對相同記憶體的訪問。

更新於:2019年10月17日

3K+ 次檢視

啟動你的職業生涯

透過完成課程獲得認證

開始
廣告