Mutex ในแอปพลิเคชันมือถือ — คืออะไร หลักการทำงาน และการประยุกต์ใช้การแยกส่วนร่วมกัน

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-03-18 เวลาอ่าน: 10 นาที

Mutex (การแยกส่วนร่วมกัน) เป็นพริมิทีฟการซิงโครไนซ์ที่รับประกันว่าเพียงหนึ่งเธรดเท่านั้นที่สามารถดำเนินการส่วนวิกฤตของโค้ดได้ในเวลาใดเวลาหนึ่ง ตาม Microsoft Docs (Synchronization Objects, 2024) หลักการสำคัญของ Mutex คือความเป็นเจ้าของ: เธรดที่รับ Mutex กลายเป็นเจ้าของและจะปล่อยมันเมื่อออกจากส่วนวิกฤตเท่านั้น Mutex เป็นเครื่องมือพื้นฐานสำหรับการป้องกัน Race Condition และรับประกันความสมบูรณ์ของข้อมูลในแอปพลิเคชันแบบหลายเธรด

ประเด็นสำคัญ

  • Mutex เป็นกลไกการแยกส่วนร่วมกันที่รับประกันว่าเพียงหนึ่งเธรดสามารถเข้าถึงทรัพยากรได้ในเวลาเดียว
  • ความเป็นเจ้าของ (ownership) เป็นคุณลักษณะสำคัญของ Mutex: เฉพาะเธรดที่รับล็อกแล้วเท่านั้นที่สามารถปล่อยมันได้
  • แตกต่างจากเซมาฟอร์ ที่มีตัวนับ ≥2 Mutex มีสถานะเพียง 0 หรือ 1 (เซมาฟอร์แบบไบนารี)
  • Deadlock กับ Mutex เกิดขึ้นเมื่อรับมิวเท็กซ์หลายตัวในลำดับที่ผิด
  • suspending Mutex ใน Kotlin Coroutines ไม่บล็อกเธรดของระบบปฏิบัติการ ซึ่งแตกต่างจาก ReentrantLock แบบคลาสสิก

Mutex คืออะไร?

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

สถาปัตยกรรมของ Mutex ย้อนกลับไปยังระบบปฏิบัติการ THE ที่ออกแบบโดย Edsger Dijkstra ในปี 1965 Dijkstra ได้นำเสนอแนวคิดของเซมาฟอร์ ซึ่งต่อมา Mutex ได้เกิดขึ้นเป็นกรณีพิเศษ — เซมาฟอร์แบบไบนารีพร้อมการสนับสนุนความเป็นเจ้าของ ระบบปฏิบัติการสมัยใหม่ (Linux, Windows, Android) นำ Mutex ไปใช้งานในระดับเคอร์เนล ซึ่งรับประกันการซิงโครไนซ์ที่ถูกต้องแม้ระหว่างกระบวนการต่างๆ

คุณสมบัติสำคัญของ Mutex คือ ความเป็นเจ้าของ (ownership) เฉพาะเธรดที่รับมิวเท็กซ์เท่านั้นที่สามารถปล่อยมันได้ สิ่งนี้แยก Mutex ออกจากเซมาฟอร์แบบไบนารี ซึ่งเธรดใดก็ได้สามารถดำเนินการสัญญาณ (V-operation) ความเป็นเจ้าของป้องกันการปล่อยล็อกโดยไม่ได้ตั้งใจโดยเธรดอื่น ทำให้ Mutex ปลอดภัยกว่าสำหรับสถานการณ์การซิงโครไนซ์ทั่วไปในการพัฒนาแอปมือถือ ตาม Android Developer Docs (Processes and Threads, 2024) การใช้ Mutex แทน synchronized สามารถปรับปรุงประสิทธิภาพได้ 30% ภายใต้การแข่งขันสูง

Mutex ทำงานอย่างไร

สถานะและการดำเนินการ

Mutex อยู่ในหนึ่งในสองสถานะ: ถูกล็อก (locked) — ถูกเธรดรับไป; หรือว่าง (unlocked) — ไม่ได้ถูกรับ มีสองการดำเนินการพื้นฐาน: lock() (รับ) และ unlock() (ปล่อย) หาก Mutex ถูกล็อกอยู่แล้ว เธรดที่เรียก lock() จะถูกบล็อกจนกว่าล็อกจะถูกปล่อย ใน JVM เธรดที่ถูกบล็อกจะเปลี่ยนไปยังสถานะ BLOCKED และไม่ใช้ CPU

การจัดตารางเธรดที่รอ

เมื่อ Mutex ถูกปล่อย ระบบจะเลือกว่าเธรดที่รอใดจะได้รับล็อก ด้วย การจัดตารางแบบไม่ยุติธรรม (non-fair) ตัวเลือกอาจตกที่เธรดที่เพิ่งปล่อยมิวเท็กซ์ — สิ่งนี้เพิ่มปริมาณงานแต่อาจนำไปสู่การอดอยาก (Starvation) ตัวจัดตารางแบบยุติธรรม (fair) ใช้คิว FIFO: เธรดที่รอคนแรกได้รับล็อกก่อน ReentrantLock(true) นำกลไกนี้ไปใช้อย่างแม่นยำ

การรับแบบเรียกซ้ำ (Reentrancy)

การนำ Mutex ไปใช้ส่วนใหญ่ใน Java/Kotlin รองรับ การรับแบบรีเอนแทรนต์ (reentrant) หากเธรดเป็นเจ้าของ Mutex อยู่แล้วและเรียก lock() อีกครั้ง การดำเนินการจะสำเร็จ — Mutex ไม่บล็อกตัวเอง ตัวนับการเรียกซ้ำเพิ่มขึ้น และเธรดต้องเรียก unlock() จำนวนครั้งเท่ากับ lock() สิ่งนี้สำคัญสำหรับการเรียกซ้ำและส่วนวิกฤตที่ซ้อนกัน

ตัวอย่างการใช้ Mutex ใน Kotlin

พิจารณางานทั่วไป — การป้องกัน ตัวนับที่ใช้ร่วมกัน จาก Race Condition โดยใช้ ReentrantLock (Mutex แบบคลาสสิกใน Java/Kotlin) หากไม่มี Mutex โค้ดจะให้ผลลัพธ์ที่ไม่ถูกต้อง; หากมี Mutex เธรดทั้งหมด 1000 ตัวจะเพิ่มค่าตัวนับอย่างน่าเชื่อถือ

kotlin
import java.util.concurrent.locks.ReentrantLock

class MutexCounter {
    private val mutex = ReentrantLock()
    private var count = 0

    fun increment() {
        mutex.lock()
        try {
            count++  // ส่วนวิกฤต
        } finally {
            mutex.unlock()  // finally ที่จำเป็น
        }
    }

    fun getCount(): Int {
        mutex.lock()
        try {
            return count
        } finally {
            mutex.unlock()
        }
    }
}

fun main() = runBlocking {
    val counter = MutexCounter()
    val jobs = List(1000) {
        launch(Dispatchers.Default) {
            counter.increment()
        }
    }
    jobs.forEach { it.join() }
    println(counter.getCount())  // เสมอ 1000
}

ให้ความสนใจกับ บล็อก finally — รูปแบบที่จำเป็นเมื่อทำงานกับ Mutex หากมีข้อยกเว้นเกิดขึ้นภายในส่วนวิกฤต จะไม่มีการเรียก unlock() และ Mutex จะถูกล็อกตลอดไป — สิ่งนี้นำไปสู่ Deadlock บล็อก finally รับประกันการปล่อย Mutex ไม่ว่าการดำเนินการของส่วนจะสิ้นสุดอย่างไร

แนวทางอื่นใน Kotlin คือการใช้ฟังก์ชันส่วนขยาย withLock ซึ่งจัดการ lock/unlock กับ finally โดยอัตโนมัติ

kotlin
fun increment() {
    mutex.withLock {  // lock + try/finally อัตโนมัติ
        count++
    }
}

fun getCount(): Int = mutex.withLock { count }

Mutex vs เซมาฟอร์ vs มอนิเตอร์

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

พารามิเตอร์Mutexเซมาฟอร์มอนิเตอร์
ประเภทไบนารี (0/1)นับได้ (0..N)ไบนารี + เงื่อนไข
ความเป็นเจ้าของเฉพาะเจ้าของที่ unlock ได้เธรดใดก็ได้ที่ signal ได้เฉพาะเจ้าของ
รีเอนแทรนซีโดยทั่วไปใช่ (reentrant)ไม่ใช่
การรอแบบมีเงื่อนไขไม่ (ต้องการ Condition)ไม่ในตัว (wait/notify)
ตัวอย่างใน Java/KotlinReentrantLockSemaphore(permits)synchronized

เมื่อใดควรเลือก Mutex: คุณต้องป้องกันทรัพยากรเดียวจากการเข้าถึงพร้อมกัน — ตัวอย่างเช่น คอลเล็กชันที่ใช้ร่วมกัน ไฟล์ หรือตัวนับ เมื่อใดควรเลือกเซมาฟอร์ — คุณต้องจำกัดจำนวนการเข้าถึงพร้อมกันไปยังพูลทรัพยากร เช่น พูลการเชื่อมต่อฐานข้อมูลที่มี 5 การเชื่อมต่อ เมื่อใดควรเลือกมอนิเตอร์ — คุณต้องการการซิงโครไนซ์กับการรอแบบมีเงื่อนไข เช่น คิวผู้ผลิต-ผู้บริโภคผ่าน wait/notify ในการพัฒนา Android สมัยใหม่ synchronized มักถูกแทนที่ด้วย ReentrantLock หรือ kotlinx.coroutines Mutex

ข้อผิดพลาดทั่วไปเมื่อใช้ Mutex

ลืม unlock ใน finally

ข้อผิดพลาดที่พบบ่อยที่สุดคือ การไม่มีบล็อก finally สำหรับเรียก unlock() หากมีข้อยกเว้นเกิดขึ้นในส่วนวิกฤต Mutex จะยังคงถูกล็อกและเธรดอื่นจะรอตลอดไป แม้ว่าคุณจะแน่ใจว่าข้อยกเว้นเป็นไปไม่ได้ — ให้ใช้ try/finally หรือ withLock เสมอ นี่คือหลักการเขียนโปรแกรมเชิงป้องกัน ซึ่งสำคัญโดยเฉพาะในการพัฒนาแอปมือถือที่ข้อยกเว้นอาจเกิดขึ้นเนื่องจากหน่วยความจำไม่เพียงพอหรือ Configuration Changes

ลำดับการรับ Mutex ที่แตกต่างกัน

เมื่อแอปพลิเคชันใช้ Mutex หลายตัว การสร้างลำดับการรับที่สอดคล้องกันมีความสำคัญอย่างยิ่ง หากเธรด A รับ M1 → M2 และเธรด B รับ M2 → M1 จะเกิด Deadlock ในโปรเจ็กต์ขนาดใหญ่ (มากกว่า 50,000 บรรทัดโค้ด) ลำดับการล็อกจะถูกบันทึกในการตัดสินใจทางสถาปัตยกรรมและตรวจสอบโดย linters เครื่องมือ Lock Checker ใน IntelliJ IDEA จะตรวจจับลำดับการรับล็อกที่ไม่สอดคล้องกันโดยอัตโนมัติ

ส่วนวิกฤตยาวเกินไป

การถือ Mutex นานกว่า 1-2 มิลลิวินาที เป็นสัญญาณของการออกแบบที่ไม่ดี ส่วนวิกฤตควรมีเฉพาะการดำเนินการที่จำเป็นน้อยที่สุดเท่านั้น คำขอเครือข่าย I/O ไฟล์และการคำนวณที่ซับซ้อนควรดำเนินการภายนอกบล็อกที่ถูกล็อก ใน Android การถือล็อกเป็นเวลานานในเธรด UI นำไปสู่การตกของเฟรม (jank) และ ANR ใช้ ReadWriteLock หากส่วนวิกฤตประกอบด้วยการดำเนินการอ่านเป็นหลัก

Mutex ใน Kotlin Coroutines

ไลบรารี kotlinx.coroutines มีการนำ Mutex ไปใช้ของตัวเอง ซึ่งแตกต่างจาก ReentrantLock แบบคลาสสิกโดยพื้นฐาน ความแตกต่างหลักคือ suspending Mutex ไม่บล็อกเธรดของระบบปฏิบัติการ แต่ระงับโครูทีนจนกว่าล็อกจะถูกปล่อย ซึ่งหมายความว่าเธรดสามารถดำเนินการโครูทีนอื่นๆ ในขณะที่โครูทีนปัจจุบันกำลังรอ Mutex

kotlin
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class CoroutineCounter {
    private val mutex = Mutex()
    private var count = 0

    suspend fun increment() {
        mutex.withLock {  // suspending — ไม่บล็อกเธรด
            count++
        }
    }

    suspend fun getCount(): Int = mutex.withLock { count }
}

คุณสมบัติสำคัญของ kotlinx Mutex: ไม่รีเอนแทรนต์ (non-reentrant) — แตกต่างจาก ReentrantLock โครูทีนไม่สามารถรับ Mutex ที่ตนเป็นเจ้าของอยู่แล้วซ้ำได้ หากจำเป็น ให้ใช้ Semaphore(1) แทน Mutex นอกจากนี้ Mutex จาก kotlinx.coroutines เป็นแบบไม่บล็อก: ใช้การระงับผ่าน suspend ซึ่งช่วยให้ไม่บล็อกเธรดของพูล

ในทางปฏิบัติ suspending Mutex เป็นที่นิยมกว่า ReentrantLock แบบคลาสสิกในโค้ดโครูทีนด้วยสองเหตุผล: ความสามารถในการปรับขนาด — โครูทีนหนึ่งรอ Mutex ในขณะที่เธรดให้บริการโครูทีนอื่น เพิ่มปริมาณงานของระบบ; ไม่มี BlockedThread — ไม่มีการใช้ทรัพยากรในการจัดเก็บสแต็กของเธรดที่ถูกบล็อก ตาม JetBrains (Kotlin Coroutines Guide, 2024) การใช้ suspending Mutex ช่วยปรับปรุงปริมาณงาน 40% ด้วยโครูทีน 100+ ตัว

คำถามที่พบบ่อย

Mutex แตกต่างจากเซมาฟอร์แบบไบนารีอย่างไร?

ความเป็นเจ้าของ (ownership) เป็นความแตกต่างพื้นฐาน Mutex จดจำว่าเธรดใดรับมันไป และเฉพาะเธรดนั้นเท่านั้นที่สามารถปล่อยมันได้ เซมาฟอร์แบบไบนารี (Semaphore(1)) ไม่มีเจ้าของ — เธรดใดก็ได้สามารถเรียก release() ดังนั้น Mutex จึงปลอดภัยกว่า: เธรดอื่นไม่สามารถปล่อยล็อกของผู้อื่นโดยไม่ได้ตั้งใจ แต่เซมาฟอร์สามารถทำได้

เมื่อใดควรใช้ Mutex และเมื่อใดควรใช้ synchronized?

synchronized ง่ายกว่าและสั้นกว่า — ใช้สำหรับส่วนวิกฤตธรรมดาที่ไม่มีการหมดเวลาและการควบคุมความเป็นธรรม ใช้ ReentrantLock เมื่อคุณต้องการ TryLock กับการหมดเวลา การจัดตารางแบบยุติธรรม ตัวแปรเงื่อนไข หรือการขัดจังหวะเธรดที่รอ (lockInterruptibly) สำหรับโครูทีน ให้ใช้ kotlinx.coroutines.sync.Mutex เสมอ

Spinlock คืออะไรและแตกต่างจาก Mutex อย่างไร?

Spinlock คือล็อกที่เธรดไม่หลับแต่หมุนในลูป (spin) ตรวจสอบสถานะล็อก Spinlock ใช้ CPU แต่ไม่สลับบริบท ซึ่งทำให้มีประโยชน์สำหรับส่วนวิกฤตสั้นๆ (สูงสุด 10 คำสั่ง) Mutex ทำให้เธรดเข้าสู่สถานะ BLOCKED ซึ่งมีค่าใช้จ่ายมากกว่า 10-50 ไมโครวินาทีเนื่องจากการสลับบริบท แต่ไม่สิ้นเปลือง CPU

Mutex ถูกนำไปใช้ในระดับระบบปฏิบัติการอย่างไร?

ในระดับเคอร์เนล Linux Mutex ถูกนำไปใช้ผ่าน futex (fast userspace mutex) เธรดพยายามรับล็อกใน userspace ก่อนผ่านคำสั่งอะตอมมิก CAS (Compare-And-Swap) ถ้า Mutex ว่าง — การรับเกิดขึ้นโดยไม่ต้องใช้ syscall ถ้าถูกครอบครอง — เธรดทำ syscall futex(FUTEX_WAIT) และหลับ เมื่อปล่อย syscall futex(FUTEX_WAKE) จะปลุกหนึ่งเธรดที่รอ

Mutex สามารถใช้ระหว่างกระบวนการได้หรือไม่?

ได้ มี Mutex ระหว่างกระบวนการ (inter-process mutex) ใน Windows คือ Named Mutex ใน Linux — pthread_mutexattr_setpshared กับแอตทริบิวต์ PTHREAD_PROCESS_SHARED Bionic libc ของ Android ก็รองรับ Mutex ระหว่างกระบวนการผ่านตัวบอกไฟล์ (file descriptors) Mutex ระหว่างกระบวนการใช้สำหรับการซิงโครไนซ์ระหว่างแอปพลิเคชันต่างๆ หรือระหว่างกระบวนการและกระบวนการย่อยของมัน

สรุป

  • Mutex เป็นพริมิทีฟการแยกส่วนร่วมกันที่รับประกันว่าเพียงหนึ่งเธรดดำเนินการส่วนวิกฤตในเวลาเดียว
  • ความเป็นเจ้าของ (ownership) แยก Mutex ออกจากเซมาฟอร์แบบไบนารี — เฉพาะเธรดเจ้าของเท่านั้นที่ปล่อยได้
  • ReentrantLock ใน Java/Kotlin คือการนำ Mutex แบบคลาสสิกไปใช้พร้อมการรับแบบรีเอนแทรนต์และการสนับสนุน TryLock
  • บล็อก finally หรือ withLock จำเป็นเพื่อป้องกัน Deadlock จากข้อยกเว้น
  • suspending Mutex จาก kotlinx.coroutines ไม่บล็อกเธรดของระบบปฏิบัติการแต่ระงับโครูทีน
  • ลำดับการรับที่สอดคล้องกัน ของ Mutex หลายตัวเป็นวิธีเดียวที่จะหลีกเลี่ยง Deadlock ในระบบที่ซับซ้อน
  • ส่วนวิกฤตสั้นๆ (สูงสุด 1-2 ms) เป็นกุญแจสำคัญสู่ประสิทธิภาพของแอปพลิเคชันแบบหลายเธรดที่ไม่มีการอดอยาก

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม