สไตล์ของ parallel programming ในปัจจุบัน

ชี้แจง: จากคำทักท้วงของหลายๆ ท่าน พบว่าจริงๆ แล้ว blog นี้ควรจะชื่อว่า concurrent programming ไม่ใช่ parallel programming อ้างอิงจากคำศัพท์ที่เค้านิยมใช้กันครับ ขออภัยที่ให้ข้อมูลที่ผิดมา ณ​ ที่นี้ด้วยครับ

เมื่ออยู่ในยุคที่การพัฒนาความเร็วของฮาร์แวร์คอมพิวเตอร์เป็นไปด้วยการเพิ่ม core ของ CPU การเขียนโปรแกรมแบบ single thread ก็มีความสามารถไม่เพียงพอต่อการใช้ทรัพยากรของเครื่องให้คุ้มค่าอีกต่อไป เพราะโปรแกรมที่เขียนนั้นจะสามารถใช้งาน CPU ได้เพียงแค่ core เดียว วิธีการเขียนโปรแกรมที่ทำให้สามารถประมวลผลมากกว่า 1 สิ่งต่อหนึ่งช่วงเวลา เพื่อจะได้กระจายงานไปให้แต่ละ core ประมวลผลได้พร้อมๆ กัน เป็นแขนงหนึ่งของ การเขียนโปรแกรมแบบขนาน (parallel programming)

การเขียนโปรแกรมแบบขนานแบบหนึ่งที่เป็นที่นิยมที่สุดในหลายๆ ปีที่ผ่านมา คือ การเขียนโปรแกรมแบบ multi-threading คือการสร้าง thread ขึ้นมาเพื่อทำการประมวลผลแยกจากโปรแกรมหลัก แต่ปัญหาของการเขียนโปรแกรมแบบ multi-threading คือ เป็นการเขียนโปรแกรมที่ต้องการความชำนาญสูง เพราะโปรแกรมอาจจะเกิด race condition หรือ dead lock ได้ง่ายหากเขียนอย่างไม่รอบคอบเพียงพอ ซึ่งส่งผลให้เกิดบั๊กที่หาสาเหตุได้ยาก ทำให้การพัฒนาโปรแกรมเป็นไปได้ช้าและอาจเกิดความผิดพลาดสูง การเขียนโปรแกรมในสไตล์อื่นๆ ที่พยายามทำให้เขียนโปรแกรมแบบขนานได้ง่ายขึ้นจึงได้รับความนิยมมากขึ้นตามลำดับ

สร้างหลายๆ โปรเซส (Process-level parallelism)

เหมือนกับการเขียนโปรแกรมแบบ single thread ปกติทุกอย่าง เพียงแต่สร้าง process ของโปรแกรมขึ้นมาหลายๆ ตัว โดยแต่ละตัวประมวลผลในงานประเภทเดียวกัน แต่ต่างคนต่างทำงานแทบจะไม่เกี่ยวข้องกันเลย

ข้อดี คือ เอาโปรแกรมที่เขียนสำหรับ single thread app มาใช้ได้เลย

ข้อจำกัด 

  • หากเป็นโปรแกรมที่ต้องมีการโต้ตอบกับระบบภายนอกบ่อยครั้ง โปรแกรมนั้นต้องเป็น stateful เพราะมีความเป็นไปได้ที่การโต้ตอบกับระบบภายนอก 2 ครั้งจะไม่เป็นการโต้ตอบกับโปรเซสเดิม
  • External dependency เช่น database ต้องไม่มีปัญหากับการรองรับหลาย concurrent request
  • แต่ละโปรเซสต้องมีการโหลดโปรแกรมที่เหมือนๆ กันขึ้นมาบนเมมโมรี ทำให้เมมโมรีไม่ถูกใช้ให้เกิดประโยชน์สูงสุด และอาจจะไม่พอได้ ซึ่งปัญหานี้สามารถแก้ไขได้ด้วยการใช้เครื่องที่มีแรมเยอะ
  • ช่วงที่มีการเรียก I/O โปรเซสจะต้องมีการหยุดทำงานรอ ทำให้ช่วงเวลานั้น core ไม่ถูกใช้งาน ถือเป็นการสูญเสียทรัพยากรโดยเปล่าประโยชน์

ตัวอย่างของโปรแกรมที่ใช้วิธีนี้ ได้แก่ Rails ที่ใช้ Unicorn เป็น application server

อีเวนท์ (Evented programming)

วิธีนี้โดยตัวมันเองไม่ได้เป็น parallel programming แต่เป็นการพยายามทำให้ CPU ถูกใช้งานอย่างเต็มที่ ไม่ต้องถูกหยุดการใช้งานเมื่อมีการเรียก I/O โดยเมื่อมีการเรียก I/O โปรเซสหลักจะทำการหยุดการทำงานที่ทำอยู่ไว้และสลับไปทำงานอื่นแทน เมื่อ I/O ทำงานเสร็จจะมีการส่ง event กลับไปที่ตัวโปรเซสเพื่อให้กลับมาทำงานต่อ

ข้อดี อย่างที่ได้กล่าวไปแล้ว คือ CPU จะถูกใช้งานอย่างเต็มที่ เนื่องจากไม่ถูกหยุดการทำงานเมื่อมีการเรียก I/O

ข้อจำกัด

  • หากงานหลักของโปรแกรมนั้นเป็นการประมวลผลอย่างหนัก (CPU intensive application) จะมีประสิทธิ์ภาพไม่ต่างจากโปรแกรม single thread ทั่วไป ในจุดนี้อาจใช้การสร้างหลายโปรเซสเข้ามาช่วยได้
  • การเขียนโปรแกรมแบบอีเวนท์ ค่อนข้างจะไล่ flow การทำงานยากกว่าโปรแกรมทั่วไป เพราะการประมวลผลไม่ได้เป็นเส้นตรง ที่มีทางเริ่มและทางออกเพียงแค่ทางเดียว ในเวลาหนึ่งๆ อาจจะเกิด event ที่เป็นจุดเริ่มต้นการประมวลผลได้จากหลายจุด

ตัวอย่างของโปรแกรมที่ใช้วิธีนี้ ได้แก่ Node.js

Software transactional memory (STM)

เหมือน multi-threading แต่มีข้อแตกต่างอยู่ที่ เมื่อจะมีการแก้ไขทรัพยากรที่ใช้งานร่วมกันระหว่าง thread (shared resource) จะมีการสร้าง transaction ขึ้น ซึ่งเมื่อ transaction จะทำการ commit จะมีการเช็คว่า shared resource นั้นเปลี่ยนไปจากตอนเริ่ม transaction หรือไม่ หากมีความแตกต่างก็จะทำการ rollback สิ่งที่ทำไปทั้งหมด และกลับไปทำใหม่ตั้งแต่ต้น transaction

ข้อดี การเขียนโปรแกรมแทบจะเหมือนกับ single thread ทุกอย่าง เพียงแค่ระบุจุดที่จะทำ transaction

ข้อจำกัด

  • อาจเกิดการประมวลผลที่เปล่าประโยชน์จำนวนมาก หากเกิดการ rollback บ่อยๆ
  • เนื่องจากการ implement ระบบ transaction และ rollback ค่อนข้างมีความซับซ้อน โปรแกรมที่สามารใช้ความสามารนี้ได้จึงจำกัดอยู่ในกลุ่ม functional programming ที่มี Immutable data structure ที่สามารถทำการ rollback ได้ง่ายมากเนื่องจากมั่นใจได้ว่าค่าเก่าจะไม่ถูกเปลี่ยนนอก transaction  ไม่ว่ากรณีใดๆ

ตัวอย่างของโปรแกรมที่ใช้วิธีนี้ ได้แก่ Haskell, Clojure, Akka (Scala ของ library)

Actor Model

แก้ปัญหา race condition และ dead lock ของ multi-thread programming ด้วยการบังคับให้แต่ละ thread (หรือโปรเซส) คุยกันผ่านกล่องจดหมายประจำตัวของแต่ละโปรเซส หาก thread หนึ่งต้องการส่งข้อมูลไปให้อีก thread หนึี่งก็จะส่งไปที่กล่องจดหมายของ thread ปลายทาง โดยตัวกล่องจดหมายนี้มีการ implement ด้วย queue ที่ thread ปลายทางนั้นจะดึงจดหมายออกมาประมวลผลทีละ 1 จดหมายต่อหนึ่งช่วงเวลา

ข้อดี คือ โปรแกรมเมอร์ไม่ต้องกังวล dead lock หรือ race condition ใดๆ เลย

ข้อจำกัด

  • โปรแกรมแบบ single thread และ multi-thread ที่มีอยู่แล้วจะไม่สามารถปรับมาใช้ Actor ง่ายๆ เพราะการจัดเลือกสร้าง Actor แต่ละตัว และการสื่อสารระหว่าง Actor เป็นสิ่งที่แตกต่างจากโครงสร้างการเขียนโปรแกรมโดยทั่วไป
  • ต้องการ Immutable data structure เช่นเดียวกับ STM เนื่องจากต้องรับประกันได้ว่าสิ่งที่อยู่ในกล่องจดหมายของ actor แต่ละตัวจะไม่ถูก actor ตัวอื่นมาแก้ไข

ตัวอย่างของโปรแกรมที่ใช้วิธีนี้ ได้แก่ Erlang, Scala, Elixir

Goroutine และ Channel

คล้ายกับ actor แต่มีจุดต่างตรงที่ channel (ซึ่งเปรียบได้กับกล่องจดหมายของ actor) ถูกแชร์ระหว่างหลายๆ  goroutine แทนที่จะขึ้นอยู่กับ goroutine เดียวเหมือนอย่างกล่องจดหมายของ actor

ฟังแล้วอาจจะเหมือนโปรแกรม multi-thread ที่ใช้ immutable queue ในการสื่อสารระหว่าง thread แต่ข้อแตกต่างอยู่ตรงที่ goroutine หลายๆตัวทำงานอยู่ใน thread เดียวกันทำให้ใช้ทรัพยากรน้อยกว่า thread  และ switch การทำงานได้เร็วกว่า โดยที่มี channel เป็นตัวที่คอยกระตุก event และส่ง message ไปที่ goroutine ที่หลับอยู่ เพื่อตื่นขึ้นมาทำงาน

ข้อดี 

  • แต่ละ goroutine ทำงานคล้ายกับระบบ evented (channel เปรียบเสมือน I/O) จึงทำให้ใช้ CPU ได้อย่างเต็มที่ โดยไม่ต้องปวดหัวกับ flow การทำงานแบบ evented
  • สามารถเปลี่ยนโปรแกรมแบบ multi-thread มาใช้ goroutine ได้ง่าย เพราะแนวคิดคล้ายกัน

ข้อจำกัด

  • ถึงแม้ว่า goroutine จะใช้ทรัพยากรน้อย แต่ก็ต้องระวังการ leak ของ goroutine ด้วยเช่นกัน
  • อาจเกิด dead lock ที่ channel ได้

ตัวอย่างของโปรแกรมที่ใช้วิธีนี้ ได้แก่ Go, core.async library ของ Clojure

หวังว่าทุกท่านจะได้รับความเข้าใจในแนวคิดเบื้องต้นของ parallel programming ในแบบต่างๆ กันไปพอสมควรนะครับ

บทความนี้ถึงแม้ผมจะเขียนโดยมิได้อ้างอิงถึงเอกสารใดๆ แต่ความรู้และความเข้าใจที่ผมมีมาก็เกิดจากการฟังและการอ่านมากมายในอดีต ซึ่งผมขออนุญาตไม่ระบุถึงแหล่งเหล่านั้น เพราะไม่สามารถจำได้แล้ว จำได้แค่เพียงว่าสิ่งที่จุดประกายความอยากเข้าใจสไตล์ในแบบต่างๆ คือ talk ของ José Valim ในงาน RedDotRubyConf2013 และ podcast ของ ThinkRelevance ที่สัมภาษณ์ Rich Hickey เรื่อง core.async library

บทความนี้เขียนสรุปขึ้นจากความรู้และความเข้าใจของผมในเรื่องนี้ ซึ่งห่างไกลจากความสมบูรณ์อย่างมาก หากมีความผิดพลาด หรือมีข้อเสนอแนะใดๆ คอมเมนต์มาได้เสมอครับ

Advertisements

2 thoughts on “สไตล์ของ parallel programming ในปัจจุบัน

  1. Remixman∞ (@haoremixman)

    ผมมองว่าสไตล์พวกนี้มันออกไปในเชิง Concurrency มากกว่าอ่ะครับ (ซึ่งจริงๆนิยายมันก็ซ้อนกันไปมา) อาจจะไม่ตรงกับคำว่า Parallel Programming ที่ใช้กันในปัจจุบันเท่าไหร่

    ผมเคยลองลอง Haskell กับ Erlang ดู รู้สึกว่ามันยังไม่เหมาะกับ Parallel เอามากๆเลยครับ โดยเฉพาะกับพวก Data Parallelism (ซึ่งจริงๆมันสามารถทำได้บ้าง ด้วย Map-Reduce แต่ในระดับที่ต้อง Flexible กว่านั้นจะยากมากๆแล้ว)

    ภาษาที่เป็น Parallel จริงๆน่าจะเป็นพวก Charm++, UPC, Fortress, X10, Chapel อะไรพวกนี้ครับ

    Reply
    1. Tap Post author

      อื้ม รู้สึกว่าเรื่องที่เขียนมันเอียงไปทาง concurrent มากกว่า parallel จริงๆ ครับ ตามนิยามที่เค้าใช้กันอยู่ในปัจจุบัน เปรียบเทียบจาก wikipedia ของสองเรื่องนี้

      http://en.wikipedia.org/wiki/Concurrent_computing
      http://en.wikipedia.org/wiki/Parallel_computing
      (แต่ใน parallel programming ก็จัด erlang อยู่ในภาษาหนึ่งอยู่ดี)

      ขอไป edit ด้านต้นเนื้อหาเพิ่มครับ แต่คงไม่แก้เนื้อหาหลักกับหัวข้อนะครับ

      ขอบคุณมากครับ

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s