Tuesday, July 19, 2016

ตอบเรื่อง java thread safety

จาก post ในกลุ่ม THJUG บน facebook มีคนชวนคุยเรื่อง thread safety ใน java ตามนี้ครับ





เหมือนเดิมครับ ตอบยาว เลยเอามาแปะบน blog เพื่อไม่ให้มันหายไปกับกาลเวลา...


เรื่อง thread safety ผมเห็นว่าเป็นรูปแบบนึงของผลกระทบจาก mutability + concurrency ครับ ปัญหานี้มีทุกภาษา แต่จะชัดและปวดหัวมากกับภาษาที่ไม่มีวิธีบังคับ immutability

เรื่องนี้หลักการไม่ยาก ถ้าคิดถูกแต่แรกก็เขียนให้ถูกง่าย แต่การ test ว่าถูกรึเปล่าบางทีก็ไม่ง่ายครับ เพราะเวลาผิด มันไม่ได้ตายทุกครั้งเนี่ยแหละ  ... "ระบบผิด ไม่โหดเท่า ระบบผิดบางครั้ง"

การแก้ปัญหา มีหลายแบบ แต่ละแบบก็มีข้อดีข้อเสียและลักษณะปัญหาที่ถูกโฉลกต่างกันไป ทำให้ถูกอย่างเดียวไม่ยากครับ lock แหลก แต่ทำยังไงให้ ทั้งถูก ทั้งเร็ว ทำยังไงให้ระบบเรารับ request จำนวนมาก ๆ ที่อิงกับ resource ที่ต้องใช้ร่วมกันแล้วถูกเสมอ นี่คือปัญหา

การ lock หรือที่เรียกกันใน java ว่า synchronized จะที่ method หรือที่ instance ก็เป็นเรื่องห้ามใช้ ถ้าจะทำระบบที่ high-performant (ไม่ถูก grammar เนอะ) เพราะ thread มันแพง server 1 ตัว รองรับ os thread ได้จำกัด ถ้า thread ไหนโดน lock ปุ๊บ thread นั้นจะนอนนิ่ง ๆ ทันที ไม่มีใครเอาไปใช้งานได้ ถ้า app ไหนมี lock แบบนี้แล้ว thread มากองเยอะ ๆ นี่ app หนืดได้ในไม่กี่วินาที นอกจากนี้ยังต้องระวังเรื่อง deadlock อีก ต้องเล็งดี ๆ เมื่อไหร่ wait เมื่อไหร่ notify เกิด lock พลาดเจอ deadlock ระบบก็หลับยาว..

ส่วนเรื่องทำให้ immutable โดยไม่ให้มี setter อย่างเดียวก็อาจจะไม่รอดครับ เช่น ถ้า private field เป็น final list แล้วมีการเรียก list.add(..) ต้องเพิ่มนิดนึงว่า field ใต้ instance นั้นต้อง immutable ด้วย

จริง ๆ ต้องบอกว่า immutability แค่ทำให้เราวิเคราะห์พฤติกรรมของโปรแกรมเราได้ง่าย มันไม่ได้รับประกันว่าจะไม่เจอปัญหานะครับ เพราะ software ที่ใช้งานจริง ยังไงก็ต้องมีการ mutate ค่า ถึงเราทำชิ้นย่อย ๆ ของเราให้ immutable แต่ยังไง layer บน ๆ ก็ต้อง mutate อะไรซักอย่าง

แล้วทางแก้ race condition ดี ๆ แบบ lock-free ล่ะ ? เอาเท่าที่ผมเคยใช้นะครับ

1. actor ตัวนี้ต้นตอมาจากไหนไม่รู้ แต่ดังโดย erlang และลอกโดย scala ถ้าชาว java จะใช้อาจจะต้องดู lib ชื่อ akka จริง ๆ มันทำประโยชน์ได้หลายอย่างมาก แต่สำหรับเรื่อง race condition หลักการมีแค่ว่า มี resource ที่จำกัดกี่ตัว ก็ให้มี actor เป็นตัวคุม resource เท่านั้นตัว เช่น ระบบเรา read x พร้อมกันได้เยอะ ๆ แต่ห้าม mutate x พร้อมกัน เราก็ออกแบบให้มี mutator actor ตัวเดียวที่คอย mutate x ใครอยาก mutate x ให้บอก mutator actor ให้ทำให้ หรือ ระบบ legacy หลังบ้านเรารับ api ได้แค่ 5 concurrent เท่านั้น เราก็มี actor สำหรับคอยยิง api 5 ตัวแบ่งงานกันทำ แค่นี้ทำยังไงก็ไม่มีโอกาสที่ใครจะมา mutate x หรือยิง api > 5 ตัวพร้อมกันแล้ว ข้อเสียมีหลายอย่างเหมือนกัน แต่สำหรับชาว java ไม่ต้องสนครับ java มีข้อเสียพวกนั้นครบอยู่แล้วถึงไม่ได้ใช้ actor ก็ตาม

2. software transactional memory (stm) นึกถึง optimistic locking ใน hibernate ครับ ตอนเรา load row มา เราจะมี tag เก็บไว้ (version) พอเราจะ commit ถ้า tag ไม่ตรงก็คือมีคนอื่นชิง write ก่อนเรา เราเจ๊ง ต้อง load data สดใหม่มาทำขั้นตอนเดิมอีกรอบเอง แต่ stm คือเราเขียน code แล้วบอกว่า code นี้ให้เช็ค tag ให้ด้วย ถ้า tag ตอน load กับตอน commit ไม่ตรง มันจะ re-run code block ให้เราเองไปเรื่อย ๆ จนกว่าจะผ่าน ข้อดีคือเราเขียน code ได้ธรรมชาติมาก เหมาะใช้กับงานที่เรา "คิดว่าคง conflict ไม่บ่อย" เพราะถ้า conflict บ่อยจะเปลืองตรงการทำอีกครั้ง ข้อเสียคือ ใช้กับ IO ไม่ได้ เช่น เราเขียน socket เสร็จ แต่เจอว่าระหว่างนั้นมีใคร mutate ตัวแปรเรา เราดึงข้อความที่เราเขียนกลับมาไม่ได้แล้ว รู้สึกตัวนี้จะมีใน akka เหมือนกัน

3. channel ไม่กล้าเรียก communicating sequential processes (csp) เพราะผมไม่รู้ว่า implement/prove แบบไหนถึงเรียกว่า csp แค่ได้ยินว่า channel ใน golang เป็น csp พอดีผมใช้ channel ตอนเขียน haskell (เป็นแค่ lib ตัวนึง) ดู implementation แล้วก็ไม่ได้มีอะไรพิเศษ  (implementation จริงของ haskell โกงครับ ใช้ lock แต่เป็น lock บน green threads ก็เลยไม่กิน os thread)  การใช้งานก็คล้าย ๆ actor ครับ มีคนเขียน มีคนอ่าน มี fan-out ถ้ามี resource อะไรจำกัดก็ให้มีคนจัดการ resource นั้นเท่านั้นตัว ใครอยากยุ่งกับ resource นั้นก็ส่งข้อมูลผ่าน channel ข้ามไปข้ามมา  แต่อันนี้ไม่รู้มีใน akka รึเปล่า