@MainActor เป็นแอ็กเตอร์ระดับโลกในภาษา Swift ที่รับประกันการทำงานของโค้ดบนเธรดหลัก ตามข้อมูลจาก Apple Developer, 2024 @MainActor จะทำให้การสลับไปยังเธรดหลักเป็นไปโดยอัตโนมัติเมื่อทำงานกับ UI ช่วยให้นักพัฒนาไม่ต้องเรียก DispatchQueue.main.async ด้วยตนเอง คำอธิบายประกอบนี้ปรากฏใน Swift 5.5 พร้อมกับระบบ async/await
ประเด็นสำคัญ
@MainActor เป็นแอ็กเตอร์ระดับโลกใน Swift ที่รวมคุณสมบัติของแอ็กเตอร์เข้ากับการรับประกันการทำงานบนเธรดหลักของแอปพลิเคชัน มันเป็นส่วนหนึ่งของระบบการทำงานพร้อมกันของ Swift ที่เปิดตัวใน Swift 5.5 พร้อมกับ async/await และการทำงานพร้อมกันแบบมีโครงสร้าง คำอธิบายประกอบช่วยให้นักพัฒนาไม่ต้องกังวลเกี่ยวกับการสลับเธรดด้วยตนเองและลดจำนวนข้อผิดพลาดของ UI
แอ็กเตอร์ใน Swift เป็นชนิดอ้างอิงที่แยกสถานะของตัวเองและรับประกันว่ามีเพียงเธรดเดียวเท่านั้นที่สามารถแก้ไขได้ @MainActor เป็น แอ็กเตอร์ระดับโลก พิเศษซึ่งมีตัวดำเนินการคือเธรดหลัก โค้ดใดๆ ที่ถูกทำเครื่องหมายด้วย @MainActor จะทำงานบนเธรดหลัก — แม้ว่าจะถูกเรียกจากงานเบื้องหลังก็ตาม
ก่อน @MainActor นักพัฒนาเปลี่ยนไปยังเธรดหลักด้วยตนเองผ่าน DispatchQueue.main.async นี่เป็นแหล่งที่มาของข้อผิดพลาดบ่อยครั้ง: นักพัฒนาลืมสลับ ทำให้เกิดการค้างเนื่องจากการอัปเดต UI ที่ไม่ได้อยู่บนเธรดหลัก @MainActor แก้ปัญหานี้ได้ในระดับระบบชนิด
ที่มาของข้อบกพร่องส่วนใหญ่ในแอปพลิเคชัน iOS คือ ความไม่ปลอดภัยของ UI — การอัปเดตอินเทอร์เฟซจากเธรดเบื้องหลัง Apple ได้รวม @MainActor เข้าไปใน Swift Concurrency เพื่อทำให้การสลับไปยังเธรดหลักเป็นแบบอัตโนมัติและตรวจสอบได้โดยคอมไพเลอร์ ซึ่งกำจัดข้อผิดพลาดรันไทม์ทั้งคลาส
หลักการทำงานของ @MainActor ขึ้นอยู่กับระบบการทำงานของ Swift Concurrency เมื่อเธรดเรียกฟังก์ชันที่ทำเครื่องหมายด้วย @MainActor ตัวจัดตารางเวลาจะระงับมันบนตัวดำเนินการปัจจุบันและดำเนินการต่อบนเธรดหลัก คอมไพเลอร์จะติดตามขอบเขตการเรียกและรับประกันความปลอดภัย
การทำงานของ @MainActor จัดการโดย MainActor.shared — ตัวดำเนินการที่เชื่อมโยงกับเธรดหลักของแอปพลิเคชัน เมื่อฟังก์ชันแบบอะซิงโครนัสถูกทำเครื่องหมายด้วย @MainActor มันจะดำเนินการต่อบนตัวดำเนินการนี้เสมอ ไม่ว่าเธรดใดที่งานเดิมถูกเริ่มต้น
import SwiftUI
class ViewModel: ObservableObject {
@Published var items: [String] = []
@MainActor
func loadData() async {
let result = await fetchRemoteData()
items = result // อย่างปลอดภัย MainActor รับประกันเธรดหลัก
}
}
หากฟังก์ชันถูกทำเครื่องหมายด้วย @MainActor และเรียกฟังก์ชันแบบอะซิงโครนัสอื่น มันจะสืบทอด บริบทของแอ็กเตอร์ โดยค่าเริ่มต้น ซึ่งหมายความว่าการเรียกที่ซ้อนกันทั้งหมดก็ทำงานบนเธรดหลักเช่นกัน เว้นแต่จะระบุเป็นอย่างอื่น คอมไพเลอร์ติดตามสิ่งนี้และแสดงข้อผิดพลาดเมื่อพยายามส่งคลosure ที่ไม่สอดคล้องกัน
การเปรียบเทียบ @MainActor และ DispatchQueue.main ช่วยให้เข้าใจว่าทำไมกลไกใหม่จึงถือว่าปลอดภัยและสะดวกกว่า แม้ว่าทั้งสองจะแก้ปัญหาเดียวกัน — การทำงานของโค้ดบนเธรดหลัก
@MainActor คือการตรวจสอบ ในระดับคอมไพเลอร์ หากคุณพยายามเรียกฟังก์ชัน @MainActor จากบริบทที่ไม่ปลอดภัย คอมไพเลอร์จะแสดงคำเตือนหรือข้อผิดพลาด DispatchQueue.main.async คือการเรียกรันไทม์: โค้ดจะคอมไพล์ได้แต่อาจค้างในรันไทม์เมื่อพยายามอัปเดต UI จากเธรดเบื้องหลัง
DispatchQueue.main.async เพิ่มบล็อกลงในคิวที่อาจทำงานด้วยความล่าช้า @MainActor กับ async/await ทำ การสลับตัวดำเนินการโดยตรง โดยไม่สร้างคลosure ที่ไม่จำเป็น ซึ่งช่วยลดค่าใช้จ่ายและทำให้เวลาทำงานคาดเดาได้มากขึ้น
// แนวทางเก่า
DispatchQueue.main.async {
self.updateUI()
}
// แนวทางใหม่กับ @MainActor
@MainActor
func updateUI() {
// ทำงานบนเธรดหลัก
self.label.text = "อัปเดตแล้ว"
}
| เกณฑ์ | @MainActor | DispatchQueue.main |
|---|---|---|
| การตรวจสอบ | คอมไพเลอร์ | รันไทม์ |
| ไวยากรณ์ | คำอธิบายประกอบ (เชิงประกาศ) | การเรียก (เชิงคำสั่ง) |
| ค่าใช้จ่าย | ต่ำ (การสลับตัวดำเนินการ) | ปานกลาง (คลosure + คิว) |
| ความสามารถในการทดสอบ | สูง (สามารถเปลี่ยน MainActor.shared ได้) | ต่ำ (ยากที่จะจำลอง) |
ในโปรเจกต์ iOS จริง @MainActor ถูกใช้ในชั้น ViewModel มุมมอง SwiftUI และตัวควบคุม UIKit คำอธิบายประกอบสามารถใช้ได้ทั้งกับเมธอดเดี่ยวและทั้งชนิด
การทำเครื่องหมายคลาสด้วย @MainActor คุณรับประกันว่า เมธอดและคุณสมบัติทั้งหมด สามารถเข้าถึงได้เฉพาะบนเธรดหลักเท่านั้น สิ่งนี้สะดวกเป็นพิเศษสำหรับมุมมอง SwiftUI และคลาส ObservableObject: คุณเพียงเพิ่ม @MainActor หน้า class และคุณสมบัติ @Published ทั้งหมดจะอัปเดตอย่างปลอดภัย
@MainActor
final class UserListViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
func fetchUsers() async {
isLoading = true
users = await api.getUsers()
isLoading = false
}
}
เมื่อทำงานกับโค้ด UIKit ดั้งเดิมที่การสลับเธรดเป็นแบบแมนนวล คุณสามารถใช้ MainActor.run สำหรับการสลับอย่างชัดเจน สิ่งนี้สะดวกสำหรับการเปลี่ยนผ่านไปยัง Swift Concurrency แบบค่อยเป็นค่อยไปโดยไม่ต้องเขียนฐานโค้ดทั้งหมดใหม่
await MainActor.run {
self.tableView.reloadData()
}
แม้จะมีข้อดีทั้งหมด @MainActor มีข้อจำกัดหลายประการที่สำคัญต้องพิจารณาเมื่อออกแบบสถาปัตยกรรมแอปพลิเคชัน การทำความเข้าใจขอบเขตของการนำไปใช้ช่วยหลีกเลี่ยงการใช้งานที่ไม่ถูกต้อง
หากสายการเรียกทั้งหมดถูกทำเครื่องหมายด้วย @MainActor งานหนักใดๆ จะทำงานบนเธรดหลัก ทำให้ UI ค้าง ขอแนะนำให้ทำเครื่องหมายเฉพาะชั้น UI ด้วย @MainActor โดยปล่อยให้ลอจิกทางธุรกิจและคำขอเครือข่ายอยู่ในแอ็กเตอร์เบื้องหลังหรือตัวดำเนินการระดับโลก
API แบบ callback ดั้งเดิม (เช่น URLSession ที่ไม่มี async/await) ไม่รองรับบริบทของแอ็กเตอร์ การรวมต้องใช้ตัวห่อด้วย CheckedContinuation นอกจากนี้ @MainActor ไม่เข้ากันกับ performSelector, target-action และแพทเทิร์นที่ไม่ใช่แบบอะซิงโครนัสอื่นๆ ของ UIKit
เมื่อดีบักแอปพลิเคชันด้วย @MainActor การจำลอง สภาวะการแข่งขัน ทำได้ยากกว่าเพราะคอมไพเลอร์ป้องกันหลายอย่างในเวลาสร้างแทนที่จะเป็นรันไทม์ อย่างไรก็ตาม สิ่งนี้อาจสร้างความรู้สึกปลอดภัยที่ผิดพลาด: การทำงานที่ไม่ถูกต้องกับออบเจ็กต์ที่เปลี่ยนแปลงได้ซึ่งใช้ร่วมกัน (เช่น NSCache หรือตัวแปรระดับโลกที่ใช้ร่วมกัน) ยังคงเป็นไปได้หากไม่ได้ทำเครื่องหมายด้วย @MainActor และใช้โดยไม่มีการซิงโครไนซ์อย่างชัดเจน
@MainActor ช่วยลดความซับซ้อนของการทดสอบลอจิก UI อย่างมากเนื่องจากไม่ต้องสลับเธรดด้วยตนเองในการทดสอบ อย่างไรก็ตาม มีลักษณะเฉพาะที่ต้องพิจารณาเมื่อเขียนการทดสอบหน่วยและการทดสอบ UI
ใน XCTest สภาพแวดล้อมการทดสอบจะตั้งค่า ตัวดำเนินการเธรดหลัก โดยอัตโนมัติ เมื่อเมธอดทดสอบทำงานบนเธรดหลัก การเรียกฟังก์ชัน @MainActor ไม่ต้องการการตั้งค่าเพิ่มเติม — ฟังก์ชันเหล่านี้ทำงานในบริบทเดียวกัน สำหรับการทดสอบสถานการณ์เบื้องหลัง ให้ใช้ MainActor.run ภายใน Task ที่มีลำดับความสำคัญและตัวดำเนินการอย่างชัดเจน โดยตรวจสอบแยกต่างหากว่าโค้ดทำงานถูกต้องเมื่อเรียกจากเบื้องหลัง
วิธีการทั่วไปคือการทดสอบ ViewModel กับ @MainActor ซึ่งตรวจสอบว่าคุณสมบัติ @Published อัปเดตอย่างถูกต้องหลังจากการดำเนินการแบบอะซิงโครนัส เนื่องจากการสืบทอดบริบทของแอ็กเตอร์ การเรียก await ภายในทดสอบรับประกันการทำงานบนเธรดหลักโดยไม่ต้องมีการรับประกัน DispatchQueue เพิ่มเติมหรือการสลับบริบทด้วยตนเอง ซึ่งทำให้การเขียนทดสอบง่ายขึ้น
เมื่อปรับโครงสร้างโค้ดที่มีอยู่เป็น Swift Concurrency ให้ตรวจสอบ การแยกตัวของ @MainActor ผ่านคอมไพเลอร์: การเรียกใดๆ ไปยังเมธอดซิงโครนัสที่ไม่มี @MainActor จากบริบท @MainActor จะถูกทำเครื่องหมายเป็นข้อผิดพลาด คุณสมบัตินี้ใช้สำหรับการย้ายโปรเจกต์ไปยัง async/await แบบค่อยเป็นค่อยไป: คุณทำเครื่องหมายชั้น ViewModel เป็น @MainActor และคอมไพเลอร์จะเน้นการเรียกที่ไม่ปลอดภัยทั้งหมดที่ต้องย้ายไปยังแอ็กเตอร์เบื้องหลัง
เมื่อสร้างการจำลองสำหรับการพึ่งพา @MainActor ให้ใช้ โปรโตคอลที่มีเมธอด async ที่ประกาศฟังก์ชันแบบอะซิงโครนัสพร้อมชนิดส่งคืน สิ่งนี้ช่วยให้แทนที่บริการเครือข่าย ฐานข้อมูล และการพึ่งพาภายนอกอื่นๆ โดยไม่ทำลายการแยกตัวของแอ็กเตอร์ คอมไพเลอร์ตรวจสอบว่าการจำลองใช้ข้อกำหนดการแยกตัวทั้งหมด ป้องกันการเข้าถึงโค้ด @MainActor โดยไม่ได้ตั้งใจจากเธรดทดสอบเบื้องหลัง
เมื่อทดสอบโค้ด @MainActor แบบซิงโครนัส ให้ใช้ XCTestExpectation เพื่อรอให้การดำเนินการแบบอะซิงโครนัสเสร็จสมบูรณ์ ตั้งค่าความคาดหวังในการทดสอบและเรียก fulfillment ภายในคลosure ที่ทำงานบนเธรดหลัก หากการทดสอบค้างอย่างไม่มีกำหนด — แสดงว่าการเรียกบนเธรดหลักอาจไม่เกิดขึ้น และคุณต้องตรวจสอบการแยกตัวของแอ็กเตอร์ ในการดีบักบริบทการทำงาน การเพิ่มการตรวจสอบ Thread.isMainThread ภายในโค้ดทดสอบจะมีประโยชน์
คำถามที่พบบ่อย
ไม่ เพียงพอ ที่จะทำเครื่องหมายเฉพาะเมธอดที่อัปเดต UI อย่างไรก็ตาม หากคลาสมีเมธอดแบบนี้หลายวิธี การเพิ่ม @MainActor ทั้งคลาสจะง่ายกว่า สิ่งนี้รับประกันว่าสมาชิกทั้งหมดทำงานบนเธรดหลักและทำให้การบำรุงรักษาโค้ดง่ายขึ้น
@MainActor เป็นอินสแตนซ์เฉพาะของแอ็กเตอร์ระดับโลกที่เชื่อมโยงกับเธรดหลัก @globalActor เป็นโปรโตคอลสำหรับสร้างแอ็กเตอร์ระดับโลกของคุณเอง ตัวอย่างเช่น คุณสามารถสร้าง @BackgroundActor สำหรับทำงานโค้ดบนเธรดเบื้องหลังหากสถาปัตยกรรมโปรเจกต์ต้องการ
ได้ ฟังก์ชันซิงโครนัส กับ @MainActor ก็ทำงานบนเธรดหลักเช่นกัน อย่างไรก็ตาม คุณค่าหลักของ @MainActor เปิดเผยกับ async/await เมื่อฟังก์ชันแบบอะซิงโครนัสกลับมาทำงานบนเธรดหลักโดยอัตโนมัติโดยไม่ต้องสลับด้วยตนเองผ่าน DispatchQueue.main
Task.cancel() ทำงานกับงาน @MainActor เช่นเดียวกับงานปกติ งาน @MainActor สามารถตรวจสอบ Task.isCancelled หรือโยน CancellationError เมื่อยกเลิก เธรดหลักจะไม่ถูกบล็อก — งานเพียงหยุดทำงานที่จุดระงับที่ใกล้ที่สุด
คอมไพเลอร์ รับประกันความปลอดภัย: หากคุณเรียกฟังก์ชัน @MainActor จากบริบทเบื้องหลัง คอมไพเลอร์จะชี้ให้เห็นข้อผิดพลาด สำหรับการเรียกแบบอะซิงโครนัส เพียงทำเครื่องหมายโค้ดที่เรียกด้วย await และตัวดำเนินการจะสลับไปยังเธรดหลักเอง สำหรับการเรียกแบบซิงโครนัส จำเป็นต้องสลับอย่างชัดเจนผ่าน MainActor.run
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม