# 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) | 不安全 | | 效能 | 串接時低效 | 中等 (有同步開銷) | 高效 | | 記憶體使用 | 頻繁串接時高 | 較低 | 最低 | | 適用場景 | 字串不常變動 | 多執行緒環境 | 單執行緒字串建構 |
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up