Objective-C Programming Chapter 19 (Part1)

Grand Central Dispatch

 

ทุกวันนี้เทคโนโลยีของ CPU ได้ก้าวหน้าไปมาก จากแต่เดิมที่มุ่งเน้นการเพิ่มความเร็วของสัญญานาฬิกา ได้เปลี่ยนมาเป็นเพิ่มจำนวนแกนของ CPU หรือที่เรียกว่า multicore แต่การที่จะใช้ซีพียูให้เกิดประสิทธิภาพสูงที่สุดนั้น โปรแกรมจำเป็นต้องสามารถทำงานได้หลายอย่างพร้อมกันได้ ในอดีตการที่โปรแกรมที่ใช้ซีพียูแบบ multiple core นั้นจะมีการสร้าง thread หลายๆตัวเพื่อให้แต่ละ thread ได้ใช้แกนของซีพียูได้หลายแกนพร้อมกัน แต่อย่างไรก็ตามปัญหาที่ตามมาก็คือการเพิ่มจำนวน thread นั้นในบางครั้ง ไม่ได้ช่วยให้เกิดประสิทธิภาพมากขึ้น เพราะเมื่อถึงจุดหนึ่งจำนวนของเทรดจะมีผลต่อประสิทธิภาพของโปรแกรม ดังนั้นโปรแกรมเมอร์จึงต้องหาจุดที่จำนวนเทรดเหมาะสมพอดีกับซีพียู ไม่มากไปหรือไม่น้อยไป และถึงแม้ว่าจะหาจำนวนของเทรดที่พอดีได้แล้วก็ได้ตาม ก็ยังเป็นเรื่องยากที่จะต้องจัดการแต่ละ thread ไม่ให้ขัดขวางการทำงานซึ่งกันและกัน เพื่อแก้ปัญหาต่างๆเหล่านี้ Apple จึงได้สร้างเทคโนโลยีใหม่ขึ้นมาที่เรียกว่า Grand Central Dispatch ( GCD ) ซึ่งเป็นเทคโนโลยีใหม่ ที่ได้นำมาใช้กับระบบปฎิบัติการ Mac OS X และ iOS เพื่อจัดการปัญหาเหล่านี้
เทคนิคการทำงานเบื้องหลังของ GDC คือ Asynchronous Function ที่ได้อธิบายไปแล้วในบทก่อนว่า เป็นการทำงานแบบอนุญาติให้ฟังชั่นอื่นเริ่มทำงานได้ทันที โดยต้องไม่ต้องรอให้ฟังชั่นที่กำลังทำงานในปัจจุบันเสร็จสิ้นเสียก่อน แม้ว่าเทคนิคการทำงานแบบนี้มีมานานแล้ว แต่โปรแกรมเมอร์ต้องเป็นคนเขียนโค้ด asynchronous function เองรวมไปถึงสร้าง thread ที่ใช้ทำงานร่วมกับฟังชั่นด้วยตัวเองทั้งหมด แต่หลังจากที่มีเทคโนโลยี GCD เข้ามา งานของโปรแกรมเมอร์ก็ลดน้อยลงเพราะไม่ต้องเขียนโครงสร้างการทำงานแบบ asynchronous function และโค้ดที่ใช้สร้างและจัดการ thread สิ่งที่โปรแกรมเมอร์ต้องทำจะเหลือแต่เพียงแค่การเขียนโค้ดของงานที่ต้องทำจริงๆ จากนั้นหลังจากนั้นก็ส่งต่อเข้าไปยัง dispatch queue ที่เหลือจะเป็นหน้าที่ของ GCD ที่จะจัดการสร้าง thread รวมไปถึงการบริหารเวลาการทำงานให้กับ CPU แต่ละ core เอง การใช้ GCD สามารถทำได้สองทางด้วยกันคือใช้ Dispatch Queues ซึ่งเป็น C API และทางที่สองใช้ NSOperationQueue และในบทนี้จะได้ทำความเข้าใจการใช้งานทั้งสองแบบ

Dispatch Queues

ดิสแพทช์คิวคือเครื่องมือใช้ในการจัดการงานหรือที่เรียกว่า task (ต่อไปนี้จะใช้คำว่า task) อะไรควรทำก่อนหรือหลัง เช่น โหลดข้อมูลจากเซิฟเวอร์ , อ่านข้อมูลจากฮาร์ดดิส , คำนวนค่าต่างๆ การประกาศ task สามารถเขียนด้วยฟังชั่น หรือจะใช้บล็อกก็ได้เช่นเดียวกัน การจัดลำดับการทำงานของ dispatch queues เป็นแบบ FIFO (First In – First Out) หาก task ใดเข้ามาในคิวก่อนก็จะได้รับสิทธิให้ทำงานก่อน ดิสแพทช์คิวได้แบ่งออกเป็นทั้งหมด 3 แบบด้วยกันคือ

Serial (private dispatch queue) เหมาะกับงานที่ต้องใช้ทรัพยากรร่วมกัน (shared resource) เนื่องจากดิสแพทช์คิวแบบนี้ อนุญาติให้ task ทำงานได้ทีละหนึ่ง task เท่านั้น ผู้ใช้สามารถสร้างคิวแบบนี้ได้ไม่จำกัด และดิสแพทช์คิวแต่ละคิวจะแยกการทำงานกันออกจากกัน เช่น โปรแกรมมีทั้งหมด 3 คิว โดยแต่ละคิวก็มี task เป็นของตัวเอง เมื่อให้โปรแกรมทำงาน คิวทั้ง 3 สามารถที่จะทำงานไปพร้อมกันได้ อย่างไรก็ตามคิวแต่ละคิวจะอนุญาติให้ task ได้ทำงานได้เพียงทีละ task เท่านั้น

serial

Concurrent (global dispatch queue) เป็นคิวที่อนุญาติให้ task ทำงานพร้อมกันได้มากกว่าหนึ่ง task และยังทำงานตามลำดับของ task ที่เข้ามาในดิสแพทช์คิว จำนวนของคิวที่สามารถสร้างได้นั้นขึ้นอยู่ทรัพยากรของระบบ เมื่อดิสแพทช์คิวทำงาน คิวจะสร้างเทรดขึ้นมาเพื่อรองรับ task และมีการบริหารจัดการโดยคิวเอง เช่นเดียวกับคิวแบบ serial

concurrent

Main dispatch queue อนุญาติให้ task ทำงานได้พร้อมกันเช่นเดียวกันกับ concurrent ดิสแพทช์คิวนี้จะไม่สร้างเทรดใหม่เหมือนกับสองคิวทีผ่านมา แต่จะให้ task ทำงานที่ Application Main Thread แทน คิวนี้มีการทำงานที่ main thread ดังนั้นผู้ใช้จะไม่ได้สร้างคิวขึ้นมาเอง แต่จะเรียกใช้ฟังชั่นเพื่อขอ main dispatch queue จากระบบ
เมื่อเข้าใจการทำงานของคิวทั้งสามแบบแล้ว ต่อไปก็ลงมือเขียนโปรแกรมกัน ซึ่งเป็นตัวอย่างการใช้งาน GCD อย่างง่ายๆ เช่น การดาวน์โหลด รูปภาพจากอินเทอร์เน็ต พร้อมกันหลายๆรูป

Program 19.1

main.m

 

อันดับแรกคือการสร้างคิวขึ้นมาใช้งาน สามารถทำได้ด้วยฟังชั่น dispatch_queue_create พร้อมกับส่งชื่อของคิวที่ต้องการจะสร้างเป็นพารามิเตอร์แรก ส่วนพารามิเตอร์ที่สองเป็นการกำหนดลักษณะการทำงานของคิว เช่น serial หรือ concurrent ในกรณีไม่ได้กำหนดจะเป็น serial โดยอัตโนมัติ

 

หลังจากประกาศคิวเสร็จเรียบร้อยแล้ว ก็เพิ่ม task ให้กับคิวด้วยคำสั่ง dispatch_async ซึ่งพารามิเตอร์แรกก็คือคิวที่ต้องการให้ task ทำงาน ส่วนพารามิเตอร์ที่สองเป็นโค้ดบล็อกของ task ที่ต้องการจะให้ทำงาน ( โค้ดตัวอย่างใช้เมธอดของ NSData เพื่อใช้ดาวน์โหลดรูป สำหรับการใช้งานจริงควรใช้ NSURLSession )

การใช้คำสั่ง dispatch_async นั้นเป็นการเพิ่ม task ให้ทำงานแบบ asynchronous ซึ่งหมายความว่าไม่ต้องรอให้ทำงานเสร็จ แต่เนื่องจากโปรแกรมที่เราเขียนเป็นแบบ console ถ้าหากให้โปรแกรมทำงานก็จะปิดตัวอย่างรวดเร็ว เพราะ main ไม่ต้องรอให้ task ทำงานเสร็จก่อน ดังนั้นแล้วจึงจำเป็นต้องเขียน while loop เพื่อรอให้ task ทำงานเสร็จเสียก่อน และเมื่อให้โปรแกรมทำงานก็จะเห็นผลลัพธ์ดังนี้

Program 19.1 Output

Start
1 Download: http://www.sampleserver.th/image_1.jpg completed
2 Download: http://www.sampleserver.th/image_2.jpg completed
3 Download: http://www.sampleserver.th/image_3.jpg completed
4 Download: http://www.sampleserver.th/image_4.jpg completed
Finish

เราได้เห็นถึงการสร้าง serial queue คิวอย่างง่ายกันไปแล้ว ส่วนการสร้างดิสแพทช์คิวแบบ concurrent ทำได้ด้วยการใช้เมธอด dispatch_queue_create เช่นเดียวกัน แต่กำหนดพารามิเตอร์ที่สองเป็น DISPATCH_QUEUE_CONCURRENT นอกจากวิธีนี้แล้ว ยังสามารถที่จะขอใช้ global dispatch queue ของระบบด้วยการใช้ฟังชัน dispatch_get_global_queue เมื่อขอดิสแพทช์คิวของระบบต้องระบุลำดับความสำคัญ (priority) ของคิว ซึ่งมีอยู่ด้วยกันทั้งหมด 4 ลำดับ คือ high , default , low และ background ซึ่งคิวที่มีลำดับความสำคัญต่ำ ก็จะทำงานหลังคิวที่มีลำดับสูงกว่า  เราจะแก้ไขโปรแกรม 19.1 เพื่อให้เปลี่ยนไปใช้ global dispatch queue แทนการสร้างคิวขึ้นมาเอง ด้วยโค้ดต่อไปนี้

พารามิเตอร์แรกคือลำดับความสำคัญของคิว และพารามิเตอร์ที่สอง SDK ปัจจุบัน (Mac OS X 10.8 ,iOS 7 ) ยังไม่ได้ใช้งานใดๆ เมื่อให้โปรแกรมทำงานก็จะได้ผลดังนี้

Program 19.1 Output ( Global Queue )

Start
1 Download: http://www.sampleserver.th/image_1.jpg completed
2 Download: http://www.sampleserver.th/image_4.jpg completed
3 Download: http://www.sampleserver.th/image_3.jpg completed
4 Download: http://www.sampleserver.th/image_2.jpg completed
Finish

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

ถ้าต้องการรอให้ task ทำงานไปจนเสร็จ ก็สามารถที่จะเพิ่ม task แบบ synchronous ได้ด้วยคำสั่ง dispatch_sync ดังเช่นโปรแกรมต่อไปนี้

Program 19.2

main.m

โค้ดของโปรแกรม 19.2 แทบจะเหมือนกับโปรแกรม 19.1 สิ่งที่เปลี่ยนไปก็คือ การเปลี่ยนมาใช้คำสั่ง dispatch_sync ซึ่งเมื่อใช้คำสั่งนี้ โปรแกรมจะรอให้ task เสร็จสิ้นเสียก่อน ถึงจะทำอย่างอื่น ดังนั้นโปรแกรมนี้จึงไม่ต้องเขียนโค้ด while loop เพื่อรอให้ task ทำงานเสร็จ ดังเช่นโปรแกรมที่ผ่านมา

Program 19.2 Output

Start
Download: http://www.sampleserver.th/image_1.jpg completed
Download: http://www.sampleserver.th/image_2.jpg completed
Download: http://www.sampleserver.th/image_3.jpg completed
Download: http://www.sampleserver.th/image_4.jpg completed
Finish

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

จากโค้ดตัวอย่างโปรแกรมจะแสดงผลลัพธ์คือ 1234 หรือ 2143 หรือ 2413 อย่างใดอย่างหนึ่ง เพราะ asynchronous ไม่ต้องรอให้ task จบการทำงาน ดังนั้นแล้ว 2 ซึ่งไม่ได้อยู่ในคิวจึงอาจจะแสดงผลก่อน 1 หรือทีหลังก็ได้ ขึ้นอยู่กับระบบเป็นคนจัดการ  แต่จะเห็นว่าผลลัพธ์ 1 นั้นจะแสดงก่อน 3 เสมอ เพราะการทำงานของ 1 และ 3 อยู่ในคิวที่เป็นแบบ serial ที่อนุญาติให้ทำงานได้ทีละอย่างเท่านั้น  ลองพิจารณาโค้ดอีกสักตัวอย่าง

จากตัวอย่างโปรแกรมจะแสดงผลลัพธ์ 1234 เสมอ นั่นก็เพราะว่า synchronous นั้น ต้องรอให้ task ทำงานเสร็จก่อน ถึงจะทำอย่างอื่นนั่นเอง โปรแกรมที่ได้เขียนผ่านมาใช้คิวทั้งแบบ concurrent และ serial กันไปแล้ว แต่ยังขาด main dispatch queue โปรแกรมต่อไปที่จะเขียน จะเป็นตัวอย่างการใช้งาน main dispatch queue ซึ่งมีโค้ดตัวอย่างการใช้งานดังนี้

Program 19.3

main.m

โปรแกรมได้เรียกใช้ฟังชั่น dispatch_async สองครั้ง ครั้งแรกได้กำหนด task ให้ทำหน้าที่ดาวน์โหลดรูปภาพ เมื่อได้รูปภาพเสร็จเรียบร้อยแล้วก็เรียก dispatch_async อีกครั้ง การเรียกใช้ฟังชันทั้งสองนี้แตกต่างกัน ครั้งแรกกำหนดให้ task ทำงานในคิวที่ผู้ใช้งานสร้างขึ้น เมื่อโปรแกรมทำงานจะสร้างเทรดขึ้นมารองรับ task การทำงานของคิวนี้ ส่วนครั้งที่่สองนั้นเรียกใช้ฟังชัน dispatch_get_main_queue เพื่อกำหนดให้ task ทำงานใน main thread ซึ่งเป็นเทรดหลักของโปรแกรม
โปรแกรมแบบ console นี้อาจจะยังไม่เห็นประโยชน์ของการให้ task ทำงานที่ main thread มากนัก แต่หากเป็นโปรแกรมที่มี user interface เช่น iOS หรือ Mac OS X จะมีประโยชน์เป็นอย่างมาก เพราะหลังจาก task ทำงานเสร็จเรียบร้อยแล้วเช่นการอ่านข้อมูล สิ่งที่มักจะทำต่อมาก็คือการนำข้อมูลนั้นมาแสดงในทันที การกำหนดให้ task ที่เกี่ยวกับ UI ทำงานที่ main thread นี้ก็เพื่อให้โปรแกรมได้แสดงข้อมูลได้รวดเร็วมากที่สุดและไม่เกิดอาการสะดุดหรือ freezing นั่นเอง

Dispatch Group

ถ้าต้องการรอให้ task ทำงานเสร็จอาจจะกำหนดให้ task ทำงานแบบ synchronous ดังเช่นตัวอย่างที่ผ่านมา แต่การทำงานแบบนี้ต้องรอให้ task ทำงานทีละ task ตามลำดับ นอกจากวิธีการนี้ยังมีอีกหนึ่งวิธีการที่ใช้รอ task หนึ่งหรือหลายๆ task ทำงานเสร็จั่นก็คือ dispatch group ข้อดีของการใช้ dispatch group คือ task สามารถทำงานได้พร้อมกัน ดังเช่นตัวอย่างต่อไปนี้

Program 19.4

main.m

Program 19.4 Output

Task B
Task C
Task A
All done

จากผลลัพธ์ของโปรแกรมจะเห็นว่าโปรแกรมได้รอให้ task ทั้งสามทำงานจนเสร็จก่อนที่จะแสดงผลลัพธ์ All done และ task ทั้งสามนี้ยังทำงานพร้อมกันได้ทันท โดยไม่ต้องรอ ให้ task ใด task หนึ่งเสร็จก่อน (ผลลัพธ์แต่ละครั้งอาจจะแตกต่างกัน)

การใช้ dispatch queue เป็นหนึ่งในวิธีการใช้เทคโนโลยี Grand Central Dispatch ในช่วงแรกของการเริ่มต้น อาจจะดูยากและยังสับสน แต่เมื่อทำความเข้าใจสักพักจะพบว่าง่ายกว่าการเขียนเทรดแบบเดิมมาก และนอกจาก dispatch queue ยังสามารถใช้เทคโนโลยีนี้ได้เช่นกัน นั่นคือการใช้คลาส NSOperation และ NSOperationQueue

Leave a Reply