---
# System prepended metadata

title: 平行程式 6C~6D Communication Routines and Parallel Function Code
tags: [平行程式]

---

教材:10710周志遠教授平行程式
https://www.youtube.com/playlist?list=PLS0SUwlYe8cxqw70UHOE5n4Lm-mXFXbZT

20250825 筆記
內容可能有錯僅供參考

6C~6D Communication Routines and Parallel Function Code

今日大綱
MPI-IO 與共享記憶體
平行 I/O 的重要性與挑戰
MPI-IO 的解決方案
MPI-IO 讀寫操作的類型
共享記憶體程式設計
Process 與 Thread 的差異
為什麼單機平行程式偏好 Threads
Pthreads 是什麼

### **MPI-IO 與共享記憶體**

```
Examples: Divide MPI tasks into two group

int rank, numtasks; MPI_Group orig_group, new_group; MPI_Comm new_comm;

MPI_Init(); MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm_size(MPI_COMM_WORLD, &numtasks);

/* Extract the original group handle */ MPI_Comm_group(MPI_COMM_WORLD, &orig_group);

/* Divide tasks into two distinct groups based upon rank */ int rank1[4] = {0,1,2,3}, rank2[4] = {5,6,7,8}; if (rank < numtasks/2) MPI_Group_incl(orig_group, numtasks/2, rank1, &new_group); else MPI_Group_incl(orig_group, numtasks/2, rank2, &new_group);

/* Create new communicator & Broadcast within the new group */ MPI_Comm_create(MPI_COMM_WORLD, new_group, &new_comm);

MPI_Barrier(new_comm);
```


#### **MPI 群組與變數理解**

- **群組建立的困惑**: 在MPI中，大家可能會對`new group`的建立感到困惑。
    - `rank1` (包含0,1,2,3) 和 `rank2` (包含5,6,7,8) 在執行 `defil` 時的行為是**不一致的**。
    - 這兩個函數碼執行後會**建立兩個不同的新群組**。
    - 由於這是**集體操作 (collective code)**，原群組中的所有行程 (processes) 都必須呼叫此函數碼。
    - 雖然所有行程都呼叫同一函數，但其結果可以同時建立兩個群組。
    - 每個 MPI 測試取得的 `new group` 取決於行程本身。前方的行程獲得的是第一個群組的項目，後方的行程獲得的是第二個群組的。
    - 對於未出現在這些群組中的行程（例如數字4），其 `new group` 是**沒有意義的**，因為它根本不屬於任何群組。
- **變數的獨立性**: 在編寫 MPI 程式時，即使變數名稱相同，但在計算過程中，每個行程取得的值以及 API 回傳的值都**可能不同**。
    - 這是因為它們各自擁有**不同的記憶體空間 (memory space)**。
    - 雖然使用相同的變數名稱可以讓程式碼更簡潔，但必須**注意避免錯誤覆寫**，並理解這些看似相同的變數實際上具有不同的值。

#### **平行 I/O 的重要性與挑戰**

- **I/O 在平行計算中的關鍵性**: I/O 是非常重要的部分，尤其與行程搭配時更是如此。
- **速度瓶頸**: 處理器速度、記憶體速度以及磁碟存取速度之間存在**巨大的差異**。
    - 如果只專注於處理而忽視檔案存取，程式很多時候會被卡住。
- **計算與儲存的分離**: 在傳統高效能計算系統中，計算資源（如超級電腦）和儲存資源（如分散式檔案系統）甚至是由**兩個不同的系統**提供。
    - 分散式檔案系統是一個獨立的電腦群組和軟體，專門為前端的超級電腦提供資料。
- **平行檔案系統 (Parallel File System, PFS)**:
    - PFS 擁有**大量的儲存節點 (storage nodes)**。
    - 每個儲存節點都有**獨立的網路連結通道**連接到計算節點 (compute nodes)。
    - 這使得計算節點和儲存節點可以**一對一配對**進行資料傳輸。
    - 更多的平行行程、更多的儲存節點和計算節點，將會帶來**更大的聚合網路頻寬 (aggregate networking)**，從而**加快讀取速度**。這就是所謂的 **平行 I/O (parallel IO)**。

#### **未使用 MPI-IO 的問題**

- **直覺式 `fopen`/`fread` 的限制**: 若不透過 `MPI-IO` 函式庫，各行程會直覺地使用 `fopen` 和 `fread` 進行循序 I/O 操作。
- **檔案系統視角**: 對於檔案系統而言，它會看到**許多獨立的行程同時嘗試開啟同一個檔案**。
    - **只讀操作 (Read-only)**: 如果只是讀取，通常檔案系統會允許。但重複開啟仍會因**鎖定問題 (locking issues)** 而變慢。
    - **寫入權限 (Write permission)**: 如果目的是寫入，一般檔案系統**不允許**多個行程同時寫入，因為這會導致**資料一致性問題 (data consistency problem)**。直接使用 `fopen` 帶寫入權限會導致錯誤。
- **簡單解決方案：分割檔案**:
    - 將一個大檔案（例如10GB）分割成許多小檔案（例如10個1GB的檔案）。
    - 每個行程可以獨立存取一個小檔案，只要不同時讀取同一個檔案即可。
    - **缺點**:
        - 若檔案極大或行程數量多，會產生**數量龐大的小檔案**（例如1,000或10,000個）。
        - 這對檔案系統造成巨大的壓力，因為需要**額外的管理成本 (management cost)**。
        - 對使用者而言也很困擾，因為難以管理這麼多檔案。
        - 若行程數量改變，檔案ID的對應關係也需重新考慮，管理起來很麻煩。

#### **MPI-IO 的解決方案**

- **MPI-IO 的核心**: `MPI-IO` 函式庫正是為了解決上述問題。
- **`MPI_File_open` 的機制**:
    - 行程呼叫 `MPI_File_open` (而非直接 `fopen`)。
    - `MPI_File_open` 是一個 **集體操作 (collective code)** 的 API。
    - 它會收集所有行程的資訊，然後向下產生**單一的 `file open` 指令**給底層檔案系統。
    - 因此，檔案系統只會看到一個 `fopen` 操作。
    - MPI 函式庫會管理檔案的令牌 (token)，並協調所有行程的存取，決定存取方式和順序。
    - `MPI_File_open` **必須是集體操作**。
- **功能與效益**: MPI-IO 的 APIs 協助統一執行檔案的開啟、關閉、讀取和寫入操作。
    - 透過 MPI-IO，多個 MPI 行程可以**同時存取單一檔案**而不會產生問題。
    - 這能有效**改善效能**。

#### **MPI-IO 讀寫操作的類型**

- **讀寫操作的細分**: 不論是讀取 (`read`) 或寫入 (`write`)，都可以分為兩種：
    - **集體 I/O (Collective IO)**
    - **獨立 I/O (Independent IO)**

1. **集體 I/O (Collective IO)**:
    
    - **所有行程必須同時呼叫**讀取或寫入操作。
    - MPI 函式庫內部會維護一個 **internal IO buffer**。
    - 所有行程**共用**這個 buffer 來傳輸資料 (先寫入 buffer ，或從 buffer 讀取後再複製到使用者程式)。
    - **優點**:
        - 將許多由個別行程發出的小型、可能不連續但邏輯上相連的 I/O 請求，**聚合 (aggregate) 成一個大型的 I/O 請求**。
        - 這對於檔案系統而言，變成了一個更大的 I/O 大小，**效率會非常好**。
        - 特別適合於處理小量 I/O 請求。
2. **獨立 I/O (Independent IO)**:
    
    - **沒有內部 buffer**。
    - 每個行程直接呼叫自己的讀取或寫入操作。
    - **不是集體操作 (not collective)**。行程不需同時呼叫，一個行程讀取或寫入後即可返回，不會被卡住。
    - **優點**:
        - 適合處理**大量 I/O (large IO)** 請求。
        - 若單個行程一次讀寫的資料量已經很大（例如數MB或GB），則不需要與其他行程混合處理。
        - 直接使用獨立 I/O 的效能會比集體 I/O 更好，因為集體 I/O 多了一次記憶體複製 (memory copy) 的開銷，且需要等待其他行程。
    - **寫入一致性問題 (Write consistency)**:
        - 在獨立 I/O 中，程式設計師必須在應用層面**自行確保**各行程不會同時寫入相同位置。
        - 這可以透過計算索引或使用同步機制（例如 barrier）來避免。
        - 如果沒有處理好且寫入位置相同，函式庫層會對寫入進行排序，**後寫的資料會覆蓋 (overwrite) 先寫的結果**。
        - 獨立 I/O 不會有鎖定問題，但資料一致性由使用者程式負責。

- **MPI-IO 的其他函數**: 除了讀寫，還有 `MPI_File_close` 和 `MPI_File_sync` (將資料刷新到儲存系統) 等函數，這些都必須透過 MPI 函式庫來與檔案系統互動。
- **實作考量 (作業)**:
    - 在僅有本機檔案系統而非平行檔案系統（如 Lustre）的環境下，集體 I/O 和獨立 I/O 的效能差異可能**不明顯**。因為最終所有 I/O 都會集中在同一處形成瓶頸。
    - 但在真實的高效能計算環境中，若有平行檔案系統，MPI-IO 對效能的影響極大，且集體/獨立 I/O 的選擇需根據**寫入模式 (write pattern)** 進行調優。

#### **Even-Odd Sort (作業一)**

- **演算法概述**:
    - 一個簡單的排序演算法，基於迭代 。
    - 在不同迭代中，會與左右鄰居進行**交換**。
    - `Even-Odd` 指的是在奇數迭代和偶數迭代中，交換的對象不同（例如，一輪與左鄰居交換，下一輪與右鄰居交換）。
    - 透過比較和交換，將較大的數字推向左邊，較小的數字推向右邊，最終使整個陣列排序完成。
- **基本版**:
    - 必須**完全遵循演算法的計算過程**，即每次只允許**交換一個元素**。
    - 即使是在同一機器內部或跨機器邊界，排序操作也必須依此演算法進行。
    - 此版本**不追求效能優化**，只要能正確運作即可。
    - **主要挑戰**: 處理**跨機器邊界 (machine boundary)** 的資料交換，需要使用 `MPI_Send` 和 `MPI_Receive`。使用**blocking S/R** 可能會節省時間。
- **進階版**:
    - 只在**通訊模式 (communication pattern) 層面**進行限制。
    - 例如，`rank0` 只能發送訊息給 `rank1`；`rank1` 可以發送給 `rank0` 或 `rank2`。通訊模式需遵循 `Even-Odd` 概念，但針對行程 ID 或機器 ID。
    - **不限制每個行程內部對數字的處理方式** (計算方式)。每個行程可以自由處理其子陣列的排序。
    - **通訊彈性**: 行程之間傳輸資料時，可以傳輸**所有元素、一半或任意數量**。
    - 由於計算方式沒有嚴格限制，此版本允許**多種優化方式**。
- **演算法特性**:
    - **循序版 Even-Odd Sort**: 複雜度很高，效率不高，因為每次迭代內部都有許多小的交換。
    - **平行版 Even-Odd Sort**: 在平行化後，所有行程可以在同一個迭代中同時向左右鄰居發送或接收資料。其計算速度可以**非常快 (umlinear time)**，是一種不錯的平行排序方法。
- **實作考量**:
    - **邊界檢查 (Boundary Check)**: 需特別處理邊界情況，例如最左邊的行程不能再向左發送。
    - **資料分割 (Data Partitioning)**: 每個行程處理的數字數量可能不同，理想情況是**越平均越好**，以平衡計算量、I/O 量和網路流量。若無法完全平均，也需考慮邊界情況。
    - 進階版的優化將圍繞這些考量進行。

#### **共享記憶體程式設計**

- **平行程式設計的兩大模型**:
    - **訊息傳遞 (Message Passing)**，例如 `MPI`。
    - **共享記憶體 (Shared Memory)**，例如 `Pthreads` 和 `OpenMP`。
- **共享記憶體程式的特性**:
    - **API 數量少**: 非常底層，主要用於建立、刪除、合併執行緒 (threads)。
    - **通訊方式**: 無需專門的 API，因為執行緒直接存取共享記憶體中的資料。資料交換和資訊共享非常容易。
    - **主要挑戰：同步化 (Synchronization)**:
        - 所有執行緒可以隨意修改共享記憶體中的值，且不必通知其他執行緒。
        - 必須添加程式碼（如 **locks** 或 **barriers**）來確保修改的順序和時間點沒有衝突。
        - 若同步控制不當，可能導致 **deadlock**，即執行緒互相等待，最終所有執行緒都無法動彈。
        - **Deadlock 原因對比**: 共享記憶體中的 deadlock 是由於互相等待，而訊息傳遞中的 deadlock 通常是由於集體操作中某個行程卡住等待其他行程，更難預測和偵錯。
    - **快取一致性 (Cache Coherency)**:
        - 執行緒可能運行在不同的核心 (cores) 上，每個核心有獨立的快取。
        - 需要確保不同核心的快取中的值是一致的 (cache copy)。
        - 這個問題通常由 **CPU 處理器** 自動解決，程式設計師無需處理。
- **共享記憶體的常見實作**:
    - **Pthreads (POSIX Threads)**: 一種執行緒函式庫 (thread library)。
    - **OpenMP**: 透過編譯器指令 (compiler directive) 協助產生平行程式碼。
    - **Unix 行程 (Unix processes)**: 也可以透過作業系統在行程之間建立共享記憶體進行通訊，但較不常見。

#### **行程 (Process) 與執行緒 (Thread) 的差異**

- **行程 (Process)**:
    - 在執行時擁有**獨立的記憶體內容**，例如：
        - **堆疊 (stack)** 儲存區域變數 (local variables)。
        - **檔案描述符 (file descriptors)** 儲存資源。
        - **資料區 (data memory)** 儲存全域變數 (global variables)。
    - 每個行程擁有其**獨立的這些內容**。
    - 通常一個行程等同於一個執行緒 (multi-threaded process 中的主執行緒)。
