Semaphore คือพรีมิทีฟการซิงโครไนซ์ที่ควบคุมการเข้าถึงทรัพยากรที่ใช้ร่วมกันผ่านตัวนับและคิวของเธรดที่รออยู่ ตามข้อมูลจาก Wikipedia, 2024 เซมาฟอร์ถูกเสนอโดย Edsger Dijkstra ในปี 1965 เพื่อแก้ปัญหา การโต้ตอบแบบมัลติเธรด เครื่องมือนี้ช่วยให้จำกัดจำนวนเธรดที่ทำงานพร้อมกันกับส่วนวิกฤตได้
ประเด็นสำคัญ
Semaphore คือพรีมิทีฟการซิงโครไนซ์ที่ใช้ตัวนับเพื่อควบคุมการเข้าถึงทรัพยากรที่ใช้ร่วมกัน แนวคิดนี้ถูกเสนอโดย Edsger Dijkstra ในปี 1965 และกลายเป็นรากฐานสำหรับกลไกการซิงโครไนซ์สมัยใหม่ทั้งหมดในระบบปฏิบัติการ
เซมาฟอร์คือตัวแปรจำนวนเต็มที่มีการดำเนินการอะตอมมิกสองอย่าง: wait (acquire) และ signal (release) การดำเนินการ wait จะลด ตัวนับ ในขณะที่ signal จะเพิ่มขึ้น เมื่อตัวนับถึงศูนย์ เธรดที่เรียก wait จะถูกบล็อกจนกว่าเธรดอื่นจะดำเนินการ signal
วัตถุประสงค์หลักของเซมาฟอร์คือการปกป้องส่วนวิกฤตจากการเข้าถึงพร้อมกันโดยหลายเธรด แตกต่างจากมิวเท็กซ์ เซมาฟอร์ไม่ต้องการ การผูกติดกับเธรดเจ้าของ ทำให้เหมาะสำหรับงานประสานงานที่หลากหลายมากขึ้น
แนวคิดของเซมาฟอร์เกิดขึ้นในบริบทของระบบปฏิบัติการ THE ที่พัฒนาขึ้นที่ Technische Hogeschool Eindhoven Dijkstra ทำให้เซมาฟอร์เป็นนามธรรมทางคณิตศาสตร์ โดยพิสูจน์ความเพียงพอในการ implement พรีมิทีฟการซิงโครไนซ์ใดๆ
กลไกของเซมาฟอร์ ขึ้นอยู่กับการดำเนินการอะตอมมิกสองอย่างและคิวรอภายใน เมื่อเรียก acquire เธรดจะตรวจสอบค่าตัวนับและดำเนินการต่อหรือถูกบล็อกจนกว่าทรัพยากรจะถูกปล่อย
เมื่อสร้างเซมาฟอร์ ค่าเริ่มต้นของ ตัวนับสิทธิ์ จะถูกกำหนด การเรียก acquire แต่ละครั้งจะลดตัวนับลง 1 หากหลังจากนี้ตัวนับกลายเป็นลบ เธรดจะถูกบล็อก การดำเนินการ release จะเพิ่มตัวนับและปลุกหนึ่งในเธรดที่รออยู่
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} กำลังทำงาน")
} finally {
semaphore.release()
}
}
เมื่อเธรดเรียก acquire โดยที่ตัวนับเป็นศูนย์ ระบบปฏิบัติการจะวางไว้ใน คิว FIFO ของเซมาฟอร์ เธรดจะเปลี่ยนเป็นสถานะ BLOCKED โดยไม่ใช้เวลา CPU หลังจากการเรียก release เธรดแรกในคิวจะเปลี่ยนเป็นสถานะ RUNNABLE และได้รับสิทธิ์เข้าถึงทรัพยากร
ในทฤษฎีการซิงโครไนซ์ มีการแยกแยะ เซมาฟอร์ สองประเภทหลัก: ไบนารีและการนับ การเลือกประเภทขึ้นอยู่กับงานเฉพาะของการจัดการการเข้าถึงทรัพยากร
เซมาฟอร์แบบไบนารีรับเฉพาะค่า 0 และ 1 ในพฤติกรรมมันคล้ายกับมิวเท็กซ์ แต่ไม่มี ข้อกำหนดความเป็นเจ้าของ — เธรดใดๆ ก็สามารถดำเนินการ release ได้ เซมาฟอร์เหล่านี้สะดวกสำหรับการ implement แฟล็กความพร้อมและเหตุการณ์ระหว่างเธรด
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("ข้อมูลพร้อมแล้ว")
}
เซมาฟอร์แบบนับ สามารถรับค่าที่ไม่เป็นลบใดๆ ใช้สำหรับจัดการพูลของทรัพยากรที่คล้ายกันซึ่งมีหลายอินสแตนซ์ ตัวอย่างเช่น พูลของการเชื่อมต่อเครือข่าย 5 รายการ: แต่ละ acquire จะใช้หนึ่งการเชื่อมต่อ release จะส่งคืนไปยังพูล
เซมาฟอร์แบบนับเป็นสิ่งที่ขาดไม่ได้สำหรับการจำกัดความเร็วในการเข้าถึงบริการภายนอกและการ implement พูลเธรด ช่วยให้ควบคุมระดับความขนานได้อย่างแม่นยำโดยไม่ต้องจัดการเธรดด้วยตนเอง
| พารามิเตอร์ | เซมาฟอร์ไบนารี | เซมาฟอร์แบบนับ |
|---|---|---|
| ช่วง | 0 หรือ 1 | 0 ถึง N |
| เธรดพร้อมกัน | 1 | สูงสุด N |
| การประยุกต์ใช้ | การส่งสัญญาณ, แฟล็ก | พูลทรัพยากร, การจำกัดอัตรา |
นักพัฒนามักสับสนระหว่าง เซมาฟอร์ และมิวเท็กซ์ แม้ว่าจะมีความแตกต่างพื้นฐานระหว่างทั้งสอง การเข้าใจความแตกต่างเหล่านี้มีความสำคัญอย่างยิ่งต่อการเลือกกลไกการซิงโครไนซ์ที่ถูกต้องในโปรเจกต์
ความแตกต่างหลักคือ แนวคิดความเป็นเจ้าของ มิวเท็กซ์รู้เสมอว่าเธรดใดได้ครอบครองมัน และมีเพียงเธรดนั้นเท่านั้นที่สามารถปล่อยมันได้ เซมาฟอร์ไม่มีเจ้าของ: เธรดใดๆ ก็สามารถเรียก release ได้โดยไม่ต้องเรียก acquire สิ่งนี้ทำให้มิวเท็กซ์ปลอดภัยกว่าสำหรับการป้องกันข้อมูล และเซมาฟอร์มีความยืดหยุ่นมากกว่าสำหรับการประสานงาน
ในทางปฏิบัติ มิวเท็กซ์ เร็วกว่าสำหรับการแยกส่วนร่วมกันอย่างง่ายเนื่องจากการปรับให้เหมาะสมสำหรับสถานการณ์ทั่วไป เซมาฟอร์ต้องการค่าใช้จ่ายเพิ่มเติมในการบำรุงรักษาตัวนับ อย่างไรก็ตาม สำหรับการจำกัดความขนานหรือ implement รูปแบบผู้ผลิต-ผู้บริโภค เซมาฟอร์เป็นสิ่งที่ขาดไม่ได้
| ลักษณะ | Semaphore | Mutex |
|---|---|---|
| ความเป็นเจ้าของ | ไม่มีเจ้าของ | มีเจ้าของ |
| การปล่อย | เธรดใดก็ได้ | เฉพาะเธรดเจ้าของ |
| ตัวนับ | 0 ถึง N | ไบนารี |
| กรณีการใช้งาน | การจำกัดความขนานและการส่งสัญญาณ | การป้องกันส่วนวิกฤต |
| การเรียกซ้ำ | ไม่ | ใช่ (เข้าซ้ำได้) |
ในการพัฒนาแอปพลิเคชันมือถือ Semaphore ใช้สำหรับจัดการการเข้าถึงทรัพยากรที่มีจำกัด: การเชื่อมต่อเครือข่าย ไฟล์ ฐานข้อมูล และส่วนประกอบฮาร์ดแวร์ แพลตฟอร์มสมัยใหม่มี built-in implementation ที่สะดวก
กรณีการใช้งานทั่วไปคือ พูลการเชื่อมต่อ HTTP แอปพลิเคชันสามารถส่งคำขอพร้อมกันสูงสุด 4 คำขอไปยังเซิร์ฟเวอร์ เนื่องจาก API ของผู้ให้บริการจำกัดความขนาน เซมาฟอร์ที่มีค่าเริ่มต้น 4 รับประกันว่าภายใต้โหลดใดๆ จำนวนคำขอพร้อมกันจะไม่เกินขีดจำกัด ในขณะที่เธรดอื่นรอในคิว
หากไม่มีเซมาฟอร์ การเพิ่มขึ้นอย่างรวดเร็วของกิจกรรมผู้ใช้อาจทำให้โครงสร้างพื้นฐานของเซิร์ฟเวอร์โอเวอร์โหลดกะทันหัน นำไปสู่การหมดเวลาและข้อผิดพลาด 429 Too Many Requests Semaphore ทำหน้าที่เหมือนฟิวส์ อนุญาตให้มีการเรียกพร้อมกันตามจำนวนที่กำหนดอย่างเคร่งครัด โดยไม่ขึ้นกับจำนวนเธรดที่ทำงานอยู่
Android มีคลาส Semaphore จากแพ็คเกจ java.util.concurrent มาดูตัวอย่างการจำกัดคำขอเครือข่ายพร้อมกันเป็นสองเธรดเพื่อป้องกันการโอเวอร์โหลดของเซิร์ฟเวอร์
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
ใน iOS DispatchSemaphore จาก GCD แก้ปัญหาเดียวกัน นักพัฒนาใช้มันเพื่อซิงโครไนซ์การเข้าถึงทรัพยากรในโค้ดแบบอะซิงโครนัสโดยไม่บล็อกเธรดหลัก
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
ข้อผิดพลาดที่พบบ่อยที่สุดคือ การลืม release เมื่อเกิดข้อยกเว้น หากเธรดสิ้นสุดด้วยข้อผิดพลาดก่อนเรียก release เซมาฟอร์จะถูกบล็อกถาวรสำหรับเธรดอื่น ใช้ try/finally หรือ defer เพื่อรับประกันการปล่อย ปัญหาที่สองคือ deadlock เมื่อครอบครองหลายเซมาฟอร์ในลำดับที่แตกต่างกันโดยเธรดที่แตกต่างกัน
เซมาฟอร์ไม่ได้ใช้เฉพาะสำหรับการป้องกันข้อมูล แต่ยังใช้สำหรับ การประสานงานเธรด ในสถานการณ์มัลติเธรดที่ซับซ้อน การรู้รูปแบบทั่วไปช่วยเร่งการพัฒนาและลดโอกาสเกิดข้อผิดพลาดในการซิงโครไนซ์
มี รูปแบบ ที่ได้รับการพิสูจน์แล้วหลายรูปแบบสำหรับการใช้เซมาฟอร์ในโปรเจกต์จริง การรู้จักช่วยหลีกเลี่ยงข้อผิดพลาดทั่วไปและสร้างระบบมัลติเธรดที่เชื่อถือได้
เซมาฟอร์ที่มีค่าเริ่มต้น N และการปล่อยเป็นระยะผ่านตัวจับเวลา implement การจำกัดอัตรา ของคำขอ API ตัวอย่างเช่น บริการอนุญาต 10 คำขอต่อวินาที: เซมาฟอร์เริ่มที่ 10 แต่ละคำขอจะลดตัวนับ และ TimerTask แยกต่างหากจะคืนค่าตัวนับเป็นค่าเริ่มต้นทุกวินาที สิ่งนี้ปกป้องทั้งแอปพลิเคชันและเซิร์ฟเวอร์จากการโอเวอร์โหลด
ในปัญหาคลาสสิกของผู้ผลิต-ผู้บริโภค เซมาฟอร์สองตัว จัดการบัฟเฟอร์: empty (สิทธิ์การเขียน) และ full (สิทธิ์การอ่าน) ผู้ผลิตเรียก acquire ที่ empty และ release ที่ full ในขณะที่ผู้บริโภคทำตรงกันข้าม โครงร่างนี้รับประกันว่าผู้บริโภคจะไม่อ่านบัฟเฟอร์ว่าง และผู้ผลิตจะไม่ทำให้มันล้น
โครงร่างเดียวกันนี้เป็นพื้นฐานของ bounded buffer ในระบบปฏิบัติการ — บัฟเฟอร์แบบวงแหวน ที่มีขนาดคงที่ ในแอปพลิเคชันมือถือ รูปแบบนี้ใช้สำหรับประมวลผลคิวของรูปภาพ ไฟล์วิดีโอ และเหตุการณ์วิเคราะห์
เซมาฟอร์ถูกใช้อย่างประสบความสำเร็จสำหรับ การจำกัดการเรียกเครือข่าย ในบริการพื้นหลัง ตัวอย่างเช่น แอปพลิเคชันวิเคราะห์ส่งแพ็กเก็ตเหตุการณ์ไปยังเซิร์ฟเวอร์ หากไม่จำกัดเธรดพร้อมกันในช่วงโหลดสูงสุด (การเริ่มแอป การซิงโครไนซ์หลังออฟไลน์) จำนวนคำขอพร้อมกันอาจเกินขีดจำกัดของเซิร์ฟเวอร์ เซมาฟอร์ที่มีค่าเริ่มต้น 3 รับประกันการส่งที่ราบรื่นและป้องกันการบล็อกฝั่งเซิร์ฟเวอร์
คำถามที่พบบ่อย
Semaphore ไม่ใช่แค่ตัวนับ แต่เป็นพรีมิทีฟการซิงโครไนซ์ที่มีการดำเนินการอะตอมมิกและคิวรอ ตัวนับปกติไม่บล็อกเธรดและไม่รับประกันการเพิ่มแบบอะตอมมิกเมื่อหลายเธรดเข้าถึงพร้อมกัน
ใช่ deadlock เป็นไปได้เมื่อครอบครองหลายเซมาฟอร์ในลำดับที่แตกต่างกันโดยเธรดที่แตกต่างกัน ตัวอย่างเช่น เธรด A ครอบครอง S1 แล้ว S2 ในขณะที่เธรด B ครอบครอง S2 แล้ว S1 กำหนดลำดับการครอบครองเดียวสำหรับเซมาฟอร์ทั้งหมดในโปรเจกต์
เธรดจะ ถูกบล็อก และเข้าสู่สถานะรอ มันไม่ใช้เวลา CPU จนกว่าเธรดอื่นจะเรียก release ใน Java นี่คือสถานะ BLOCKED ใน Swift เธรดจะถูกระงับโดย GCD
ความแตกต่างหลักคือ ความเป็นเจ้าของ Mutex สามารถปล่อยได้โดยเธรดเจ้าของเท่านั้น Binary Semaphore สามารถปล่อยได้โดยเธรดใดก็ได้ ซึ่งสะดวกสำหรับการส่งสัญญาณระหว่างเธรดแต่ปลอดภัยน้อยกว่าสำหรับการปกป้องความสมบูรณ์ของข้อมูล
ค่าเริ่มต้นขึ้นอยู่กับ สถานการณ์ สำหรับการปกป้องทรัพยากรเดียว — 1 สำหรับพูลของการเชื่อมต่อ N — N สำหรับการส่งสัญญาณระหว่างเธรด ให้ใช้ 0 เพื่อให้เธรดผู้บริโภครอสัญญาณจากผู้ผลิต
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม