Semaphore Programming

การจัดจังหวะการทำงานของเทรด (Thread Synchronization)

ความหมายพื้นฐานของคำว่า semaphores คือระบบการให้สัญญาณด้วยการถือธงในท่าต่างๆ ที่เราจะพบเห็นได้ของเจ้าหน้าที่ประจำสถานีรถไฟ (พนักงานชานชลา) เพื่อใช้ในการส่งสัญญาณบอกคนขับรถไฟ ว่าจะสามารถขับรถไฟแต่ละขบวนในการเข้าใช้รางรถไฟนั้นได้หรือไม่ ตัวอย่างเช่น

  • เมื่อพนักงานชานชลาลดธงสัญญาณต่ำลง จะเป็นการส่งสัญญาณบอกคนขับรถไฟว่าสามารถนำขบวนเข้ามาในรางนี้ได้

  • แต่ถ้าพนักงานชานชลายกธงสัญญาณขึ้น จะห้ามรถไฟขบวนนั้นเข้ามาในรางนี้

รูปสัญญาณการเดินรถไฟ

ต่อมาในปี ค.ศ. 1965 ก็ได้มีผู้เชี่ยวชาญทางด้านวิทยาศาสตร์คอมพิวเตอร์ Mr. Edsger Dijkstra ได้นำแนวคิดของ semaphores มาช่วยแก้ไขปัญหาของการประสานงานของกระบวนการ (process synchronization) เนื่องจากภายในระบบปฏิบัติการจะมีโปรเซสเกิดขึ้นอยู่ตลอดเวลาและเกิดขึ้นมากน้อยอยู่เสมอแต่ด้วยข้อจำกัดของทรัพยากรของระบบที่มีจำกัดทำให้ระบบปฏิบัติการจะต้องคอยจัดสรรและควบคุมให้โปรเซสต่างๆ ที่ต้องการเข้าใช้ทรัพยากรภายในระบบสามารถได้ใช้อย่างต่อเนื่องและทั่วถึงให้มากที่สุด

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

โดยนาย Dijkstra ได้เสนอแนวคิดโดยการกำหนดตัวแปรแบบจำนวนเต็ม (integer) ที่เรียกว่า semaphore S ซึ่งสามารถกำหนดค่าเริ่มต้นได้และใช้งานได้โดยผ่านคำสั่ง 2 คำสั่งเท่านั้น คือ down (wait) และ up (signal) โดยคำสั่งทั้งสองนี้มีนิยามดั้งเดิมดังนี้

โดยคำสั่ง down (หรือ wait()) นั้นจะทำการลดค่าที่ละหนึ่ง และคำสั่ง up (หรือ signal) ก็จะเป็นการเพิ่มค่าที่ละหนึ่งเช่นเดียวกัน เมื่อโปรเซสใดก็ตามที่เรียกคำสั่ง wait() แล้วค่า S ณ ตอนนั้นมีค่าน้อยกว่าหรือเท่ากับศูนย์ โปรเซสเหล่านั้นก็จะต้องถูกบล็อคให้รอจนกว่าโปรเซสที่เข้าในเขตวิกฤตจะออกมาแล้วทำการเพิ่มค่า S โดยเรียกคำสั่ง signal()

เพื่อให้เข้าใจได้ง่ายขึ้นเกี่ยวกับ semaphore สามารถเปรียบเทียบได้กับตะกร้าที่มีลูกบอล (S) จำนวนหนึ่งอยู่ โดยเมื่อมีโปรเซสใดต้องการขอเข้าถึงข้อมูลส่วนกลางในเขตวิกฤตจะต้องหยิบลูกบอลหนึ่งลูกจากตะกร้าโดยเมื่อใดที่โปรเซสนั้นเข้าถึงข้อมูลเสร็จสิ้นแล้ว ก็จะออกมาจากเขตวิกฤติแล้วคืนลูกบอลนั้นกลับไปในตะกร้าเช่นเดิม

รูปเปรียบเทียบการหยิบลูกบอลในตะกร้าของ semaphores

โดยทั่วไปจะไม่ได้กำหนดว่าค่า semaphores จะต้องไม่เกินเท่าไหร่ แต่จะขึ้นอยู่กับแพลตฟอร์มนั้นๆหรือขึ้นอยู่กับการกำหนดการใช้งานของผู้ใช้เอง ซึ่งการใช้แนวคิดการนับของ semaphores เปรียบได้กับยามคอยเฝ้าดูแลการเข้าใช้งานทรัพยากรส่วนกลางของระบบ เพราะเมื่อมีโปรเซสตัวหนึ่งในฐานะผู้ผลิต (producer) กำลังบันทึกข้อมูลลงในอาเรย์ที่ถูกใช้งานร่วมกัน และมีโปรเซสอื่นที่เข้ามาอ่านข้อมูลออกจากอาเรย์นี้ในฐานะผู้ใช้ (consumer) ดังนั้นเมื่อ producer ได้เขียนข้อมูลลงไปแล้วก็จะเรียก signal เพื่อส่งสัญญาณแจ้งไปยัง consumer ว่าให้เข้ามาอ่านไปได้เลย โดย consumer ก็จะเรียก wait เพื่อเข้าถึงข้อมูลต่อไป ด้วยวิธีการ signal นี้จะทำให้ไม่มี consumer ไหนเข้าถึงข้อมูลโดยที่ภายในอาเรย์ยังไม่ได้มีข้อมูลอยู่

ระบบที่รองรับ semaphore นั้นมีทั้ง System V และ POSIX ซึ่ง POSIX semaphore นั้นถือว่าเป็นตัวที่พัฒนาตามหลังจาก System V semaphore ที่มีอยู่กับระบบปฏิบัติการ UNIX มานาน รวมทั้งอาจจะมีความยุ่งยากในการเรียกใช้งาน ทำให้ POSIX semaphore ได้ถูกพัฒนาให้ดีขึ้นและแก้ไขจุดเสียของ System V semaphore จนทำให้มีขนาดเล็กกระทัดรัด การรียกใช้งานง่าย และมีความทันสมัยรองรับสถาปัตยกรรมใหม่ๆได้ดีกว่ามาก

เมื่ออ้างถึงการใช้ mutex และ semaphore นั้นจะมีส่วนที่แตกต่างกันกล่าวคือ mutex จะสามารถถูกนำไปใช้ได้สำหรับหลายเทรดที่อยู่ภายในโปเซสเดียวกันเท่านั้น แต่ในขณะที่ POSIX semaphore สามารถถูกนำไปใช้ได้ทั้งระหว่างเทรดภายในโปรเซสเดียวกัน และสำหรับการสื่อสารระหว่างโปรเซสได้ด้วยเช่นกัน โดย POSIX semaphore นี้จะมีการกำหนดชื่อ (named) ก็ได้ หรือไม่กำหนดชื่อ (unnamed) ก็ได้ (เฉพาะในกรณีที่เป็นโปรเซสลูกกับโปรแกรมแม่) เช่นเดียวกันกับการใช้ pipes

