Objective-C Programming Chapter 14 (Part 1)

Chapter 14

Concurrent Programming

การทำงานแบบ concurrent หรือการทำงานหลายๆอย่างพร้อมๆกัน ทั้งใน Mac OS X และ iOS ได้แบ่งระดับของ API ไว้เป็นหลายระดับ ซึ่งก็มีข้อดีข้อเสียและข้อจำกัดต่างๆในแต่ละดับแตกต่างกันไป ในบททนี้เราจะได้เรียนรู้ API พื้นฐานเช่น NSThread เพื่อนำไปต่อยอดกับ Grand Central Dispatch ในบทต่อไป การทำงานแบบ concurrent จะประกอบด้วยสองส่วนสำคัญคือ process และ thread

Process โปรเซสหมายถึงโปรแกรมที่กำลังถูกประมวลผลหรือทำงานอยู่ในขณะนั้น เช่นเมื่อเปิดโปรแกรม A ระบบปฎิบัติการจะสร้างตัวแทน (instance) ของโปรแกรมขึ้นมาทำงานซึ่งนั่นก็คือ process ในการทำงานของโปรเซสอาจจะประกอบไปด้วย thread จำนวนมาก หรือทำงานเพียงหนึ่ง thread

Thread เทรดคือหน่วยการทำงานย่อยๆที่เกิดขึ้นในโปรเซส หากโปรเซสมีเพียงเทรดเดียวจะเรียกว่า single threading  และถ้าโปรเซสมีหลายเทรดก็จะเรียกว่า multithreading โดยที่เทรดแต่ละตัวจะทำงานอิสระแยกจากกัน เนื่องจากการทำงานของ CPU สามารถทำงานได้ทีละอย่าง ดังนั้นในระบบที่มีการทำงานแบบ multithreading เวลาในทำงานของ CPU จะถูกแบ่งเป็นส่วนย่อยๆ เพื่อใช้ในการประมวลผลแต่ละเทรด การจัดสรรเวลาการทำงานของเทรดจะมี schduler ของระบบปฎิบัติการเป็นคนจัดการ เมื่อเทรดทำงานจนครบเวลาที่ schduler จัดไว้ให้ ระบบจะทำการเก็บสถานะของเทรดที่ทำงานในขณะนั้น แล้วหยุดการทำงานไว้ จากนั้นจะสลับให้อีกเทรดทำงาน ซึ่งเรียกว่า context switch และเมื่อครบตามเวลาที่กำหนด เทรดก็จะถูกสลับเปลี่ยนไปแบบนี้เรื่อยๆ เนื่องจากการสลับการทำงานระหว่างเทรดน้ันเกิดขึ้นเร็วมาก ทำให้ดูคล้ายกับว่าเทรดทำงานพร้อมๆกัน ในกรณีคอมพิวเตอร์มี CPU มากกว่าหนึ่งตัว (Multicore) ระบบปฎิบัติการอาจจะแบ่งเทรดไปทำงานในแต่ละ CPU Core โดยที่แต่ละ CPU อาจจะมีหนึ่งเทรดหรือหลายเทรดทำงานอยู่ ในระบบคอมพิวเตอร์ที่ CPU ทำงานพร้อมกันหลายๆตัวจะเรียกการทำงานแบบนี้  Parallel ซึ่งหลายคนเข้าใจผิดว่าการทำงานแบบ parallel เหมือนกับ mutithread

โปรแกรมตั้งแต่บทแรกที่เราได้เขียนกันมาเป็นการทำงานแบบ Single threading หรือทำงานเพียงหนึ่งอย่าง การทำงานแบบนี้มีข้อดีคือจัดการทุกอย่าง เป็นอย่างๆไปตั้งแต่ต้นจนจบ ทำให้การบริหารจัดการง่าย แต่ก็มีข้อเสียคือ เสียเวลาไปกับรอให้การทำงานแต่ละอย่างเสร็จสิ้นหรือเรียกว่า idle เช่น สมมติว่า มีงาน 2 งานคืองาน A กับ C โดยที่งาน A นั้นให้ผู้ใช้งานกดปุ่มจึงสามารทำงานได้ ส่วนงาน C สามารถทำได้เลย ในระบบที่เป็น single threading จะเห็นว่าเราสูญเสียเวลาไปกับการรอให้ A ทำงานจนเสร็จ ถึงจะเริ่มการทำงาน C ได้ และเราเรียกเวลาที่ไม่ได้งานอะไรนี้ว่า idle ส่วนในระบบ multithread งาน C สามารถทำได้เลยโดยที่ไม่ต้องรอ A ทำงานเสร็จ เพื่อลด idle และทำให้ CPU เกิดประสิทธิภาพสูงที่สุดเราจึงต้องแบ่งการทำงานออกเป็นหลายๆเทรด

NSThread

การสร้างเทรดตามแบบเดิมนั้นจะใช้ฟังก์ชั่นภาษา C ในการสร้าง thread หรือเรียกว่า POSIX Thread (pthread) ซึ่งเป็นคำสั่งในระดับล่าง (Low Level) และในปัจจุบันเราไม่จำเป็นต้องใช้ pthread เพราะมีคลาสที่ช่วยให้การสร้างเทรดนั้นง่ายขึ้นนั่นก็คือ NSThread การสร้างเทรดด้วย NSThread มีอยู่ด้วยกันสองวิธีแบบแรกคือสร้างอ็อบเจ็กของ NSThread ขึ้นมาใช้งาน แบบที่สองคือใช้คลาสเมธอด detachNewThreadSelector:toTarget:withObject: โปรแกรมที่เราจะได้เขียนกันเป็นโปรแกรมแรกคือโปรแกรมจำลองการ download ซึ่งมีโค้ดดังนี้

Program 14.1

Download.h

Download.m

โค้ดส่วนของ for loop ของเมธอด startDownload เป็นเพียงการจำลองการดาวน์โหลดไฟล์ โดยจะวนไปจนกว่าจะเท่ากับขนาดของไฟล์ที่กำหนด

main.m

โปรแกรมเริ่มด้วยการสร้างอ็อบเจ็ก music และ image จากนั้นบรรทัดที่ 12 เป็นการสร้างเทรดคือ imgThread ในการสร้างเทรดเราต้องกำหนดอ็อบเจ็กและเมธอดที่ใช้เริ่มต้นในการทำงาน จากโค้ดเราได้กำหนดให้อ็อบเจ็ก image ทำงานที่เทรด imgThread และให้เริ่มการทำงานด้วยเมธอด startDownload ส่วนพารามิเตอร์สุดท้าย withObject: จะเป็นการกำหนดอ็อบเจ็กที่จะถูกใช้ในเมธอดเริ่มต้น เช่นสมมติเมธอด startUpload: ต้องการอ็อบเจ็กเพื่อเป็นพารามิเตอร์ เราสามารถกำหนดพารามิเตอร์ที่ใช้ในเมธอดได้ เช่น โค้ดตัวอย่าง

บรรทัดที่ 16 เราได้สร้างเทรดด้วยการเรียกคลาสเมธอดซึ่งก็มีการกำหนดค่าพารามิเตอร์เช่นเดียวกับการสร้างเทรดอ็อบเจ็ก สิ่งที่แตกต่างกันระหว่างสองวิธีนี้คือ หากใช้คลาสเมธอดเทรดจะเริ่มทำงานทันทีหลังจากเรียกเมธอด แต่ถ้าเป็นเทรดอ็อบเจ็ก เราต้องเป็นคนสั่งให้เทรดทำงานด้วยการเรียกเมธอด start ดังเช่นบรรทัดที่ 20 นอกจากนี้การสร้างเทรดอ็อบเจ็ก เราสามารถกำหนดความสำคัญของเทรด และสามารถกำหนดให้เทรดหยุดทำงานได้อีกด้วย เมื่อสร้างเทรดเสร็จเรียบร้อย ในส่วนของการทำงานหลักเราทำการจำลองการ upload file ดังนั้นแล้วโปรแกรมของเราก็จะมีการทำงานด้วยกันทั้งหมด 3 เทรด ดังรูป

thread

เมื่อให้โปรแกรมทำงานก็จะมีผลลัพธ์ลักษณะดังนี้

Program 14.1 Output

Start downloading Music
Start downloading Image
Uploading Movie 10.00 %
Downloading Image – 20.00 %
Downloading Music – 33.00 %
Uploading Movie 20.00 %

Music done

Image done

Uploading Movie 90.00 %
Uploading Movie 100.00 %

โปรแกรมจะเริ่มทำงานที่ main thread ซึ่งเป็นเทรดหลักที่ใช้สร้างเทรดอื่นๆ หลังจากนั้นเทรด Music ก็เริ่มทำงานและได้แสดงข้อความ Start downloading Music เป็นอันดับแรก ถัดมาเทรดที่สามหรือเทรด image ก็เริ่มทำงานและได้แสดงข้อความ Start downloading Image เมื่อเทรดทั้งสามเริ่มทำงานแล้ว ก็จะแยกทำงานอิสระจากกัน จะเห็นว่า Output นั้นแสดงผล Downloading Music , Downloading Image , Uploading Movie สลับกันมาระหว่างเทรดทั้ง 3 ขึ้นอยู่กับ schduler ว่าจะให้เทรดใดทำงาน หากให้โปรแกรมทำงานแล้วข้อความของผลลัพธ์ไม่ได้มีลำดับตรงกับ Output ที่แสดงก็ไม่ต้องแปลกใจ เพราะผลลัพธ์ที่ได้แต่ละครั้งอาจจะแตกต่างกันไปขึ้นอยู่กับระบบปฎิบัติการเป็นคนจัดการ แต่อย่างไรก็ตามเราจะเห็นว่าเทรด Music จะทำงานเสร็จก่อนเทรด Image เสมอ นั่นก็เพราะว่ามีการทำงานที่สั้นกว่านั่นเอง

จากโปรแกรมที่ผ่านมาเราพอจะมองเห็นภาพคร่าวๆของการทำงานแบบ multithread สิ่งที่เห็นได้ชัดเจนก็คือเทรดแต่ละเทรดนั้นทำงานแยกอิสระจากกัน ถึงแม้จะอิสระจากกัน แต่ Music thread และ Image Thread ทั้งสองเทรดนี้เป็นเพียงส่วนการทำงานย่อยของ main thread ถ้าหาก main thread เสร็จสิ้นการทำงาน โปรแกรมก็จะจบลงโดยไม่สนใจเทรดย่อยที่ยังทำงานไม่เสร็จ ถ้าหากเราเปลี่ยนโค้ดลูปบรรทัดที่ 22-23 ให้ทำงานสั้นลง โดยกำหนดให้ i น้อยกว่าหรือเท่ากับ 2

เมื่อลองคอมไฟล์และให้โปรแกรมทำงาน ผลลัพธ์ที่ console จะแสดงผลดังนี้

Program 14.1 Output ( i <= 2)

Start downloading Image
Start downloading Music
Uploading Movie 10.00 %
Downloading Image – 33.00 %
Downloading Music – 20.00 %
Uploading Movie 20.00 %

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

Program 14.2

main.m

Program 14.2 Output

Start downloading Music
Start downloading Image
Downloading Image – 20.00 %
Downloading Music – 33.00 %

Music done

Image done

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

Thread Priority

เมื่อโปรแกรมมีเทรดทำงานหลายตัวพร้อมๆกัน ปัญหาที่เกิดขึ้นก็คือทรัพยากรของระบบไม่เพียงพอต่อความต้องการของทุกๆเทรด เช่นสมมติว่า เราเขียนเกมส์ซึ่งมีเทรดที่ใช้เชื่อมต่อกับ server และมีอีกเทรดประมวลผลภาพ 3D ถ้าหากเราไม่ได้กำหนดความสำคัญให้กับเทรด เกมส์ก็อาจจะเกิดอาการภาพกระตุก เพราะโปรแกรมอาจจะแบ่งทรัพยากรให้กับสองเทรดเท่าๆกัน เมื่อเทียบกับระหว่างการเชื่อมต่อกับการประมวลผลภาพ 3D สำหรับเกมส์การประมวลผลภาพมีความสำคัญมากกว่าแน่นอน ดังนั้นการกำหนดความลำดับความสำคัญ (priority) ให้กับเทรดจึงเป็นสิ่งจำเป็น คลาส NSThread มีเมธอดที่ใช้สำหรับการกำหนด priority คือเมธอด setPriority: โดยมีค่าตั้งแต่ 0 ซึ่งเป็นลำดับต่ำที่สุดไปจนถึงค่ามากที่สุดคือ 1.0
Program 14.3

main.m

Program 14.3 Output

Start downloading Image
Downloading Image – 20.00 %
Start downloading Music
Downloading Image – 40.00 %
Downloading Music – 33.00 %

Image done
Downloading Music – 100.00 %
Music done

เมื่อพิจารณาจากพารามิเตอร์ fileSize จะเห็นว่าเทรด Music นั้นน้อยกว่ากว่า Image ทำให้โปรแกรมที่ผ่านมาสองโปรแกรมเทรด Music จึงทำงานเสร็จก่อนเสมอ แต่โปรแกรม 14.3 กำหนด priority ให้กับเทรด Image ให้สูงกว่า Music ดังนั้นโปรแกรมจะจัดสรรทรัพยากรให้กับเทรด Image ก่อนเป็นอันดับแรก ฉะนั้นเทรด Image จึงได้รับการประมวลผลและทำงานเสร็จก่อนเทรด Music

Sharing Of Resource

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

Program 14.4

Program 14.4-Prefix.pch

Producer.h

Producer.m

คลาส Producer มีเมธอดหลักๆคือ cameraToStore: เป็นเมธอดที่จำลองการผลิตกล้องถ่ายรูป โรงงานจะประทับตรายี่ห้อซึ่งจะถูกกำหนดโดยชื่อของเทรดที่ทำงาน  โดยรหัส ID กล้องจะเพิ่มขึ้นทุกๆครั้งเมื่อมีการผลิต เมื่อผลิตเสร็จก็จะนำไปเก็บไว้ยัง store โรงงานจะผลิตกล้องไปเรื่อยๆจนกว่าจะถึงขีดจำกัดของกำลังการผลิตในแต่ละวัน นอกจากนี้เรายังได้เพิ่ม autorelease pool เข้ามาในเทรด เนื่องจากเทรดแยกอิสระจากกัน แต่ละเทรดจะมี stack เป็นของตัวเองและการจัดการ autorelease pool ก็แยกจากกัน ดังนั้นหากเทรดมีอ็อบเจ็กที่เป็น autorelease ก็ต้องมี autorelese pool เพื่อจัดการกับอ็อบเจ็กเหล่านี้ มิฉะนั้นก็จะเกิด memory leak ได้ และจากโค้ดเราได้เรียกใช้

เราจะได้อ็อบเจ็กที่เป็น autorelease ดังนั้น เราจึงต้องมี autorelease pool มารองรับนั่นเอง

main.m

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

Program 14.4 Output

Receive: Nikon camera 1
Receive: Nikon camera 2
Receive: Nikon camera 3

จากผลลัพธ์แสดงให้เห็นว่าโปรแกรมที่ได้เขียนไปเราสามารถใช้ตัวแปรร่วมกันระหว่างเทรดได้ นั่นคือตัวแปร camID ซึ่งเป็นแบบ global variable และอ็อบเจ็ก store ที่ใช้วิธีการส่งผ่านทางพารามิเตอร์ เราจะปรับปรุงโปรแกรม 14.4 โดยเพิ่มจำนวนของเทรด Producer เพื่อผลิตกล้องต่างยี่ห้อกัน ดังโค้ดต่อไปนี้

Program 14.5

main.m

สิ่งที่ได้เปลี่ยนแปลงจากโปรแกรมก่อนหน้าคือเพิ่มจำนวนเทรดในการผลิตกล้อง ตอนนี้เรามีโรงงานผลิตกล้องทั้งหมด 3 โรงงานดังนั้นจำนวนกล้องที่จะผลิตได้ทั้งหมด คือ 9 ตัว โดยมีหมายเลขตั้งแต่ 1-9 ที่ไม่ซ้ำกัน เมื่อให้โปรแกรมทำงานจึงมีผลลัพธ์ดังนี้

Program 14.5 Output

Receive: Nikon camera 1
Receive: Nikon camera 2
Receive: Nikon camera 3
Receive: Cannon camera 1
Receive: Cannon camera 5
Receive: Cannon camera 6
Receive: Sony camera 7
Receive: Sony camera 8
Receive: Sony camera 9

กล้องที่ผลิตได้มีทั้งหมด 9 ตัว แต่หมายเลขของกล้องกลับมีบางหมายเลขซ้ำกัน ทั้งๆที่ควรจะมีหมายเลขไม่ซ้ำกันเพราะทุกๆครั้งที่ผลิตกล้อง ค่าของ camID จะเพิ่มขึ้นทุกๆครั้ง ปัญหาที่เกิดขึ้นคือ เทรดแรกที่ได้ทำงานอ่านค่า camID ไปแล้วแต่ยังไม่ได้ทันได้ปรับค่าก็ถูก schduler สลับให้เทรดอื่นทำงาน เมื่อเทรดต่อมาทำงานก็อ่านค่าเดิม ทำให้ได้ค่าเก่าซ้ำซ้อนกัน เราเรียกปัญหาแบบนี้ว่า race condition

race condition

จากรูปเมื่อเทรด Nikon ทำงานก็อ่านค่า camID ซึ่งได้ค่า 1 จากนั้น schduler ได้สลับให้เทรด Cannon ทำงานต่อ โดยที่เทรด Nikon ยังทำงานไม่จบเพราะต้องเพิ่มค่าให้กับ camID และเมื่อเทรด Cannon ทำงาน ก็ได้อ่านค่า camID ซึ่งเป็นค่าเดิมที่ยังไม่ได้เปลี่ยนแปลง ดังนั้นโปรแกรมจึงให้ผลผิดพลาด ปัญหา Race condition ยังแบ่งย่อยออกเป็นหลายแบบ เช่น Write after read และ Read after Write เป็นต้น การแก้ปัญหานี้สามารถทำได้ด้วยเครื่องมือที่เรียกว่า Mutual Exclusion

Leave a Reply