Programming
น้องๆ เคยสงสัยไหมว่าทำไมโปรแกรมบางทีมันทำงานช้าจัง? หรือทำไมบางแอปถึงทำหลายอย่างพร้อมๆ กันได้? นั่นแหละคือเรื่องของ Concurrency กับ Parallelism ที่เราจะมาคุยกันวันนี้ มันสำคัญมากๆ เพราะมันช่วยให้โปรแกรมเราทำงานได้เร็วขึ้น และจัดการงานต่างๆ ได้ดีขึ้น โดยเฉพาะในยุคที่ CPU มีหลาย core และเราต้องจัดการงานพร้อมๆ กันเยอะๆ
Concurrency คือการจัดการงานหลายอย่าง "เหมือน" ทำพร้อมกัน แต่จริงๆ แล้วสลับกันทำไปเรื่อยๆ เหมือนเราเป็น Supermarket cashier ที่สลับกันคิดเงินลูกค้าแต่ละคนไปเรื่อยๆ ส่วน Parallelism คือการทำงานหลายอย่าง "พร้อมกันจริงๆ" โดยใช้ CPU หลาย core เหมือนมี cashier หลายคนคิดเงินลูกค้าพร้อมๆ กันเลย
Python กับ Go เป็นภาษาที่ support ทั้ง Concurrency และ Parallelism แต่ด้วยวิธีที่ต่างกัน Python จะเน้น Concurrency มากกว่า เพราะติดเรื่อง Global Interpreter Lock (GIL) ส่วน Go ออกแบบมาให้ Parallelism ทำงานได้ดีกว่า
ก่อนจะไปลงมือเขียน code เราต้องเข้าใจ concept พื้นฐานพวกนี้ก่อนนะ
Process คือโปรแกรมที่กำลังทำงานอยู่ แต่ละ process จะมี memory space เป็นของตัวเอง ส่วน Thread คือหน่วยย่อยของการทำงานใน process หนึ่งๆ process หนึ่งอาจจะมีหลาย thread ก็ได้ Thread จะ share memory space กันได้ ทำให้สื่อสารกันง่าย แต่ก็ต้องระวังเรื่อง race condition ด้วย
สมัยผมทำร้านเน็ต ผมเคยเจอเคสที่โปรแกรม server crash เพราะหลาย thread เข้าไปแก้ข้อมูลใน memory พร้อมๆ กัน ทำให้ข้อมูลมันเพี้ยนไปหมด ต้องมานั่ง debug กันหัวแตกเลย
Race condition คือสถานการณ์ที่ผลลัพธ์ของการทำงานขึ้นอยู่กับลำดับการทำงานของ thread หลายๆ ตัว ถ้าลำดับมันเปลี่ยน ผลลัพธ์ก็เปลี่ยนไปด้วย ซึ่งส่วนใหญ่จะไม่ใช่สิ่งที่เราต้องการ Mutex (Mutual Exclusion) เป็นเครื่องมือที่ใช้ป้องกัน race condition โดยการ lock resource ไม่ให้ thread อื่นเข้ามายุ่ง จนกว่า thread ที่ lock จะปล่อย
คิดภาพง่ายๆ เหมือนห้องน้ำสาธารณะ มีคนเข้าไปใช้แล้วล็อคประตู คนอื่นก็ต้องรอจนกว่าคนข้างในจะออกมาปลดล็อคก่อนถึงจะเข้าไปได้
มาดูวิธีใช้ Concurrency และ Parallelism ใน Python และ Go กัน เริ่มจาก Python ก่อนนะ
Python มี module threading ที่ช่วยให้เราสร้างและจัดการ thread ได้ง่ายๆ ลองดูตัวอย่างนี้
import threading
import time
def task(name):
print(f"Thread {name}: starting")
time.sleep(2) # Simulate some work
print(f"Thread {name}: finishing")
threads = []
for i in range(3):
t = threading.Thread(target=task, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join() # Wait for all threads to complete
print("All threads finished")
code นี้สร้าง 3 thread แต่ละ thread จะ run function task ซึ่งจะพิมพ์ข้อความ แล้ว sleep ไป 2 วินาที จากนั้นก็พิมพ์ข้อความอีกที สังเกตว่า thread ทั้ง 3 ตัวจะทำงาน "เหมือน" พร้อมกัน แต่จริงๆ แล้ว Python จะสลับกัน run thread แต่ละตัวไปเรื่อยๆ เพราะติด GIL
asyncio เป็น library ที่ช่วยให้เราเขียน asynchronous code ใน Python ได้ง่ายขึ้น Asynchronous code จะใช้ event loop ในการจัดการ task หลายๆ อย่าง ทำให้โปรแกรมเราไม่ block และสามารถทำงานอื่นๆ ไปพร้อมๆ กันได้
import asyncio
import time
async def task(name):
print(f"Task {name}: starting")
await asyncio.sleep(2) # Simulate some work
print(f"Task {name}: finishing")
async def main():
tasks = [task(i) for i in range(3)]
await asyncio.gather(*tasks) # Run all tasks concurrently
if __name__ == "__main__":
asyncio.run(main())
code นี้คล้ายกับตัวอย่าง threading แต่ใช้ async และ await แทน asyncio.sleep จะไม่ block event loop ทำให้ task อื่นๆ สามารถทำงานได้ในขณะที่รอ sleep เสร็จ
Go มี concept ที่เรียกว่า Goroutine ซึ่งคล้ายกับ thread แต่ lightweight กว่ามากๆ เราสามารถสร้าง Goroutine ได้เยอะๆ โดยไม่เปลือง resource มากนัก
package main
import (
"fmt"
"time"
)
func task(name int) {
fmt.Printf("Goroutine %d: starting\n", name)
time.Sleep(2 * time.Second) // Simulate some work
fmt.Printf("Goroutine %d: finishing\n", name)
}
func main() {
for i := 0; i < 3; i++ {
go task(i) // Start a new goroutine
}
time.Sleep(3 * time.Second) // Wait for goroutines to complete (not ideal)
fmt.Println("All goroutines finished")
}
code นี้สร้าง 3 Goroutine แต่ละ Goroutine จะ run function task ซึ่งจะพิมพ์ข้อความ แล้ว sleep ไป 2 วินาที จากนั้นก็พิมพ์ข้อความอีกที สังเกตว่า Goroutine ทั้ง 3 ตัวจะทำงานพร้อมกันจริงๆ เพราะ Go ออกแบบมาให้ Parallelism ทำงานได้ดี
สมัยผมทำ server ด้วย Go ผมใช้ Goroutine เยอะมากๆ เพราะมันจัดการง่ายและ performance ดีกว่า threading ใน Python เยอะเลย
Channel เป็นวิธีที่ Go ใช้ในการสื่อสารระหว่าง Goroutine มันเป็น type-safe queue ที่ช่วยให้เราส่งและรับข้อมูลระหว่าง Goroutine ได้อย่างปลอดภัย
package main
import (
"fmt"
"time"
)
func task(name int, ch chan string) {
fmt.Printf("Goroutine %d: starting\n", name)
time.Sleep(2 * time.Second) // Simulate some work
fmt.Printf("Goroutine %d: finishing\n", name)
ch <- fmt.Sprintf("Task %d completed", name)
}
func main() {
ch := make(chan string, 3) // Create a channel with buffer size 3
for i := 0; i < 3; i++ {
go task(i, ch) // Start a new goroutine
}
for i := 0; i < 3; i++ {
result := <-ch // Receive from the channel
fmt.Println(result)
}
fmt.Println("All goroutines finished")
}
code นี้ใช้ channel ในการส่ง message จาก Goroutine กลับมาที่ main Goroutine เพื่อบอกว่า task เสร็จแล้ว
นอกจาก threading, asyncio และ Goroutine แล้ว ยังมีทางเลือกอื่นๆ อีกมากมายในการจัดการ Concurrency และ Parallelism แต่ละทางเลือกก็มีข้อดีข้อเสียต่างกันไป
| เทคนิค | ภาษา | ข้อดี | ข้อเสีย |
|---|---|---|---|
| Threading | Python, Java, C++ | ใช้งานง่าย | ติด GIL (Python), overhead สูง |
| Asyncio | Python | Efficient, non-blocking | เขียนยากกว่า threading |
| Goroutines | Go | Lightweight, efficient, built-in support | ต้องเรียนรู้ syntax ใหม่ |
| Multiprocessing | Python | Bypass GIL, true parallelism | Overhead สูงกว่า threading, สื่อสารระหว่าง process ยาก |
ถ้างานของเราเป็น I/O bound (เช่น อ่าน/เขียน file หรือ network) asyncio หรือ Goroutine จะเป็นทางเลือกที่ดีกว่า เพราะมันจะไม่ block โปรแกรมของเรา แต่ถ้านานของเราเป็น CPU bound (เช่น คำนวณเลขเยอะๆ) multiprocessing จะเป็นทางเลือกที่ดีกว่า เพราะมันจะใช้ CPU หลาย core ได้อย่างเต็มที่ SiamCafe Blog มีบทความเกี่ยวกับเรื่องนี้เยอะเลย ลองไปอ่านดูได้
สุดท้ายนี้ การเลือกใช้เทคนิคไหน ก็ขึ้นอยู่กับ requirement ของโปรเจกต์เรา และความถนัดของแต่ละคน อย่ากลัวที่จะลองผิดลองถูก และเรียนรู้จากประสบการณ์จริงนะ SiamCafe Blog เองก็เป็นที่ที่ผมแชร์ประสบการณ์ต่างๆ ที่ได้จากการทำงานจริง หวังว่าน้องๆ จะได้ประโยชน์จากบทความนี้นะครับ
ดูวิดีโอเพิ่มเติมเกี่ยวกับConcurrency Parallelism Python:
น้องๆ หลายคนถามพี่ว่า "พี่บอม concurrency กับ parallelism นี่มันยากจัง มีเคล็ดลับอะไรบ้างครับ?" อ่ะ เดี๋ยวพี่เล่าให้ฟังจากประสบการณ์ตรงสมัยทำร้านเน็ต SiamCafe เลย
ตอนนั้นเน็ตเราช้ามาก (56k modem อ่ะคิดดู) แต่ลูกค้าอยากเล่นเกมออนไลน์พร้อมกันหลายเครื่อง พี่เลยต้องหาวิธีจัดการ resource ให้ดีที่สุด concurrency กับ parallelism เลยเข้ามามีบทบาท พี่ลองผิดลองถูกมาเยอะ เจ็บมาเยอะ เลยพอมีเคล็ดลับมาฝากกัน
น้องต้องเข้าใจก่อนว่า ปัญหาที่น้องเจออยู่จริงๆ คืออะไร ต้องการให้โปรแกรมทำงานเร็วขึ้น? หรือต้องการให้มันตอบสนองได้ดีขึ้น? เพราะ concurrency กับ parallelism มันแก้ปัญหาคนละแบบ ถ้าเลือกผิดชีวิตเปลี่ยนเลยนะ
สมัยพี่ทำร้านเน็ต พี่ต้องทำให้เครื่อง server รับ request จาก client ได้เยอะๆ พร้อมๆ กัน เลยเน้น concurrency มากกว่า เพราะ CPU มันไม่ได้แรงมาก แต่ต้องจัดการงานหลายอย่าง
Python นี่เขียนง่าย อ่านง่าย แต่เรื่อง concurrency อาจจะไม่ได้เปรียบเทียบเท่า Go เพราะติดเรื่อง Global Interpreter Lock (GIL) Go เกิดมาเพื่อ concurrency โดยเฉพาะ Goroutine นี่เบาหวิวมาก
ถ้างานน้องเน้น I/O-bound (รออ่านเขียนข้อมูลจาก disk หรือ network) Python อาจจะตอบโจทย์กว่า แต่ถ้าน้องต้องการ CPU-bound (คำนวณหนักๆ) Go น่าจะดีกว่า
# Python example (asyncio)
import asyncio
async def my_task(name):
print(f"Task {name}: Starting")
await asyncio.sleep(1)
print(f"Task {name}: Finished")
async def main():
tasks = [my_task("A"), my_task("B")]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
สร้าง thread เยอะเกินไปก็เปลือง resource เหมือนกัน Thread Pool ช่วยให้น้องจัดการ thread ได้อย่างมีประสิทธิภาพ reuse thread ได้ ไม่ต้องสร้างใหม่ทุกครั้ง
พี่เคยเจอเคสที่ client ส่ง request มาถี่ๆ แล้ว server สร้าง thread ใหม่ทุก request ผลคือ server แฮงค์ไปเลย เพราะ resource ไม่พอ Thread Pool ช่วยแก้ปัญหานี้ได้ดีมาก
// Go example (using goroutines and waitgroups)
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
}
อย่าคิดว่า code ที่น้องเขียนมันจะ performant ตั้งแต่แรก Test ให้เยอะ monitor ให้ดี profile ให้ละเอียด จะได้รู้ว่า bottleneck มันอยู่ตรงไหน แล้วค่อยแก้
สมัยพี่ทำร้านเน็ต พี่ใช้ tool หลายตัวในการ monitor CPU usage, memory usage, network traffic เพื่อหาจุดที่ต้องปรับปรุง
GIL มีไว้เพื่อป้องกัน data race ใน CPython (implementation หลักของ Python) มันทำให้ thread สามารถ access object ได้ทีละ thread เท่านั้น ข้อดีคือเขียน code ง่ายขึ้น ข้อเสียคือ performance อาจจะไม่ดีเท่าที่ควรใน CPU-bound task แต่ถ้าเป็น I/O-bound task GIL ไม่ค่อยมีผลเท่าไหร่
Goroutine เบากว่า thread มาก เพราะมันจัดการโดย Go runtime ไม่ใช่ OS Goroutine ใช้ memory น้อยกว่า สร้างเร็วกว่า switch เร็วกว่า ทำให้ Go สามารถ run goroutine ได้เป็นจำนวนมาก
Channel ใช้สำหรับ communicate ระหว่าง goroutine มันช่วยให้เรา synchronize ข้อมูลและป้องกัน data race ได้ ใช้เมื่อ goroutine ต้องส่งข้อมูลให้กัน หรือต้องรอให้ goroutine อื่นทำงานเสร็จก่อน
Concurrency กับ parallelism เป็นเรื่องที่ซับซ้อน แต่ถ้าเข้าใจหลักการ และเลือกใช้เครื่องมือให้เหมาะสม น้องๆ ก็จะสามารถเขียนโปรแกรมที่มีประสิทธิภาพได้
อย่าลืมว่า practice makes perfect ลองทำโจทย์ ลองเขียน code เยอะๆ แล้วน้องจะเก่งขึ้นเอง SiamCafe Blog มีบทความดีๆ อีกเยอะ ลองเข้าไปอ่านดูนะ
ถ้าสนใจเรื่อง forex ลองดู iCafeForex ได้นะ อันนี้พี่ไม่ได้ทำเองนะจ๊ะ