withContext — เป็นฟังก์ชันสลับบริบทการทำงานภายในโครูทีน ที่เปลี่ยนเธรดหรือตัวจัดสรรชั่วคราวสำหรับบล็อกโค้ดที่กำหนดและส่งคืนผลลัพธ์กลับไปยังบริบทเดิม ตามข้อมูลจาก JetBrains, 2025 withContext เป็นหนึ่งในเครื่องมือโครูทีนที่ใช้บ่อยที่สุดสำหรับคำขอเครือข่ายและการดำเนินการดิสก์ ฟังก์ชันรับประกันว่าหลังจากบล็อกเสร็จสิ้น โครูทีนจะทำงานต่อบนตัวจัดสรรเดิม ซึ่งป้องกันข้อผิดพลาดด้านความปลอดภัยของเธรดโดยไม่ตั้งใจ
ประเด็นสำคัญ
withContext เป็นฟังก์ชันระงับจากแพ็คเกจ kotlinx.coroutines ที่ดำเนินการบล็อกโค้ดที่ส่งเข้าใน CoroutineContext ที่ระบุและส่งคืนผลลัพธ์กลับไปยังบริบทเดิม ลายเซ็นของฟังก์ชันมีดังนี้:
suspend fun withContext (
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
พารามิเตอร์ context รองรับ CoroutineContext ใด ๆ — โดยทั่วไปคือ Dispatchers.IO, Dispatchers.Default หรือ Dispatchers.Main มาตรฐาน บล็อกดำเนินการในบริบทนั้นและผลลัพธ์จะถูกส่งคืนไปยังตำแหน่งที่เรียก withContext
หลังจาก lambda เสร็จสิ้น withContext จะสลับการทำงานกลับไปยัง ตัวจัดสรรเดิม อย่างแน่นอน ซึ่งหมายความว่านักพัฒนาไม่จำเป็นต้องเรียก withContext(Dispatchers.Main) ด้วยตนเองหลังการดำเนินการพื้นหลัง — การกลับมาเกิดขึ้นโดยอัตโนมัติ พฤติกรรมนี้ถูกบันทึกไว้ในข้อกำหนด Kotlin Coroutines ตั้งแต่เวอร์ชัน 1.3
การพัฒนา Android เป็นพื้นที่หลักในการใช้ withContext สถานการณ์ทั่วไป: ViewModel เริ่มโครูทีนบนเธรดหลัก ภายในเรียก withContext(Dispatchers.IO) สำหรับคำขอเครือข่าย และผลลัพธ์หลังจากการกลับมายัง Main โดยอัตโนมัติจะใช้เพื่ออัปเดต UI วิธีการนี้เป็นพื้นฐานของสถาปัตยกรรม MVVM และได้รับการแนะนำโดย Google ในคู่มือโครูทีนอย่างเป็นทางการ
เพื่อเข้าใจ withContext คุณต้องเข้าใจ CoroutineContext และส่วนประกอบหลัก — ตัวจัดสรร (Dispatcher) โครูทีนแต่ละตัวมีชุดองค์ประกอบบริบท ซึ่งตัวจัดสรรจะกำหนดว่าโค้ดจะทำงานบนเธรดหรือกลุ่มเธรดใด
| ตัวจัดสรร | วัตถุประสงค์ | ขนาดกลุ่ม |
|---|---|---|
| Dispatchers.Main | เธรดหลัก UI (Android, JavaFX, Swing) | 1 (เธรดหลัก) |
| Dispatchers.IO | การดำเนินการดิสก์และเครือข่าย | 64 เธรด (ขีดจำกัดเพิ่มขึ้น) |
| Dispatchers.Default | การคำนวณที่ใช้ CPU สูง | max(2, จำนวนคอร์) |
| Dispatchers.Unconfined | ไม่มีเธรดคงที่ | ไม่จำกัด |
สิ่งสำคัญที่ต้องเข้าใจคือ withContext ไม่ได้สร้างโครูทีนใหม่ — มันเพียง สลับบริบท สำหรับโครูทีนที่มีอยู่ นี่คือความแตกต่างหลักจาก launch และ async ซึ่งสร้างโครูทีนใหม่ การทำงานภายในของ withContext ได้รับการปรับให้เหมาะสม: หากบริบทที่ร้องขอตรงกับบริบทปัจจุบัน จะไม่มีการสลับเกิดขึ้น — ฟังก์ชันทำงานบนตัวจัดสรรเดียวกัน
Dispatchers.Main ภายใน withContext(Dispatchers.Main) ไม่ทำให้เกิดการสลับ — Kotlin Coroutines รู้จักความเหมือนกันของบริบทและข้ามการดำเนินการที่ไม่จำเป็น เช่นเดียวกัน withContext(Dispatchers.Default) ภายในโครูทีนที่ทำงานบน Default อยู่แล้วไม่สร้างค่าใช้จ่ายเพิ่มเติม การปรับให้เหมาะสมนี้ถูกนำไปใช้ใน ContinuationInterceptor
ผู้เริ่มต้นมักสับสน between withContext กับ launch และ async เนื่องจากทั้งสามฟังก์ชันทำงานกับโครูทีนและบริบท อย่างไรก็ตาม วัตถุประสงค์ของพวกมันแตกต่างกันโดยพื้นฐาน
| ลักษณะ | withContext | launch | async |
|---|---|---|---|
| สร้างโครูทีนใหม่ | ไม่ | ใช่ | ใช่ |
| ส่งคืนผลลัพธ์ | ใช่ (T โดยตรง) | ไม่ (Job) | ใช่ (Deferred<T>) |
| การดำเนินการ | ตามลำดับ | ขนาน | ขนาน |
| รอผลลัพธ์ | อัตโนมัติ | join() | await() |
| กรณีการใช้งานทั่วไป | เปลี่ยนตัวจัดสรร | ยิงแล้วลืม | การคำนวณขนาน |
หากคุณต้องการดำเนินการ หนึ่งการดำเนินการ บนเธรดพื้นหลังและรับผลลัพธ์ — ใช้ withContext หากคุณต้องการเรียกใช้หลายการดำเนินการอิสระแบบขนาน — ใช้ async กับ await หากคุณไม่ต้องการผลลัพธ์ (การบันทึก, การเขียนแคช) — ใช้ launch Google แนะนำ withContext เป็นเครื่องมือที่ต้องการสำหรับ ชั้น Repository ในสถาปัตยกรรม Android
มาดูสามสถานการณ์เชิงปฏิบัติของการใช้ withContext ในแอปพลิเคชัน Android ด้วย Kotlin แต่ละตัวอย่างแสดงงานเฉพาะและรูปแบบที่ถูกต้อง
ViewModel เรียกเมธอดของ repository จากโครูทีนบน Main ภายใน withContext(Dispatchers.IO) ดำเนินการคำขอ HTTP และผลลัพธ์ถูกส่งคืนโดยอัตโนมัติ:
class UserRepository(
private val api: UserApi
) {
suspend fun getUser(id: String): User {
return withContext(Dispatchers.IO) {
api.fetchUser(id)
}
}
}
โครูทีนใน ViewModel เรียก getUser เหมือนฟังก์ชันระงับปกติ — โดยไม่ระบุตัวจัดสรรอย่างชัดเจน withContext ซ่อนรายละเอียดของการสลับเธรด
เมื่อคุณต้องการดำเนินการ IO หลายรายการทีละรายการ withContext รวมเข้าด้วยกันเป็นบล็อกเดียว ซึ่งมีประสิทธิภาพมากกว่าการห่อแต่ละการดำเนินการใน withContext แยกต่างหาก:
suspend fun loadUserProfile(id: String): Profile {
return withContext(Dispatchers.IO) {
val user = api.fetchUser(id)
val posts = api.fetchPosts(id)
Profile(user, posts)
}
}
ทั้งสองการดำเนินการทำงานบน Dispatchers.IO และผลลัพธ์ Profile ถูกสร้างและส่งคืนโดยไม่มีการสลับบริบทที่ไม่จำเป็น หากการดำเนินการเป็นอิสระ ควรใช้ async สำหรับการดำเนินการแบบขนาน
ในบางสถานการณ์ คุณต้องดำเนินการโค้ดที่ไม่สามารถยกเลิกได้ — ตัวอย่างเช่น การบันทึกสถานะเมื่อปิดหน้าจอ การรวมกันของ withContext + NonCancellable แก้ปัญหานี้:
withContext(Dispatchers.IO + NonCancellable) {
cache.saveState(state)
analytics.logEvent("state_saved")
}
ตัวดำเนินการ + รวมองค์ประกอบบริบทสองรายการ: ตัวจัดสรร IO และแฟล็ก NonCancellable บล็อกทำงานแม้ว่าโครูทีนหลักจะถูกยกเลิก — มีประโยชน์สำหรับการดำเนินการสิ้นสุด
การทำงานภายในของ withContext อาศัยกลไก Continuation — สิ่งที่เป็นนามธรรมหลักของโครูทีน Kotlin แต่ละจุดระงับจะบันทึกสถานะการทำงานในวัตถุ Continuation และ withContext ก็ไม่เว้น
คอมไพเลอร์ Kotlin แปลง withContext เป็นการเรียกเมธอด withContext จาก kotlinx.coroutines ซึ่งภายในสร้างอินสแตนซ์ใหม่ของ DispatchedContinuation วัตถุนี้ห่อหุ้ม Continuation ดั้งเดิมและแทนที่ตัวจัดสรร หากตัวจัดสรรใหม่แตกต่างจากปัจจุบัน การดำเนินการจะถูกระงับ บล็อกจะถูกส่งไปยังกลุ่มเธรดที่เกี่ยวข้อง และหลังจากเสร็จสิ้น — ดำเนินการต่อกับบริบทเดิม
เมื่อ withContext ถูกเรียกด้วยตัวจัดสรรเดียวกันกับที่โครูทีนกำลังทำงานอยู่ Kotlin จะเปิดใช้งาน fast-path: บล็อกทำงานแบบซิงโครนัส โดยไม่สร้าง DispatchedContinuation และไม่ส่งไปยังกลุ่มเธรด ทำให้ withContext แทบไม่มีค่าใช้จ่ายสำหรับการเรียกซ้ำด้วยบริบทเดียวกัน ตามเกณฑ์มาตรฐานของ JetBrains (kotlinx.coroutines 1.8) fast-path เสร็จสิ้นในเวลา น้อยกว่า 0.1 µs
การเรียก withContext แต่ละครั้งด้วยตัวจัดสรรที่ แตกต่าง สร้าง DispatchedContinuation ใหม่และต้องสลับเธรด — ใช้เวลา 1 ถึง 5 µs ขึ้นอยู่กับโหลด สำหรับแอปพลิเคชันส่วนใหญ่ ความล่าช้านี้ไม่สังเกตเห็น แต่ภายในลูปที่มีการทำซ้ำหลายพันครั้ง ควรรวมการดำเนินการเป็นบล็อก withContext เดียว
แม้แต่นักพัฒนาที่มีประสบการณ์ก็ทำผิดพลาดเมื่อทำงานกับ withContext มาดูสี่ปัญหาที่พบบ่อยที่สุดและวิธีป้องกัน
นักพัฒนามักห่อแต่ละบรรทัดใน withContext แยกต่างหากแทนที่จะรวมการดำเนินการเป็นบล็อกเดียว การเรียกเพิ่มเติมแต่ละครั้งด้วยตัวจัดสรรที่แตกต่างสร้างค่าใช้จ่าย
วิธีที่ถูกต้อง: รวมการดำเนินการ IO ตามลำดับเป็น withContext(Dispatchers.IO) { ... } เดียว หากบางการดำเนินการใช้ CPU สูง — ใช้ withContext(Dispatchers.Default) ภายในบล็อกเดียวกัน
withContext ดำเนินการโค้ดตามลำดับ หากคำขอเครือข่ายอิสระสองรายการถูกห่อใน withContext เดียว พวกมันจะทำงานทีละรายการ สำหรับการทำงานขนาน ให้ใช้ async + await
// ตามลำดับ — ช้า
withContext(Dispatchers.IO) {
val a = api.fetchA()
val b = api.fetchB()
}
// ขนาน — เร็ว
coroutineScope {
val a = async { api.fetchA() }
val b = async { api.fetchB() }
println("${a.await()} ${b.await()}")
}
หากโครูทีนถูกยกเลิกระหว่าง withContext บล็อกบน Dispatchers.IO จะถูกขัดจังหวะเช่นกัน สำหรับการดำเนินการที่ต้องเสร็จสิ้นไม่ว่าจะอย่างไร (การเขียนฐานข้อมูล การส่งข้อมูลวิเคราะห์) ให้รวม withContext กับ NonCancellable
อย่าอัปเดตคอมโพเนนต์ View ภายใน withContext(Dispatchers.IO) withContext จะไม่กลับไปยัง Main จนกว่าบล็อกทั้งหมดจะเสร็จสมบูรณ์ วางการอัปเดต UI หลังจากวงเล็บปิดของ withContext — แล้วโครูทีนจะอยู่บนเธรดหลักแล้ว
คำถามที่พบบ่อย
withContext เป็นฟังก์ชันระงับที่ไม่บล็อกเธรด แต่สลับบริบทภายในโครูทีนที่มีอยู่ runBlocking เป็นสะพานระหว่างโครูทีนและโค้ดปกติที่บล็อกเธรดปัจจุบันจนกว่า will เสร็จสิ้น withContext ปลอดภัยสำหรับเธรด UI ส่วน runBlocking ไม่ปลอดภัย
ไม่ withContext เป็นฟังก์ชัน suspend ดังนั้นจึงเรียกได้เฉพาะจากฟังก์ชัน suspend อื่นหรือจากโครูทีน (launch/async) เท่านั้น จากฟังก์ชันปกติ ไม่สามารถเรียก withContext ได้ — สำหรับสิ่งนั้น你需要 runBlocking หรือ CoroutineScope
Kotlin เปิดใช้งาน fast-path — บล็อกทำงานแบบซิงโครนัสบนเธรดเดียวกันโดยไม่ต้องสลับ ค่าใช้จ่ายน้อยกว่า 0.1 µs ซึ่งไม่ใช่ข้อผิดพลาด แต่การเรียกเช่นนี้ซ้ำซ้อน — ควรดำเนินการโค้ดโดยไม่มี withContext จะดีกว่า
ข้อยกเว้นภายใน withContext แพร่กระจายเช่นเดียวกับในโค้ดปกติ — ผ่าน try-catch หากบล็อกโยนข้อยกเว้น มันจะแพร่กระจายไปยังโครูทีนหลักและยกเลิกมันหากไม่ได้รับการจัดการ ใช้ try-catch ภายใน withContext หรือรอบ ๆ มัน
ไม่ withContext ไม่ได้สร้างโครูทีนใหม่ มันใช้โครูทีนที่มีอยู่แต่เปลี่ยนบริบทชั่วคราว ซึ่งแตกต่างจาก launch และ async ที่สร้างโครูทีนลูก พฤติกรรมนี้ได้รับการยืนยันโดยซอร์สโค้ดของ kotlinx.coroutines
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม