---
# System prepended metadata

title: Kotlin / Ch02 - 並行程式設計

---

# Kotlin / Ch02 - 並行程式設計

## 前言：從單工到多工的演進

現代軟體應用程式需要同時處理多項任務：使用者點擊按鈕、網路資料下載、資料庫查詢、檔案讀寫等。如果這些任務都按照順序執行，使用者體驗會變得非常糟糕。想像一下，當你點擊一個按鈕下載檔案時，整個應用程式都凍結直到下載完成，這在現代軟體中是不可接受的。

並行程式設計讓我們能夠同時處理多項任務，提供流暢的使用者體驗。Kotlin 提供了從傳統的執行緒 (Thread) 到現代化的協程 (Coroutine) 等多種並行處理方式。

## 第一章：Process & Thread

### 1.1 什麼是 Process (行程)

Process 就像是電腦中正在執行的一個完整應用程式。當你打開一個文字編輯器、瀏覽器或遊戲時，每個都是一個獨立的 Process。你可以現在就打開工作管理員（Windows）或活動監視器（MacOS）來觀察目前有哪些 Process 正在運行；如果有某的應用程式卡住了，也是從這邊直接 terminate 掉該 Process 來強制重啟。

#### 1.1.1 Process 的特徵

想像 Process 就像是一間獨立的辦公室：

```mermaid
graph TD
    A[作業系統] --> B[Process A: 文字編輯器]
    A --> C[Process B: 瀏覽器]
    A --> D[Process C: 遊戲]
    
    B --> F[CPU 時間片]
    B --> E[獨立記憶體空間]
    B --> G[檔案系統權限]
    
    C --> I[CPU 時間片]
    C --> H[獨立記憶體空間]
    C --> J[網路資源]
    
    D --> K[其他資源...]
```

**Process 的優點：**
- **安全隔離**：一個 Process 當機不會影響其他 Process
- **資源保護**：每個 Process 都有自己的記憶體空間
- **穩定性高**：系統設計上較為穩定

**Process 的缺點：**
- **資源消耗大**：每個 Process 都需要獨立的系統資源
- **溝通困難**：Process 之間的資料交換需要特殊機制
- **建立成本高**：啟動新的 Process 需要較多時間和資源

### 1.2 什麼是 Thread (執行緒)

如果 Process 是一間辦公室，那麼 Thread 就是辦公室裡的員工。一間辦公室可以有多個員工同時工作，他們共享辦公室的資源（如印表機、茶水間），但各自負責不同的工作任務。

#### 1.2.1 Thread 的生命週期

```mermaid
stateDiagram-v2
    Direction LR
    [*] --> 新建立: new Thread()
    新建立 --> 可執行: start()
    可執行 --> 執行中: 排程器分配CPU
    執行中 --> 可執行: yield() 讓出CPU
    執行中 --> 等待中: sleep/wait/join
    等待中 --> 可執行: 等待條件滿足
    執行中 --> 結束: 執行完畢
    結束--> [*]
```

#### 1.2.2 在 Kotlin 中建立 Thread

建立 Thread 有兩種主要方式，就像僱用新員工有不同的招聘方式：

**方式一：實作 Runnable Interface（低耦合，推薦使用）**
```kotlin=
class DataProcessor : Runnable {
    override fun run() {
        println("正在處理資料...")
        // 實際的資料處理邏輯
        Thread.sleep(2000) // 模擬耗時操作
        println("資料處理完成！")
    }
}

fun main(){
    val processor = DataProcessor()
    val thread = Thread(processor)
    thread.start()
    
    // 等待執行緒完成
    thread.join()
}
```

**方式二：繼承 Thread Class (實作上不推薦)**
```kotlin=
class BackgroundTask : Thread() {
    override fun run() {
        println("背景任務開始執行...")
        // 背景工作邏輯
        for (i in 1..5) {
            println("處理步驟 $i")
            Thread.sleep(1000)
        }
        println("背景任務完成！")
    }
}

fun main(){
    val task = BackgroundTask()
    task.start()
    
    // 等待執行緒完成
    task.join()
}
```

### 1.3 Thread 在實際開發中的挑戰

雖然 Thread 很有用，但也帶來了一些挑戰：

#### 1.3.1 效能瓶頸

每個 Thread 都需要約 1MB 的記憶體空間，而且建立和切換 Thread 都需要系統開銷。當我們需要處理大量並發任務時，傳統 Thread 就顯得力不從心。

#### 1.3.2 複雜性問題

多個 Thread 同時存取共享資源時，容易產生競爭條件 (Race Condition)、死鎖 (Deadlock) 等問題，使程式變得難以除錯和維護。

## 第二章：高階併發模型 Coroutine

### 2.1 什麼是 Coroutine

Coroutine（協程）可以想像成是「虛擬員工」。與傳統的實體員工（Thread）不同，虛擬員工不需要實體辦公桌，可以根據工作需要靈活調度，大大節省了資源。

#### 2.1.1 Coroutine 的概念圖解

```mermaid
graph TB
    A[應用程式 Application] --> B[行程 Process]
    B --> C1[執行緒 Thread 1]
    B --> C2[執行緒 Thread 2]
    B --> C3[執行緒 Thread 3]
    
    C1 --> D1[協程 A]
    C1 --> D2[協程 B]
    C2 --> D3[協程 C]
    C2 --> D4[協程 D]
    C3 --> D5[協程 E]
    C3 --> D6[協程 F]
    
    style D1 fill:#e1f5fe
    style D2 fill:#e1f5fe
    style D3 fill:#fff3e0
    style D4 fill:#fff3e0
    style D5 fill:#e8f5e8
    style D6 fill:#e8f5e8
```

### 2.2 Coroutine 的優勢

#### 2.2.1 效能的顯著提升

這可以透過實際測試來看出差異：

**建立 10,000 個 Thread：**
```kotlin=
import kotlin.system.measureTimeMillis

fun main() {
    val time = measureTimeMillis {
        repeat(10000) {
            Thread {
                // 模擬簡單工作
                Thread.sleep(10)
            }.start()
        }
        Thread.sleep(2000) // 等待所有執行緒完成
    }
    println("Thread 方式耗時: ${time}ms")
    // 結果：約 3577ms (Run @Kotlin Playground)
}
```

**建立 10,000 個 Coroutine：**
```kotlin=
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main() = runBlocking {
    val time = measureTimeMillis {
        val jobs = List(10000) {
            launch {
                // 模擬簡單工作
                delay(10)
            }
        }
        jobs.forEach { it.join() } // 等待所有協程完成
    }
    println("Coroutine 方式耗時: ${time}ms")
    // 結果：約 517ms
}
```

**效能提升超過 6 倍！**

#### 2.2.2 資源使用效率

| 特性 | Thread | Coroutine |
|------|--------|-----------|
| 記憶體使用 | ~1MB per thread | ~幾 KB per coroutine |
| 建立速度 | 較慢 | 非常快 |
| 可同時建立數量 | 數千個 | 數十萬個 |
| CPU 切換成本 | 高 | 低 |

### 2.3 第一個 Coroutine 程式

#### 2.3.1 Hello World!

```kotlin=
import kotlinx.coroutines.*

fun main() = runBlocking { // 建立協程作用域
    launch { // 啟動新的協程
        delay(1000L) // 非阻塞延遲1秒
        println("World!")
    }
    println("Hello")
}

// 輸出：
// Hello
// World!
```

這個簡單的範例展示了 Coroutine 的核心特性：
- `runBlocking`：建立協程作用域
- `launch`：啟動新的協程而不阻塞主執行緒
- `delay`：非阻塞的延遲函數

#### 2.3.2 等待協程完成

```kotlin=
fun main() = runBlocking {
    val job = launch {
        repeat(5) { i ->
            println("協程工作中: $i")
            delay(500L)
        }
    }
    
    println("主程式繼續執行...")
    job.join() // 等待協程完成
    println("所有工作完成！")
}
```

## 第三章：Mutable ＆ Immutable

### 3.1 什麼是 Mutable ＆ Immutable

在程式設計中，物件的可變性 (Mutability) 決定了物件在建立後是否可以被修改。

- **Immutable (不可變)**：物件一旦建立後，其內容就不能被改變。任何看似「修改」的操作實際上都會建立新的物件。
- **Mutable (可變)**：物件建立後，其內容可以被直接修改，而不需要建立新的物件。

### 3.2 記憶體層面的差異

#### 3.2.1 Immutable 物件在記憶體中的行為

```kotlin=
fun demonstrateImmutableString() {
    var str = "Hello"
    println("原始字串記憶體位址: ${System.identityHashCode(str)}")
    
    str += " World"  // 看起來像是修改，實際上建立了新物件
    println("修改後字串記憶體位址: ${System.identityHashCode(str)}")
    
    val str2 = "Hello World"
    println("新字串記憶體位址: ${System.identityHashCode(str2)}")
}

// 輸出結果：
// 原始字串記憶體位址: 1234567
// 修改後字串記憶體位址: 7654321 (不同的位址！)
// 新字串記憶體位址: 7654321 (相同內容可能被快取)
```

在這個例子中可以看到，`str += " World"` 並沒有修改原本的字串物件，而是**建立了一個全新的字串物件**。

#### 3.2.2 記憶體配置圖解

```mermaid
graph TD
    A["變數 str"] --> B["記憶體位址 0x1000<br/>內容: 'Hello'"]
    C["執行 str += ' World'"]
    D["變數 str"] --> E["記憶體位址 0x2000<br/>內容: 'Hello World'"]
    F["記憶體位址 0x1000<br/>內容: 'Hello'<br/>(等待 GC)"]
    
    style B fill:#e1f5fe
    style E fill:#e8f5e8
    style F fill:#ffebee
```

### 3.3 String 的底層實作

#### 3.3.1 為什麼 String 是 Immutable

String 被設計成 immutable 有幾個重要原因：

```kotlin=
fun exploreStringImmutability() {
    val str1 = "Kotlin"
    val str2 = "Kotlin"
    
    // 字串池 (String Pool) 的效果
    println("str1 === str2: ${str1 === str2}")  // true
    println("記憶體位址相同: ${System.identityHashCode(str1) == System.identityHashCode(str2)}") // true
    
    // 嘗試「修改」字串
    val originalStr = "Hello"
    val modifiedStr = originalStr.uppercase()
    
    println("原始字串: $originalStr")        // Hello
    println("轉大寫後: $modifiedStr")        // HELLO
    println("原始字串是否改變: ${originalStr == "Hello"}")  // true，沒有改變
    println("記憶體位址不相同: ${System.identityHashCode(originalStr) == System.identityHashCode(modifiedStr)}") // false
}
```

> **String Pool 機制：**
JVM 使用字串池來儲存字串常數，相同內容的字串會共享同一塊記憶體空間，大大節省記憶體使用。

#### 3.3.2 String 串接的效能問題

```kotlin=
fun inefficientStringConcatenation() {
    var result = ""
    val startTime = System.currentTimeMillis()
    
    // 低效率的字串串接
    for (i in 1..10000) {
        result += "Item $i, "  // 每次都建立新的 String 物件
    }
    
    val endTime = System.currentTimeMillis()
    println("使用 String 串接耗時: ${endTime - startTime}ms")
    println("最終字串長度: ${result.length}")
}

// 結果：
// 使用 String 串接耗時: 547ms
// 最終字串長度: 108894
```

在上述程式碼中，每次執行 `result += "Item $i, "` 都會：
1. 建立新的字串物件
2. 將舊字串和新內容複製到新物件中
3. 舊的字串物件等待垃圾回收

對於 10,000 次循環，這意味著會建立 10,000 個臨時字串物件！

### 3.4 StringBuilder：Mutable 字串解決方案

#### 3.4.1 StringBuilder 的工作原理

StringBuilder 內部使用可變的字元陣列來儲存字串內容，避免了頻繁建立新物件的問題。

```kotlin=
fun efficientStringBuilding() {
    val sb = StringBuilder()
    val startTime = System.currentTimeMillis()
    
    // 高效率的字串建構
    for (i in 1..10000) {
        sb.append("Item $i, ")  // 直接修改內部陣列
    }
    
    val result = sb.toString()  // 最後才建立 String 物件
    val endTime = System.currentTimeMillis()
    
    println("使用 StringBuilder 耗時: ${endTime - startTime}ms")
    println("最終字串長度: ${result.length}")
}

// 結果：
// 使用 StringBuilder 耗時: 11ms
// 最終字串長度: 108894
```

#### 3.4.2 StringBuilder 內部結構

```kotlin=
fun exploreStringBuilderInternals() {
    val sb = StringBuilder()
    println("初始容量: ${sb.capacity()}")  // 預設 16
    
    // 添加內容
    sb.append("Hello")
    println("添加 'Hello' 後容量: ${sb.capacity()}")
    println("當前長度: ${sb.length}")
    
    // 繼續添加內容
    sb.append(" World! This is a longer string to trigger capacity expansion.")
    println("添加更多內容後容量: ${sb.capacity()}")  // 自動擴展容量
    println("當前長度: ${sb.length}")
}
```

> **容量擴展機制：**
當內容超過當前容量時，StringBuilder 會自動擴展內部陣列，通常是將容量翻倍。

### 3.5 StringBuffer vs StringBuilder

#### 3.5.1 執行緒安全性比較

```kotlin=
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

fun compareStringBufferAndBuilder() {
    val stringBuffer = StringBuffer()
    val stringBuilder = StringBuilder()
    val executor = Executors.newFixedThreadPool(10)
    
    // StringBuffer 測試 (執行緒安全)
    val bufferStart = System.currentTimeMillis()
    repeat(10) { threadId ->
        executor.submit {
            repeat(1000) { i ->
                stringBuffer.append("Thread-$threadId-Item-$i ")
            }
        }
    }
    
    // StringBuilder 測試 (非執行緒安全)
    repeat(10) { threadId ->
        executor.submit {
            repeat(1000) { i ->
                stringBuilder.append("Thread-$threadId-Item-$i ")
            }
        }
    }
    
    executor.shutdown()
    executor.awaitTermination(5, TimeUnit.SECONDS)
    
    println("StringBuffer 最終長度: ${stringBuffer.length}")
    println("StringBuilder 最終長度: ${stringBuilder.length}")  // 可能因為競爭條件導致資料遺失
}
```

| 特性 | String | StringBuffer | StringBuilder |
|------|--------|--------------|---------------|
| 可變性 | Immutable | Mutable | Mutable |
| 執行緒安全 | 安全 (因為不可變) | 安全 (synchronized) | 不安全 |
| 效能 | 串接時低效 | 中等 (有同步開銷) | 高效 |
| 記憶體使用 | 頻繁串接時高 | 較低 | 最低 |
| 適用場景 | 字串不常變動 | 多執行緒環境 | 單執行緒字串建構 |