ฟังก์ชันและตัวแปรที่เกี่ยวข้อง

  • ไลบรารีที่เกี่ยวข้องคือ semaphore.h

  • ตัวแปร semaphore descriptor ชื่อว่า sem_t

  • ฟังก์ชัน int sem_init(sem_t *sem, int shared, unsigned int value);

    • สำหรับสร้าง unnamed semaphore ที่ถูกชี้ไว้ด้วยตัวแปร sem ไปยัง value และสามารถกำหนดค่าใน shared ว่าตัว semaphore นี้จะสามารถถูกใช้ร่วมกันระหว่างโปรเซสได้หรือไม่ (ถ้าเป็นศูนย์ จะเป็นการระบุว่าจะไม่ถูกใช้ร่วมกัน)

    • สำหรับลบทำลายตัว unnamed semaphore ที่ถูกชี้ด้วยตัวแปร sem

  • ฟังก์ชัน sem_t sem_open(const char *name, int flags, ...);

    • สำหรับสร้าง named semaphore ตามชื่อของ name โดยมีให้เลือก flag ของตัวคือ O_CREAT สำหรับระบุว่าให้สร้าง semaphore ถ้ายังไม่มีอยู่ และ O_EXCL สำหรับระบุว่าให้ส่งผิดพลาดกลับมาด้วยถ้าพบว่ามี semaphore อยู่ก่อนหน้านี้แล้ว รวมทั้ง O_NONBLOCK สำหรับใช้ในโหมด non-blocking

  • ฟังก์ชัน int sem_close(sem_t *sem);

    • สำหรับปิดการเชื่อมต่อของพอยเตอร์ sem ของตัว named semaphore กับโปรเซสอื่นๆ (ยกเว้นโปรเซสที่เป็นคนเรียกฟังก์ชัน sem_open เท่านั้น)

  • ฟังก์ชัน int sem_unlink(const char name *name);

    • สำหรับลบ named semaphore ซึ่งจะถูกใช้หลังจากทุกโปรเซสได้สิ้นสุดและปิดการเชื่อมต่อกับ semaphore โดยการเรียกฟังก์ชัน sem_close

  • ฟังก์ชัน int sem_wait(sem_t *sem);

    • ถ้าค่า semaphore (sem) มีค่ามากกว่าศูนย์ ฟัก์ชันนี้จะทำการลบค่า semaphore ด้วย 1 แล้วคืนค่ากลับทันที ไม่เช่นนั้นโปรเซสก็จะถูกบล็อคไว้

  • ฟังก์ชัน int sem_trywait(sem_t *sem);

    • จะทำงานคล้ายกับฟังก์ชัน sem_wait() ยกเว้นถ้าค่า semaphore ไม่มากกว่าศูนย์ มันจะคืนค่ากลับเป็นความผิดพลาด

  • ฟังก์ชัน int sem_post(sem_t *sem);

    • สำหรับเพิ่มค่าของ semaphore (sem) ซึ่งถ้าค่าที่เพิ่มมีค่ามากกว่าศูนย์ โปรเซสก็อาจจะไม่ถูกบล็อค

  • ฟังก์ชัน int sem_getvalue(sem_t *sem, int *restrict sval);

    • สำหรับอ่านค่าของ semaphore (sem) และไปเก็บไว้ในตัวแปร sval

ตัวอย่างโครงร่างโปรแกรม (skeleton program) ของการเขียนโปรแกรม POSIX semaphore ดังแสดงข้างล่าง

ตัวอย่างที่ 1

แสดงตัวอย่างการควบคุม thread จำนวน 2 ตัวให้ไม่มีการแย่งกันเข้าใช้เขตวิกฤติพร้อมกัน

ตัวอย่างที่ 2

แสดงตัวอย่างการสร้าง thread จำนวน 3 ตัว โดยแต่ละตัวจะเข้าในเขตวิกฤติจำนวน 3 รอบ ซึ่งแต่ละรอบจะทำการสุ่มเวลา (time) แต่ไม่เกิน 30 วินาทีเพื่อทำการหยุดรอด้วยคำสั่ง sleep(time) ตามเวลาที่ได้สุ่มออกมา โดยในโปรแกรมจะกำหนดค่า semaphore เท่ากับ 2 เพื่ออนุญาตให้ thread สามารถเข้ามาในเขตวิกฤติในเวลาเดียวกันได้มากสุดเพียง 2 ตัวเท่านั้น

ตัวอย่างที่ 3

แสดงตัวอย่างการจำลองสถานการณ์การลงคะแนนเลือกตั้ง (voting) โดยสร้างเทรดจำนวน 8 เทรดแทนผู้มาใช้สิทธิ์เลือกตั้งที่กำลังรอเข้าไปกากบาท ตามจำนวนตู้เลือกตั้ง 3 ตู้ (booth) ดังนั้นจะต้องกำหนดให้ค่า semaphore มีค่าเท่ากับ 3 ซึ่งหมายถึงการอนุญาตให้เทรด (ผู้มาใช้สิทธิ์เลือกตั้ง) เข้าไปกากบาทเลือกที่ตู้เลือกตั้งที่เตรียมไว้ให้ 3 ตู้ได้

ตัวอย่างที่ 4

