Delphi 開發者在處理 I/O 密集型任務時,常會運用平行處理技術提升效能。本文以網頁爬蟲為例,示範如何結合 Delphi 的平行處理能力和 OmniThreadLibrary (OTL) 與 Parallel Programming Library (PPL) 框架,建構高效的爬蟲程式。Pipeline架構是本文的核心,將爬蟲流程拆解為過濾、下載、解析三個階段,各階段以非同步方式執行,大幅縮短處理時間。過濾階段利用 TStringList 儲存已爬取網址,避免重複下載。下載階段使用 THTTPClient 抓取網頁內容,並將結果傳遞至解析階段。解析階段則使用正規表示式提取超連結,實作簡潔高效的 HTML 解析。除了程式碼實作,文章也探討了多執行緒程式設計的常見議題,例如變數捕捉和分享資源的存取,並深入比較了 OTL 和 PPL 兩種框架的特性與應用場景,提供開發者更完整的平行處理技術參考。

深入探索平行處理實踐:以網頁爬蟲為例

在現代軟體開發中,平行處理技術被廣泛應用於提升程式效能,特別是在涉及大量 I/O 操作的任務中,如網頁爬蟲。本文將探討如何利用平行處理技術實作一個高效的網頁爬蟲,並分析其設計與實作細節。

網頁爬蟲的架構設計

網頁爬蟲的核心任務是從指定的起始 URL 開始,抓取網頁內容並解析其中的超連結,然後遞迴地抓取這些連結對應的網頁。為了提高效率,我們採用了Pipeline(Pipeline)架構,將整個處理過程分解為多個階段:

  1. 過濾階段(Filter Stage):檢查抓取的 URL 是否已經被處理過,或是否屬於目標網站。
  2. 下載階段(Downloader Stage):使用 THTTPClient 下載網頁內容。
  3. 解析階段(Parser Stage):解析 HTML 內容,提取其中的超連結。

Pipeline實作

Pipeline的實作根據 TPipeline 類別,該類別管理多個階段的任務並協調它們之間的資料傳遞。每個階段都執行在獨立的任務中,並透過輸入和輸出佇列進行通訊。

FPipeline := TPipeline<string, string>.Create;
FPipeline.Stage('Url filter', Asy_UniqueFilter, FUrlInput, FHtmlParseInput);
for i := 1 to TThread.ProcessorCount do
  FPipeline.Stage<THttpPage>('Downloader', Asy_HttpGet, FHtmlParseInput, FPipeline.Input);
FPipeline.Stage<string>('Html parser', Asy_HtmlParse, FPipeline.Input, FUrlInput);

內容解密:

上述程式碼展示瞭如何建立一個包含多個階段的Pipeline。首先,建立了一個 TPipeline 物件,並增加了過濾、下載和解析三個階段。其中,下載階段被複製多份,以充分利用多核心 CPU 的效能。

過濾階段詳解

過濾階段的主要任務是檢查 URL 是否已經被處理過,或是否屬於目標網站。如果 URL 透過檢查,則將其加入已處理列表並傳遞給下一階段。

procedure TWebSpider.Asy_UniqueFilter(baseUrl: string; inQueue, outQueue: TPipe<string>);
var
  visitedPages: TStringList;
begin
  visitedPages := TStringList.Create;
  try
    // 初始化 visitedPages 並進行 URL 檢查
    inQueue.Process<string>(outQueue,
      procedure (url: string)
      begin
        if url 符合條件 then
        begin
          visitedPages.Add(url);
          outQueue.Write(url);
        end
        else if TInterlocked.Decrement(FPageCount) = 0 then
          NotifyFinished;
      end);
  finally
    FreeAndNil(visitedPages);
  end;
end;

內容解密:

Asy_UniqueFilter 程式中,使用 TStringList 來儲存已處理的 URL,以避免重複處理。對於每個輸入的 URL,會檢查是否屬於目標網站且未被處理過。如果透過檢查,則將其加入 visitedPages 並傳遞給下一階段;否則,減少 FPageCount 的計數,並在計數為零時通知完成。

下載階段詳解

下載階段負責使用 THTTPClient 下載網頁內容。如果下載成功,將傳回的內容封裝成 THttpPage 物件並傳遞給下一階段。

procedure TWebSpider.Asy_HttpGet(inQueue: TThreadedQueue<string>; outQueue: TThreadedQueue<THttpPage>);
var
  httpClient: THTTPClient;
begin
  httpClient := THTTPClient.Create;
  try
    inQueue.Process<THttpPage>(outQueue,
      procedure (const url: string)
      begin
        try
          response := httpClient.Get(url);
          if response.StatusCode div 100 = 2 then
            outQueue.Write(THttpPage.Create(url, response))
          else if TInterlocked.Decrement(FPageCount) = 0 then
            NotifyFinished;
        except
          if TInterlocked.Decrement(FPageCount) = 0 then
            NotifyFinished;
        end;
      end);
  finally
    FreeAndNil(httpClient);
  end;
end;

內容解密:

