Race Condition ในแอปพลิเคชันมือถือ: สาระสำคัญ สาเหตุการเกิด และวิธีการป้องกัน

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

Race Condition — คือสถานการณ์ในการเขียนโปรแกรมแบบหลายเธรดซึ่งผลลัพธ์สุดท้ายขึ้นอยู่กับลำดับการทำงานของเธรด ตามเอกสารOracle Java Tutorials (2024) สภาวะการแข่งขันเกิดขึ้นเมื่อเข้าถึงทรัพยากรที่ใช้ร่วมกันพร้อมกันโดยไม่มีการซิงโครไนซ์ หากไม่มีกลไกที่เหมาะสม Race Condition จะนำไปสู่ความเสียหายของข้อมูลและบั๊กที่ไม่สามารถทำซ้ำได้ในแอปพลิเคชันมือถือ

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

  • Race Condition — ข้อบกพร่องในโค้ดแบบหลายเธรด เมื่อผลลัพธ์ขึ้นอยู่กับลำดับของเธรด
  • สภาวะการแข่งขัน เกิดจากการขาดการซิงโครไนซ์เมื่อเข้าถึงทรัพยากรที่ใช้ร่วมกัน
  • Data Race — ชนิดย่อยของ Race Condition เกี่ยวข้องกับการเขียนและอ่านตัวแปรพร้อมกัน
  • Mutex และเซมาฟอร์ — เครื่องมือหลักในการกำจัดสภาวะการแข่งขันในการพัฒนาแอปมือถือ
  • การดำเนินการแบบอะตอมิก รับประกันความไม่สามารถแบ่งแยกได้ของการดำเนินการและป้องกันการแข่งขันของเธรด

Race Condition คืออะไร?

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

ในการพัฒนาแอปมือถือ Race Condition เป็นอันตรายอย่างยิ่ง เพราะเธรดสามารถทำงานบนแกนประมวลผลที่แตกต่างกันด้วยความเร็วที่ต่างกัน นักพัฒนาไม่สามารถควบคุมได้ว่าเธรดใดจะดำเนินการเสร็จก่อน — ตัวจัดตารางเวลาของระบบปฏิบัติการจะเป็นผู้ตัดสิน จากการวิจัยของ IBM (Concurrency Bugs in Android, 2022) ประมาณ 23% ของบั๊กร้ายแรงในแอปพลิเคชัน Android เกี่ยวข้องกับสภาวะการแข่งขัน

คุณลักษณะสำคัญของ Race Condition คือความไม่แน่นอน โค้ดเดียวกันสามารถทำงานได้โดยไม่มีข้อผิดพลาดนับพันครั้งแล้วจู่ๆ ก็ล้มเหลว ทำให้การวินิจฉัยยากเป็นพิเศษ: บั๊กจะปรากฏเฉพาะในสถานการณ์เฉพาะเท่านั้น — ขึ้นอยู่กับโหลดของ CPU จำนวนเธรดที่ทำงานอยู่ และช่วงการจัดตารางเวลา

สภาวะการแข่งขันเกิดขึ้นได้อย่างไร

การดำเนินการที่ไม่เป็นอะตอมิก

Race Condition เกิดขึ้นเมื่อเธรดดำเนินการที่ไม่เป็นอะตอมิก — ลำดับของหลายขั้นตอนที่สามารถถูกขัดจังหวะโดยเธรดอื่น ตัวอย่างเช่น การดำเนินการเพิ่มค่า counter++ จริงๆ แล้วประกอบด้วยสามขั้นตอน: อ่านค่าจากหน่วยความจำ เพิ่มขึ้นหนึ่ง และเขียนกลับ ถ้าเธรดสองเธรดดำเนินการขั้นตอนเหล่านี้แบบสลับกัน ผลลัพธ์จะไม่ถูกต้อง

การขาดการซิงโครไนซ์

สาเหตุหลักของสภาวะการแข่งขันคือการขาดการซิงโครไนซ์เมื่อเข้าถึงข้อมูลที่ใช้ร่วมกัน เมื่อเธรดหนึ่งเปลี่ยนแปลงออบเจ็กต์และอีกเธรดหนึ่งอ่านพร้อมกัน ผลการอ่านจะคาดเดาไม่ได้ ใน Android ปัญหานี้รุนแรงขึ้นเพราะส่วนประกอบของแอปพลิเคชัน (Activity, Service, BroadcastReceiver) สามารถทำงานในเธรดที่แตกต่างกัน

การใช้งานคอรูลีนที่ไม่ถูกต้อง

ในการพัฒนา Android สมัยใหม่ด้วย Kotlin Race Condition มักเกิดจากการใช้งานคอรูลีนที่ไม่ถูกต้อง ถ้าคอรูลีนสองตัวทำงานกับสถานะที่ใช้ร่วมกันใน Dispatchers ที่ต่างกันโดยไม่มีการซิงโครไนซ์ ผลลัพธ์จะคาดเดาไม่ได้ สิ่งนี้เกิดขึ้นบ่อยครั้งเมื่อรวม Dispatchers.IO และ Dispatchers.Main เข้ากับออบเจ็กต์ mutable ที่ใช้ร่วมกัน

ตัวอย่าง Race Condition ในโค้ด Kotlin

มาดูตัวอย่างคลาสสิกของ Data Race — การเพิ่มค่าตัวนับจากหลายเธรด โดยไม่มีการซิงโครไนซ์ ค่าสุดท้ายจะน้อยกว่าที่คาดไว้เนื่องจากการดำเนินการทับซ้อนกัน

kotlin
class RaceCounter {
    private var counter = 0

    fun increment() {
        // การดำเนินการที่ไม่เป็นอะตอมิก — สามขั้นตอน
        counter++  // อ่าน เพิ่ม เขียน
    }

    fun getCount(): Int = counter
}

fun main() = runBlocking {
    val rc = RaceCounter()
    val jobs = List(1000) {
        launch(Dispatchers.Default) {
            rc.increment()
        }
    }
    jobs.forEach { it.join() }
    println(rc.getCount())  // คาดหวัง 1000 ได้รับ ~997
}

ในตัวอย่างนี้ 1000 คอรูลีนเรียก increment() พร้อมกัน เนื่องจากการดำเนินการ counter++ ไม่เป็นอะตอมิก ค่าสุดท้ายแทบจะไม่เท่ากับ 1000 เลย แต่ละรอบให้ผลลัพธ์ที่แตกต่างกัน — อาการคลาสสิกของ Race Condition ยิ่งเธรดเข้าร่วมการแข่งขันมากเท่าไหร่ ค่าเบี่ยงเบนจากที่คาดไว้ก็ยิ่งมากขึ้นเท่านั้น

วิธีแก้ไข — ใช้ชนิดอะตอมิกหรือล็อค ใน Kotlin สำหรับงานนี้ AtomicInteger จากแพ็คเกจ java.util.concurrent.atomic เหมาะสม มันรับประกันว่าการดำเนินการอ่าน-แก้ไข-เขียนจะดำเนินการเป็นหนึ่งเดียวที่ไม่สามารถแบ่งแยกได้ในระดับโปรเซสเซอร์

kotlin
import java.util.concurrent.atomic.AtomicInteger

class SafeCounter {
    private val counter = AtomicInteger(0)

    fun increment() {
        counter.incrementAndGet()  // การดำเนินการแบบอะตอมิก
    }

    fun getCount(): Int = counter.get()
}

ประเภทของสภาวะการแข่งขัน

Data Race

Data Race — ประเภทที่พบบ่อยที่สุดของ Race Condition เกิดขึ้นเมื่อเธรดหนึ่งเขียนข้อมูลลงในตัวแปร ในขณะที่อีกเธรดหนึ่งอ่านหรือเขียนตัวแปรเดียวกันพร้อมกันโดยไม่มีการซิงโครไนซ์ ใน Java Memory Model พฤติกรรมดังกล่าวถือว่าไม่สามารถระบุได้ — เธรดอาจเห็นค่าที่ไม่เป็นปัจจุบันเนื่องจากการแคชในระดับ CPU

Check-Then-Act

รูปแบบ Check-Then-Act — สถานการณ์ที่เธรดตรวจสอบเงื่อนไขแล้วดำเนินการตามการตรวจสอบนั้น ระหว่างการตรวจสอบและการดำเนินการ เธรดอื่นอาจเปลี่ยนสถานะ ตัวอย่างทั่วไป: การตรวจสอบว่ามีองค์ประกอบในคอลเลกชันหรือไม่แล้วจึงลบออก ใน Android สิ่งนี้มักเกิดขึ้นเมื่อทำงานกับ SharedPreferences หรือฐานข้อมูล

Read-Modify-Write

Read-Modify-Write — สถานการณ์ที่เธรดอ่านค่า แก้ไขในหน่วยความจำเฉพาะที่ และเขียนกลับ ถ้าระหว่างการอ่านและการเขียนเธรดอื่นเปลี่ยนค่าเดิม ผลลัพธ์ของการแก้ไขจะสูญหาย ตัวอย่างคลาสสิก — การดำเนินการ counter++ ที่วิเคราะห์ข้างต้นในโค้ด Kotlin

STM (Software Transactional Memory)

Software Transactional Memory (STM) — แนวทางที่การดำเนินการกับข้อมูลที่ใช้ร่วมกันจะดำเนินการในธุรกรรมคล้ายกับฐานข้อมูล ถ้าธุรกรรมสองรายการขัดแย้งกัน รายการหนึ่งจะย้อนกลับและดำเนินการซ้ำ ใน Kotlin สำหรับ JVM มีไลบรารี Multiverse STM ที่จัดการความขัดแย้งในการเข้าถึงโดยอัตโนมัติโดยไม่ต้องล็อคที่ชัดเจน STM มีประโยชน์โดยเฉพาะใน Android เมื่อทำงานกับออบเจ็กต์ที่เกี่ยวข้องกันหลายตัว

เธรดบางใน UI ของ Android

หมวดหมู่พิเศษของ Race Condition — เธรดบาง (thin races) ที่เกี่ยวข้องกับวงจรชีวิตของ Activity สถานการณ์ทั่วไป: เธรดพื้นหลังโหลดข้อมูลเสร็จ แต่ Activity ถูกทำลายไปแล้ว (หมุนหน้าจอ) คอรูลีนพยายามอัปเดต View ที่ไม่มีอยู่และล้มเหลวด้วย IllegalStateException วิธีแก้ไข — ใช้ viewModelScope และส่วนประกอบที่รับรู้วงจรชีวิต (Lifecycle-aware) ซึ่งยกเลิกคอรูลีนโดยอัตโนมัติเมื่อ Lifecycle Owner ถูกทำลาย

วิธีตรวจจับ Race Condition

การตรวจจับ Race Condition เป็นหนึ่งในงานที่ยากที่สุดในการดีบักแอปพลิเคชันแบบหลายเธรด การทดสอบมาตรฐานไม่ค่อยพบสภาวะการแข่งขันเพราะมันแสดงเฉพาะเมื่อเวลาตรงกันเท่านั้น ตามข้อมูลของ Google (Android Testing Guide, 2023) ประมาณ 70% ของ Race Condition ไม่ถูกตรวจพบโดยการทดสอบหน่วยเนื่องจากลำดับการดำเนินการที่แน่นอนในสภาพแวดล้อมการทดสอบ

วิธีการตรวจจับหลักรวมถึงเครื่องมือเฉพาะทาง ThreadSanitizer (TSan) — ตัววิเคราะห์พลวัตที่สร้างใน Android NDK ซึ่งติดตามการเข้าถึงหน่วยความจำทั้งหมดและตรวจจับการเข้าถึงที่ไม่ซิงโครไนซ์ สำหรับโค้ด Java/Kotlin Google แนะนำ Android Studio Layout Inspector คู่กับ StrictMode ซึ่งสกัดกั้นการเข้าถึงที่ผิดกฎหมายไปยังเธรด UI จากเธรดพื้นหลัง

อีกแนวทางที่มีประสิทธิภาพ — Stress Testing โดยการทดสอบซ้ำๆ ภายใต้โหลด เฟรมเวิร์ก Lincheck จาก JetBrains ออกแบบมาเฉพาะสำหรับการทดสอบโครงสร้างข้อมูลที่ทำงานพร้อมกันบน JVM มันสร้างสถานการณ์ด้วยการเรียงลำดับการดำเนินการต่างๆ โดยอัตโนมัติและตรวจสอบความถูกต้องของผลลัพธ์ในแต่ละกรณี

เครื่องมือแพลตฟอร์มประเภทการวิเคราะห์
ThreadSanitizerAndroid NDKการวิเคราะห์หน่วยความจำพลวัต
Intel InspectorWindowsสถิต + พลวัต
LincheckJVM / KotlinStress Testing
StrictModeAndroidการสกัดกั้นขณะทำงาน

วิธีการป้องกัน Race Condition

ตัวแปรแบบอะตอมิก

ตัวแปรแบบอะตอมิก (AtomicInteger, AtomicLong, AtomicReference) — วิธีที่ง่ายที่สุดในการกำจัด Data Race สำหรับการดำเนินการเดี่ยว พวกมันใช้คำสั่ง CAS ระดับต่ำของโปรเซสเซอร์ (Compare-And-Swap) ซึ่งทำงานแบบอะตอมิกโดยไม่ต้องล็อค สิ่งนี้ให้ประสิทธิภาพสูงสุดในสถานการณ์ที่มีการแข่งขันต่ำ

ล็อคและ Mutex

Mutex และล็อค — กลไกการซิงโครไนซ์แบบคลาสสิก เหมาะสำหรับการดำเนินการที่ซับซ้อนและส่วนวิกฤต ใน Kotlin สำหรับคอรูลีน ใช้ suspending Mutex จากไลบรารี kotlinx.coroutines ซึ่งรองรับการระงับแทนที่จะล็อคเธรด ทำให้หลีกเลี่ยงการรอคอยที่ไม่ได้ใช้งานซึ่งเป็นลักษณะเฉพาะของล็อคแบบดั้งเดิม

การแยกสถานะ

การแยกสถานะ — แนวทางเชิงสถาปัตยกรรมที่แต่ละเธรดทำงานกับสำเนาข้อมูลของตนเอง ในการพัฒนาแอปมือถือ ทำได้ผ่าน Actor-model โดยที่แต่ละ actor เป็นเจ้าของสถานะของตนเองและแลกเปลี่ยนข้อความกับ actor อื่น Kotlin Coroutines ให้การใช้งาน Actor ผ่าน Channel และ SendChannel ซึ่งกำจัด Race Condition ในระดับสถาปัตยกรรมอย่างสมบูรณ์

ระดับการป้องกันเพิ่มเติม — Immutability: ถ้าข้อมูลที่ใช้ร่วมกันไม่สามารถเปลี่ยนแปลงได้โดยหลักการแล้ว Race Condition จะเป็นไปไม่ได้แม้ไม่มีการซิงโครไนซ์ ใน Kotlin ใช้ data class ที่มีฟิลด์ val และคอลเลกชันจาก kotlinx.collections.immutable ซึ่งรับประกันความไม่เปลี่ยนแปลงของโครงสร้างเมื่อเผยแพร่ระหว่างเธรด

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

ความแตกต่างระหว่าง Race Condition และ Data Race คืออะไร?

Data Race — เป็นประเภทเฉพาะของ Race Condition ที่เธรดสองเธรดเข้าถึงหน่วยความจำเดียวกันพร้อมกัน และอย่างน้อยหนึ่งเธรดทำการเขียน Race Condition — เป็นแนวคิดที่กว้างกว่าซึ่งรวมถึงข้อผิดพลาดใดๆ ที่ขึ้นอยู่กับลำดับการทำงานของเธรด รวมถึงสภาวะการแข่งขันเชิงตรรกะ

สามารถกำจัด Race Condition ใน Android ได้อย่างสมบูรณ์หรือไม่?

ไม่สามารถกำจัดได้อย่างสมบูรณ์ แต่สามารถลดให้น้อยที่สุดได้ ใช้ออบเจ็กต์ที่ไม่เปลี่ยนแปลง (immutable) ชนิดอะตอมิก และคอรูลีนกับตัวจัดส่งแบบเธรดเดียว เครื่องมือวิเคราะห์เชิงสถิต เช่น Android Lint กับกฎ ThreadSafety ช่วยตรวจจับการแข่งขันที่อาจเกิดขึ้นในขั้นตอนการคอมไพล์

Race Condition แสดงออกในแอปพลิเคชัน UI อย่างไร?

ในแอปพลิเคชัน UI Race Condition มักแสดงเป็นหน้าจอกระพริบ การแสดงข้อมูลผิดพลาด หรือการล้มเหลวเมื่ออัปเดตรายการ สถานการณ์ทั่วไป: เธรดพื้นหลังโหลดข้อมูลและอัปเดตอะแดปเตอร์ ขณะที่ผู้ใช้เลื่อนรายการ — เกิดการเข้าถึง Adapter DataSet พร้อมกัน

volatile คืออะไรและช่วยเรื่อง Race Condition หรือไม่?

volatile รับประกันการมองเห็นการเปลี่ยนแปลงระหว่างเธรด — การเขียนไปยังตัวแปร volatile จะเห็นได้ทันทีโดยทุกเธรด อย่างไรก็ตาม volatile ไม่ได้แก้ปัญหา Read-Modify-Write และ Check-Then-Act เพราะไม่รับประกันความเป็นอะตอมิกของการดำเนินการผสม สำหรับสถานการณ์เช่นนี้ จำเป็นต้องใช้ล็อคหรือคลาสอะตอมิก

Race Condition ใน Kotlin Coroutines แตกต่างจากเธรดแบบคลาสสิกอย่างไร?

ใน Kotlin Coroutines Race Condition เกิดขึ้นที่ระดับตัวจัดตารางเวลาคอรูลีน ไม่ใช่ตัวจัดตารางเวลาเธรดของระบบปฏิบัติการ คอรูลีนสามารถสลับที่จุดพัก (suspend) ซึ่งสร้างโอกาสเพิ่มเติมสำหรับการแข่งขัน เครื่องมือ kotlinx.coroutines.debug และดีบักเกอร์ของ IntelliJ IDEA ช่วยติดตามสถานะของคอรูลีน

สรุป

  • Race Condition — ข้อผิดพลาดในโค้ดแบบหลายเธรดที่ผลลัพธ์ขึ้นอยู่กับลำดับการทำงานของเธรดที่คาดเดาไม่ได้
  • Data Race — ชนิดย่อยของสภาวะการแข่งขัน เกิดจากการเข้าถึงหน่วยความจำพร้อมกันแบบไม่ซิงโครไนซ์พร้อมการเขียน
  • การดำเนินการที่ไม่เป็นอะตอมิก (Read-Modify-Write, Check-Then-Act) — สาเหตุหลักของการแข่งขันของเธรด
  • ThreadSanitizer และ Lincheck — เครื่องมือที่มีประสิทธิภาพในการตรวจจับ Race Condition ในขั้นตอนการทดสอบ
  • ตัวแปรแบบอะตอมิก (AtomicInteger) — วิธีที่เหมาะสมที่สุดในการป้องกันการดำเนินการเดี่ยวโดยไม่ต้องล็อค
  • Mutex และ Actor-model — แนวทางเชิงสถาปัตยกรรมสำหรับการป้องกันส่วนวิกฤตที่ซับซ้อน
  • การแยกสถานะ ผ่านออบเจ็กต์ที่ไม่เปลี่ยนแปลงและตัวจัดส่งแบบเธรดเดียวกําจัด Race Condition ในระดับการออกแบบอย่างสมบูรณ์

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

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

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

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