แสดงตัวอย่างเมื่อแต่ละโปรเซสมีการใช้ shared memory ร่วมกัน ซึ่งตัว semaphore เองไม่ได้ทำงานข้ามโปรเซสแต่จะทำงานระหว่างเทรดภายในโปรเซสเดียวกัน

ทำการคอมไพล์โปรแกรม โดยไม่ได้ใช้ semaphore ในการควบคุมการเข้าถึง shared memory ซึ่งผลการทำงานของโปรแกรมจะเห็นว่าทั้ง 4 โปรเซส ต่างแย่งการเข้าใช้พื้นที่ร่วมในเวลาเดียวกัน ทำให้ค่าภายใน shared memory แสดงทับซ้อนกันดังแสดงข้างล่าง

เพิ่มเทคนิคของ semaphore เข้าไปโดยการคอมไพล์โปรแกรมใหม่อีกครั้ง และกำหนด (define) ให้ใช้ USE_SHARED_MEMORY เมื่อรันโปรแกรมอีกครั้งจะสังเกตเห็นว่า semaphore สามารถใช้ในการควบคุมจังหวะการเข้าใช้พื้นที่ shared memory ได้ทีละโปรเซส ดังแสดงข้างล่าง

จากตัวอย่างข้างต้นจะเห็นว่าสามารถใช้ semaphore เพื่อช่วยในการจัดการจังหวะการเข้าใช้งาน shared memory ของแต่ละโปรเซสที่เกิดขึ้นจากคำสั่ง fork() ได้ ดังนั้นจะเห็นว่าข้อดีของ semaphores บนกลไกของการจัดการจังหวะการทำงานของโปรเซสต่างๆที่พยายามเข้าถึงทรัพยากรกลาง ซึ่งสามารถนำไปใช้ได้ทั้งกลุ่มโปรเซสที่เกี่ยวข้องกัน (related processes) เช่น ระหว่างโปรเซสแม่และโปรเซสลูกที่ใช้คำสั่ง fork() และกลุ่มโปรเซสที่ไม่เกี่ยวข้องกัน (unrelated processes)

ตัวอย่างที่ 5

แสดงตัวอย่างของเข้าถึงทรัพยากรกลางของโปรเซสที่เกี่ยวข้องกัน (related processes) โดยโปรเซสแม่จะทำการสร้างโปรเซสลูกด้วยคำสั่ง fork() ดังตัวอย่างข้างล่าง

จากตัวอย่างข้างต้นจะมีส่วนของหน่วยความจำ (ptr) ที่เชื่อมอยู่กับไฟล์ log.txt ซึ่งทั้งโปรเซสแม่และโปรเซสลูกจะต้องเข้าใช้งานร่วมกัน

ตัวอย่างที่ 6

แสดงตัวอย่างการจัดจังหวะการเข้าใช้งานทรัพยากรระหว่างสองโปรเซสที่ไม่ได้มีความเกี่ยวข้องกัน (unrelated processes) ซึ่งเป็นโปรแกรมที่แยกออกจากอย่างชัดเจน ดังตัวอย่างข้างล่าง

ให้เปิดหน้าต่าง terminal ขึ้นมาแล้วทำการคอมไพล์และเรียกใช้โปรแกรม sem_server เพื่อให้โปรเซสทำการเข้าไปขอเปิดใช้หน่วยความจำและกำหนดกุญแจการเข้าถึงข้อมูลภายใน

จากตัวอย่างโปรแกรมข้างต้น ตัวโปรแกรม sem_server และ sem_client นั้นจะถูกเรียกขึ้นมาแยกออกจากกัน ซึ่งจะมีการใช้ semaphore ในการควบคุมการจัดจังหวะการเข้าใช้ทรัพยากร โดยปกติเมื่อมีการใช้ mutex จะอนุญาตให้โปรเซสเข้าถึงทรัพยากรแบบตามลำดับ (serial access) แตกต่างจากการใช้ semaphore ที่สามารถให้ โปรเซสเข้าถึงทรัพยากรได้แบบคู่ขนานกันได้ (parallel access)

Last updated

Was this helpful?