Topic 33 การทำลาย Temporal Coupling

“ temporal coupling คืออะไรกันแน่” คุณอาจสงสัย มันเกี่ยวกับเรื่องเวลาครับ

เวลาเป็นแง่มุมหนึ่งของสถาปัตยกรรมซอฟต์แวร์ที่มักถูกมองข้าม เวลาที่เรามักครุ่นคิดถึงคือเวลาตามตารางงาน เวลาที่เหลือก่อนจะส่งมอบงาน—แต่นั่นไม่ใช่สิ่งที่เรากำลังพูดถึงในที่นี้ แทนที่จะเป็นแบบนั้น เรากำลังพูดถึงบทบาทของเวลาในฐานะองค์ประกอบการออกแบบของตัวซอฟต์แวร์เอง มีแง่มุมของเวลาสองอย่างที่สำคัญกับเรา นั่นคือ concurrency (สิ่งต่างๆ ที่เกิดขึ้นพร้อมกัน) และ ordering (ตำแหน่งสัมพัทธ์ของสิ่งต่างๆ ในเวลา)

เรามักไม่ได้เข้าหาการเขียนโปรแกรมโดยคำนึงถึงแง่มุมทั้งสองนี้เลยครับ เวลาที่คนทั่วไปนั่งลงออกแบบสถาปัตยกรรมหรือเขียนโปรแกรมครั้งแรก สิ่งต่างๆ มักจะเป็นเส้นตรง นั่นคือวิธีที่คนส่วนใหญ่คิด— ทำสิ่งนี้ แล้วก็ ทำสิ่งนั้น เสมอ แต่การคิดแบบนี้นำไปสู่ temporal coupling หรือการ coupling ในเชิงเวลา เมธอด A ต้องถูกเรียกก่อนเมธอด B เสมอ มีรายงานได้เพียงฉบับเดียวที่รันได้ในแต่ละครั้ง คุณต้องรอให้หน้าจอวาดใหม่เสร็จก่อนที่การคลิกปุ่มจะถูกรับรู้ Tick ต้องเกิดก่อน tock

แนวทางนี้ไม่ยืดหยุ่นเท่าไหร่ และก็ไม่ค่อยสมจริงด้วยครับ

เราต้องเปิดทางให้กับ concurrency และคิดถึงการตัดขาดการพึ่งพาด้านเวลาหรือลำดับต่างๆ ออกไป เมื่อทำแบบนี้ เราก็จะได้ความยืดหยุ่นเพิ่มขึ้นและลดการพึ่งพาที่อิงกับเวลาในหลายๆ ด้านของการพัฒนา ทั้ง workflow analysis, สถาปัตยกรรม, การออกแบบ และการ deploy ผลลัพธ์ที่ได้คือระบบที่เข้าใจง่ายขึ้น ตอบสนองได้เร็วและน่าเชื่อถือมากขึ้นด้วยครับ

Looking for Concurrency (มองหา Concurrency)

ในหลายๆ โปรเจกต์ เราต้องโมเดลและวิเคราะห์ workflow ของแอปพลิเคชันเป็นส่วนหนึ่งของการออกแบบ เราอยากรู้ว่าอะไร สามารถ เกิดขึ้นพร้อมกันได้ และอะไร ต้อง เกิดขึ้นตามลำดับที่ตายตัว วิธีหนึ่งในการทำแบบนี้คือการจับภาพ workflow ด้วยสัญลักษณ์อย่าง activity diagram [46]

Tip 56 วิเคราะห์ Workflow เพื่อปรับปรุง Concurrency

activity diagram ประกอบด้วยชุดของ action ที่วาดเป็นกล่องมุมโค้ง ลูกศรที่ออกจาก action หนึ่งจะนำไปสู่ action ถัดไป (ซึ่งเริ่มได้ทันทีที่ action แรกเสร็จสิ้น) หรือไปยังเส้นหนาที่เรียกว่า synchronization bar เมื่อ ทุก action ที่นำเข้าสู่ synchronization bar เสร็จสิ้นแล้ว คุณก็สามารถดำเนินต่อไปตามลูกศรใดก็ได้ที่ออกจากแถบนั้น ส่วน action ที่ไม่มีลูกศรนำเข้ามาเลยสามารถเริ่มได้ทุกเมื่อ

คุณสามารถใช้ activity diagram เพื่อเพิ่มความขนาน (parallelism) ให้สูงสุด โดยระบุกิจกรรมที่ สามารถ ทำแบบขนานกันได้ แต่ยังไม่ได้ทำ

ตัวอย่างเช่น สมมติเรากำลังเขียนซอฟต์แวร์สำหรับหุ่นยนต์ทำพิญาโคลาดา เราได้รับแจ้งว่าขั้นตอนมีดังนี้ครับ

เปิดเครื่องปั่นเปิดซองพิญาโคลาดามิกซ์ใส่มิกซ์ลงในเครื่องปั่นตวงเหล้ารัมขาว 1/2 ถ้วยเทเหล้ารัมลงไปเติมน้ำแข็ง 2 ถ้วย ปิดเครื่องปั่นปั่นให้เหลวเป็นเวลา 1 นาทีเปิดเครื่องปั่นหยิบแก้วหยิบร่มสีชมพูเสิร์ฟ

แต่บาร์เทนเดอร์ที่ทำตามขั้นตอนนี้ทีละขั้นตอนตามลำดับคงต้องตกงานแน่ๆ แม้ว่าขั้นตอนเหล่านี้จะถูกอธิบายแบบเรียงลำดับ (serially) แต่หลายขั้นตอนก็สามารถทำแบบขนานกันได้ เราจะใช้ activity diagram ต่อไปนี้เพื่อจับภาพและคิดวิเคราะห์ความเป็นไปได้ของ concurrency ครับ

An activity diagram represents the task carried out in a robotic piña coloda maker.

activity diagram นี้แสดงความเป็นไปได้ของ concurrency ในงานของหุ่นยนต์ทำพิญาโคลาดา มีทั้งหมด 12 งาน งานแรกคือเปิดเครื่องปั่น และงานที่สองคือเปิดมิกซ์เกิดขึ้นแบบขนานกัน ในทำนองเดียวกัน งานแรกคือเปิดเครื่องปั่นและงานที่สี่คือตวงเหล้ารัมก็เกิดขึ้นแบบขนานกันด้วย งานที่สามคือใส่มิกซ์ลงในเครื่องปั่น งานที่หกคือเติมน้ำแข็งสองถ้วย และงานที่ห้าคือเทเหล้ารัมลงไปก็เกิดขึ้นแบบขนานกัน งานที่ห้านำไปสู่งานที่เจ็ดคือปิดเครื่องปั่น งานที่แปดคือปั่น งานที่เก้าคือเปิดเครื่องปั่น งานที่สิบคือหยิบแก้ว งานที่สิบเอ็ดคือหยิบร่มสีชมพู และงานที่เก้าคือเปิดเครื่องปั่นนำไปสู่งานสุดท้ายคือเสิร์ฟ

การได้เห็นว่าจริงๆ แล้ว dependency อยู่ตรงไหนนั้นเปิดหูเปิดตาได้ไม่น้อยครับ ในกรณีนี้ งานระดับบนสุด (1, 2, 4, 10 และ 11) สามารถเกิดขึ้นพร้อมกันได้ทั้งหมดตั้งแต่ต้น ส่วนงานที่ 3, 5 และ 6 สามารถเกิดขึ้นแบบขนานกันได้ในภายหลัง ถ้าคุณอยู่ในการแข่งขันทำพิญาโคลาดา การปรับให้เหมาะสมแบบนี้อาจสร้างความแตกต่างได้มากทีเดียว

Faster Formatting

หนังสือเล่มนี้เขียนด้วยข้อความล้วน (plain text) การจะสร้างเวอร์ชันที่พร้อมพิมพ์ หรืออีบุ๊ก หรืออะไรก็ตาม ข้อความนั้นจะถูกป้อนผ่านไปป์ไลน์ของตัวประมวลผลหลายตัว บางตัวมองหาโครงสร้างเฉพาะ (การอ้างอิงบรรณานุกรม รายการดัชนี มาร์กอัปพิเศษสำหรับ tip และอื่นๆ) ตัวประมวลผลอื่นๆ ทำงานกับเอกสารทั้งหมดในภาพรวม

ตัวประมวลผลหลายตัวในไปป์ไลน์ต้องเข้าถึงข้อมูลภายนอก (อ่านไฟล์ เขียนไฟล์ ส่งผ่านไปยังโปรแกรมภายนอก) งานที่ค่อนข้างช้าเหล่านี้ทั้งหมดเปิดโอกาสให้เราใช้ประโยชน์จาก concurrency ได้ จริงๆ แล้วแต่ละขั้นตอนในไปป์ไลน์ทำงานพร้อมกัน โดยอ่านจากขั้นตอนก่อนหน้าและเขียนไปยังขั้นตอนถัดไป

นอกจากนี้ บางส่วนของกระบวนการยังใช้ทรัพยากรตัวประมวลผลค่อนข้างมาก หนึ่งในนั้นคือการแปลงสูตรคณิตศาสตร์ ด้วยเหตุผลทางประวัติศาสตร์หลายอย่าง สมการแต่ละตัวอาจใช้เวลาแปลงนานถึง 500 มิลลิวินาที เพื่อเร่งความเร็ว เราใช้ประโยชน์จาก parallelism เพราะแต่ละสูตรเป็นอิสระจากกัน เราจึงแปลงแต่ละสูตรใน process แบบขนานของตัวเอง แล้วรวบรวมผลลัพธ์กลับเข้าสู่หนังสือเมื่อพร้อมใช้งาน

ผลก็คือหนังสือเล่มนี้ build เร็วขึ้นมากบนเครื่องที่มีหลายคอร์

(และใช่ครับ ระหว่างทางเราก็เจอ concurrency error หลายจุดในไปป์ไลน์ของเราเหมือนกัน….)

Opportunities for Concurrency (โอกาสสำหรับ Concurrency)

Activity diagram แสดงพื้นที่ที่มีศักยภาพสำหรับ concurrency แต่ไม่ได้บอกอะไรเลยว่าพื้นที่เหล่านั้นคุ้มค่าที่จะใช้ประโยชน์หรือไม่ ยกตัวอย่างเช่น ในตัวอย่างพิญาโคลาดา บาร์เทนเดอร์คงต้องมีมือห้าข้างถึงจะรันงานเริ่มต้นที่มีศักยภาพทั้งหมดพร้อมกันได้

และตรงนี้แหละที่ส่วนของการออกแบบเข้ามามีบทบาท เมื่อเรามองดูกิจกรรมต่างๆ เราจะเห็นว่างานหมายเลข 8 คือปั่นให้เหลว จะใช้เวลา 1 นาที ในระหว่างนั้น บาร์เทนเดอร์ของเราสามารถไปหยิบแก้วและร่ม (กิจกรรม 10 และ 11) และอาจจะยังมีเวลาเหลือพอเสิร์ฟลูกค้าอีกคนได้ด้วย

และนั่นคือสิ่งที่เรากำลังมองหาเมื่อเราออกแบบเพื่อ concurrency เราหวังจะหากิจกรรมที่ใช้เวลา แต่ไม่ใช่เวลาในโค้ดของเรา การ query ฐานข้อมูล การเข้าถึง external service การรอ input จากผู้ใช้ สิ่งเหล่านี้ทั้งหมดปกติแล้วจะทำให้โปรแกรมของเราหยุดชะงักจนกว่ามันจะเสร็จสิ้น และนี่ก็คือโอกาสทั้งหมดที่จะทำอะไรที่มีประโยชน์มากกว่าการให้ CPU นั่งบิดนิ้วมือรอเฉยๆ ครับ

Opportunities for Parallelism (โอกาสสำหรับ Parallelism)

อย่าลืมความแตกต่างนี้ครับ concurrency เป็นกลไกด้านซอฟต์แวร์ ส่วน parallelism เป็นเรื่องของฮาร์ดแวร์ ถ้าเรามีตัวประมวลผลหลายตัว ไม่ว่าจะอยู่ในเครื่องเดียวกันหรือกระจายกันอยู่ก็ตาม แล้วเราสามารถแบ่งงานออกไปให้มันได้ เราก็จะลดเวลารวมที่ใช้ทำงานได้

สิ่งที่เหมาะจะแบ่งแบบนี้คือชิ้นงานที่ค่อนข้างเป็นอิสระจากกัน โดยที่แต่ละชิ้นดำเนินต่อไปได้โดยไม่ต้องรออะไรจากชิ้นอื่น รูปแบบที่พบได้ทั่วไปคือการเอางานชิ้นใหญ่ แบ่งเป็นชิ้นย่อยที่เป็นอิสระต่อกัน ประมวลผลแต่ละชิ้นแบบขนาน แล้วค่อยรวมผลลัพธ์เข้าด้วยกัน

ตัวอย่างที่น่าสนใจของแนวทางนี้ในทางปฏิบัติคือวิธีที่ compiler ของภาษา Elixir ทำงาน เมื่อมันเริ่มทำงาน มันจะแบ่งโปรเจกต์ที่กำลัง build ออกเป็นโมดูลต่างๆ แล้ว compile แต่ละโมดูลแบบขนานกัน บางครั้งโมดูลหนึ่งขึ้นอยู่กับอีกโมดูลหนึ่ง ในกรณีนั้นการ compile ของมันจะหยุดรอจนกว่าผลลัพธ์ของการ build โมดูลอีกตัวจะพร้อมใช้งาน เมื่อโมดูลระดับบนสุด compile เสร็จ นั่นหมายความว่า dependency ทั้งหมดถูก compile เรียบร้อยแล้ว ผลลัพธ์ที่ได้คือการ compile ที่รวดเร็วซึ่งใช้ประโยชน์จากคอร์ทั้งหมดที่มีอยู่

Identifying Opportunities Is the Easy Part (การหาโอกาสคือส่วนที่ง่าย)

กลับมาที่แอปพลิเคชันของคุณ เราได้ระบุจุดต่างๆ ที่จะได้ประโยชน์จาก concurrency และ parallelism แล้ว ทีนี้มาถึงส่วนที่ยาก นั่นคือเราจะ implement มันอย่างปลอดภัยได้อย่างไร นั่นคือหัวข้อของส่วนที่เหลือในบทนี้ครับ

Related Sections Include (หัวข้อที่เกี่ยวข้อง)

  • Topic 10, ​ Orthogonality ​
  • Topic 26, ​ How to Balance Resources ​
  • Topic 28, ​ Decoupling ​
  • Topic 36, ​ Blackboards ​

Challenges (แบบฝึกหัด)

  • ตอนเช้าที่คุณเตรียมตัวไปทำงาน คุณทำงานกี่อย่างพร้อมกันบ้างครับ คุณจะแสดงสิ่งนี้ด้วย UML activity diagram ได้ไหม คุณพอจะหาวิธีเตรียมตัวให้เสร็จเร็วขึ้นได้ไหมด้วยการเพิ่ม concurrency