- **執行緒 (Thread)**:
    - 因應多核心 CPU (multi-core CPU) 和平行計算概念的發展而生。
    - 一個行程可以**建立多個執行緒**。
    - **關鍵差異**: 同一個行程內的不同執行緒，**部分記憶體內容是共享的，部分是不共享的**。
        - **共享部分**: 是執行緒之間可以溝通的原因。主要共享的是**全域記憶體 (global memory)**，即**全域變數 (global variables)**。
        - **非共享部分**: 使得執行緒可以獨立執行各自的程式碼 (例如堆疊中的區域變數)。程式碼雖然相同，但它們執行的指令位置可以不同，從而產生不同的行為。
    - **資源共享**: 大部分資源（例如透過一個執行緒開啟的檔案，其檔案句柄 (file handle) 可以傳給任何其他執行緒使用）是**共享的**。
    - **作業系統實作**: 在實作上，許多作業系統（如 Linux）對行程和執行緒的區分不明顯，主要透過建立時的旗標 (flag) 來決定哪些記憶體內容是共享的。
    - **優點**: 透過執行緒，可以在一個行程內直接利用多核心 CPU，並透過共享記憶體空間進行程式編寫。

#### **為什麼單機平行程式偏好執行緒 (Threads)**

- **建立速度 (Creation Speed)**:
    - 建立一個執行緒 (例如 `pthread_create`) 比建立一個行程 (例如 `fork` 系統呼叫) 要快得多。
    - 行程比較肥，包含更多記憶體內容，因此建立速度慢約 **15 到 50 倍**。
    - 在單機上若只是為了共享記憶體進行通訊，建立執行緒會快很多。
- **通訊速度 (Communication Speed)**:
    - 在同一台機器內部，執行緒透過直接存取共享記憶體的方式讀取資料，速度比 MPI 透過記憶體複製 (`MPI_Send`/`MPI_Receive`) 的方式**快 2 到 15 倍**。
    - 即使 MPI 函式庫在單機環境下進行了優化，它至少仍需要進行一次記憶體複製操作，這需要時間。
- **結論**: 如果程式只在單一機器上運行，大多數人會選擇使用執行緒，因為其效率更高。但 MPI 程式仍可在單機上運行。

#### **混合平行程式設計 (Hybrid Parallel Programming)**

- **組合模式**: 結合了共享記憶體 (執行緒) 和訊息傳遞 (MPI) 這兩種平行計算方式。
- **應用場景**: 在單一機器內部利用執行緒進行共享記憶體平行化，而在跨機器時則使用 MPI 進行通訊。
- 這是目前**最常見的平行程式設計方式**。

#### **Pthreads (POSIX Threads) 是什麼**

- **Pthreads 的定義**:
    - `P` 代表 `POSIX` (Portable Operating System Interface)。
    - Pthreads 是 `POSIX` 系統 API 中定義的**執行緒介面 (thread interface)**。它是一個函式庫，是 `POSIX` 介面的一部分。
- **`pthread_create` 函數**: 用於建立執行緒的主要 API。
    - **執行緒的行為**: 一個被建立的執行緒會被指派一個**函數指標 (function pointer)**，它唯一的任務就是**執行那個函數碼**。
    - 主執行緒 (main thread) 和被建立的執行緒 (created thread) 可以同時被作業系統排程執行，從而實現平行加速。
    - **協作方式**: 執行緒透過**共享全域變數 (global variables)** 進行溝通。執行緒完成函數執行後，也可以透過**return value** 傳遞資訊。
    - **`pthread_create` 的參數**:
        1. **`thread` (執行緒 ID)**: 一個指向 `pthread_t` 變數的指標，用於儲存新執行緒的 ID 。主執行緒可以透過此令牌與新執行緒溝通（例如使用 `pthread_join` 等待其結束並取得返回值）。
        2. **`attr` (屬性)**: 指向 `pthread_attr_t` 結構的指標，用於設定執行緒的配置參數。通常設為 `NULL` 使用預設值。
            - 可配置屬性，例如指定執行緒的記憶體配置應該與執行該執行緒的 CPU 晶片在同一位置，以加速存取。
        3. **`start_routine` (起始常式/函數指標)**: 指向新執行緒將要執行的函數。
        4. **`arg` (參數)**: 一個 `void*` 型別的指標，用於向 `start_routine` 傳遞參數。
            - 若要傳遞多個參數，可以將它們**封裝在一個 struct 中**，然後傳遞該結構的指標。
            - 在執行緒的函數內部，需要對此指標進行**類型轉換 (cast)** 並解參考 (dereference) 來取出參數值。這是一個較為底層的操作，若操作不當可能導致資料錯誤。