Asy_HttpGet 程式中,使用 THTTPClient 傳送 HTTP GET 請求以下載網頁內容。如果下載成功且狀態碼為 2xx,則將 URL 和回應內容封裝成 THttpPage 物件並傳遞給下一階段;否則,減少 FPageCount 的計數,並在計數為零時通知完成。

解析階段詳解

解析階段負責解析 HTML 內容,提取其中的超連結。由於 Delphi 標準函式庫中未包含 HTML 解析器,本範例使用正規表示式來簡化實作。

procedure TWebSpider.Asy_HtmlParse(inQueue: TThreadedQueue<THttpPage>; outQueue: TThreadedQueue<string>);
begin
  // 使用正規表示式解析 HTML,提取超連結
end;

內容解密:

Asy_HtmlParse 程式中,透過正規表示式匹配 HTML 中的 <a href="..."><a href='...'> 字串,以提取超連結。這些連結將被傳遞給過濾階段進行進一步處理。

多執行緒程式設計模式探討

在多執行緒程式設計的世界中,Delphi 的 Parallel Programming Library(PPL)框架提供了多種高階的模式來簡化開發者的工作。這些模式包括任務(Task)、非同步/等待(Async/Await)、Join、Future、平行 For(Parallel For)以及管線(Pipeline)。本章將探討這些模式的原理、應用以及相關的最佳實踐。

任務與模式

在 PPL 框架中,任務是基本的執行單元。開發者可以將任務視為一個獨立的執行緒,它們可以被非同步地執行。PPL 提供了豐富的 API 來管理和控制任務,包括任務的建立、執行、取消以及例外處理。

變數捕捉

在多執行緒程式設計中,變數捕捉是一個重要的議題。開發者需要小心處理迴圈變數的捕捉,以避免未預期的行為。PPL 提供了一些機制來幫助開發者正確地捕捉變數。

平行迴圈

PPL 的 Parallel For 模式允許開發者輕鬆地將傳統的迴圈轉換為平行執行的版本。這種模式可以大幅提升程式的效能,但也需要開發者小心處理分享資源的存取。

管線模式

管線模式是一種強大的平行程式設計模式,它允許開發者將一個複雜的任務分解為多個階段,每個階段可以獨立地執行。本章將以一個網頁爬蟲的例子來說明管線模式的應用。

網頁爬蟲實作

網頁爬蟲是一個典型的管線應用範例。我們可以將網頁爬蟲的工作流程分解為多個階段,包括網址過濾、網頁下載、HTML 解析等。每個階段都可以獨立地執行,從而實作平行處理。

procedure TWebSpider.Asy_HtmlParse(
  inQueue: TThreadedQueue<THttpPage>;
  outQueue: TThreadedQueue<string>);
var
  hrefMatch: TRegEx;
  match: TMatch;
  page: THttpPage;
begin
  hrefMatch := TRegEx.Create(
    '<a href=["''](.*?)["''].*?>',
    [roIgnoreCase, roMultiLine]);
  inQueue.Process<string>(outQueue,
    procedure (const page: THttpPage)
    begin
      try
        match := hrefMatch.Match(page.Value.ContentAsString);
        while match.Success do
        begin
          if outQueue.ShutDown then
            break; //while;
          TInterlocked.Increment(FPageCount);
          outQueue.Write(match.Groups[1].Value.Split(['#', '?'])[0]);
          match := match.NextMatch;
        end;
      except
      end;
      FPipeline.Output.Write(page.Key);
      if TInterlocked.Decrement(FPageCount) = 0 then
        NotifyFinished;
    end);
end;

#### 內容解密:

此段程式碼實作了 HTML 解析的非同步處理。首先,建立了一個正規表示式 hrefMatch 用於匹配 HTML 中的超連結。然後,透過 inQueue.Process<string> 方法處理輸入佇列中的每個網頁內容。對於每個網頁內容,使用 hrefMatch.Match 方法找出所有的超連結,並將其寫入輸出佇列中。同時,更新 FPageCount 的計數,以追蹤尚未處理完畢的工作單元數量。當所有工作單元都處理完畢後,呼叫 NotifyFinished 方法通知完成。

OmniThreadLibrary 簡介

OmniThreadLibrary(OTL)是一個開源的多執行緒程式設計框架,它提供了豐富的平行程式設計模式,包括 Async、Join、Future、平行任務(Parallel Task)、背景工作者(Background Worker)以及管線(Pipeline)。本章將探討 OTL 的安裝、使用以及與 PPL 的比較。

更多平行程式設計模式

在前一章中,我們探討了 Delphi 的 PPL 框架所提供的多執行緒程式設計模式。在本章中,我們將繼續探索更多的平行程式設計模式,特別是那些由 OmniThreadLibrary(OTL)提供的模式。

OmniThreadLibrary 的安裝與使用

OTL 是一個強大的開源框架,它為 Delphi 開發者提供了豐富的平行程式設計功能。首先,我們需要了解如何安裝和使用 OTL。

Blocking Collections

OTL 提供了 blocking collections,這是一種特殊的集合類別,它允許執行緒安全地存取和操作資料。我們將探討如何使用 blocking collections 來簡化多執行緒程式設計。

Async、Join 和 Future

OTL 實作了多種常見的平行程式設計模式,包括 Async、Join 和 Future。我們將比較這些模式在 OTL 和 PPL 中的實作差異,並探討如何在實際應用中使用它們。

平行任務和背景工作者

OTL 的平行任務和背景工作者模式為開發者提供了更多的彈性來管理多執行緒任務。我們將學習如何使用這些模式來處理複雜的多執行緒應用程式。

管線模式在 OTL 中的實作

OTL 的管線模式提供了一種更靈活和更易於使用的方式來實作複雜的多階段處理流程。我們將探討 OTL 管線模式的優點和應用場景。

Map 和 Timed Task

最後,我們將介紹 OTL 中的 Map 和 Timed Task 功能,這些功能可以進一步簡化多執行緒程式設計。

使用OmniThreadLibrary進行平行模式開發

OmniThreadLibrary(OTL)是一個為Delphi設計的多執行緒函式庫,主要由本文作者編寫。它最初是為Delphi 2007版本開發的,並且至今仍支援該版本。OTL在多執行緒程式設計領域提供了豐富的功能,包括平行模式的實作。

OmniThreadLibrary簡介

OTL採用OpenBSD授權協定,允許在商業應用中免費使用。它支援VCL、控制檯和服務操作於Windows平台,但不支援其他作業系統。

安裝OTL最簡單的方式是透過Delphi的GetIt模組進行安裝。另一種方式是從GitHub上下載最新版本或克隆其儲存函式庫,然後將主OTL資料夾新增至搜尋路徑中。

多執行緒支援層級

OTL提供了兩種不同層級的多執行緒支援:低階多執行緒和高階多執行緒。

  • 低階多執行緒是對TThread的封裝,增加了諸如執行緒池、通訊、無鎖集合(堆積疊和佇列)和取消機制等功能。
  • 高階多執行緒則將常見的多執行緒問題包裝成平行模式,包括Async、Future、Join、ParallelTask、For、ForEach、Background worker、Pipeline、Fork/Join、Map和Timer等。

阻塞式集合

在探討OTL的平行模式之前,我們先來瞭解一個被多種模式使用的資料結構——阻塞式集合(Blocking Collection)。它是一個執行緒安全的佇列,支援多個同時讀寫的操作。

IOmniBlockingCollection介面

IOmniBlockingCollection介面定義在OtlCollections單元中,其定義如下:

IOmniBlockingCollection = interface
  procedure Add(const value: TOmniValue);
  procedure CompleteAdding;
  function GetEnumerator: IOmniValueEnumerator;
  function IsCompleted: boolean;
  function IsEmpty: boolean;
  function IsFinalized: boolean;
  function Next: TOmniValue;
  procedure ReraiseExceptions(enable: boolean = true);
  procedure SetThrottling(highWatermark, lowWatermark: integer);
  function Take(var value: TOmniValue): boolean;
  function TryAdd(const value: TOmniValue): boolean;
  function TryTake(var value: TOmniValue; timeout_ms: cardinal = 0): boolean;
  property ContainerSubject: TOmniContainerSubject read GetContainerSubject;
  property Count: integer read GetApproxCount;
end;

阻塞式集合的使用

阻塞式集合中的元素型別為TOmniValue,能夠儲存任何Delphi型別。它不依賴於OTL的任務框架,可以與Delphi的TThread或PPL任務一起使用。

var
  bc: IOmniBlockingCollection; // 在執行緒間共用的阻塞式集合

// 執行緒1:
var
  data: TOmniValue;
while GenerateData(data) do
  bc.Add(data);
bc.CompleteAdding;

// 執行緒2:
for var data in bc do
  ProcessData(data);

#### 內容解密:

  • IOmniBlockingCollection介面提供了一系列方法來操作阻塞式集合,如AddCompleteAddingTakeTryTake等。
  • TOmniValue是一種能夠儲存任意Delphi型別的通用型別。
  • 在多執行緒環境中,阻塞式集合能夠簡化執行緒之間的通訊和資料交換。

平行模式的比較

本章將探討OTL中的重要平行模式,並與PPL(Parallel Programming Library)中的相應解決方案進行比較。

Async和Async/Await

Async和Async/Await模式用於在背景執行緒中執行程式碼,並在執行完成時(可選)通知呼叫者。

Future

Future模式在背景執行緒中執行計算並傳回結果。

Join

Join模式用於平行執行多個任務。

ParallelTask

ParallelTask模式以多個平行副本執行相同的程式碼。

For和ForEach

For和ForEach模式提供了兩種平行執行for迴圈的方式,一種快速,一種功能強大。

Background worker

Background worker模式在背景執行資料處理伺服器,處理可以執行在多個執行緒上。

Pipeline

Pipeline模式在多個背景執行緒上執行多階段處理程式。

Fork/Join

Fork/Join模式是一個框架,用於解決分治(遞迴)演算法。

Map

Map模式在多個平行執行緒上對資料陣列執行資料轉換函式。

Timer

Timer模式實作了一個在特定間隔被喚醒的背景工作執行緒。