@MainActor — khái niệm, ứng dụng và đặc điểm trong mã bất đồng bộ Swift

Tác giả: IT Sectr Đã đăng: 2026-03-19 Thời gian đọc: 8 phút

@MainActor là một tác tử toàn cục trong ngôn ngữ Swift, đảm bảo thực thi mã trên luồng chính. Theo Apple Developer, 2024, @MainActor tự động chuyển sang luồng chính khi làm việc với UI, giải phóng nhà phát triển khỏi việc gọi thủ công DispatchQueue.main.async. Chú thích này xuất hiện trong Swift 5.5 cùng với hệ thống async/await.

Ý chính

  • @MainActor — tác tử toàn cục Swift đảm bảo thực thi trên luồng chính.
  • Hệ thống async/await — nền tảng mà @MainActor được xây dựng trên đó.
  • Chú thích lớp tự động đặt tất cả các phương thức của nó lên luồng chính.
  • Khác với DispatchQueue.main, @MainActor kiểm tra luồng ở cấp trình biên dịch.
  • Cập nhật UI — lĩnh vực ứng dụng chính của @MainActor trong phát triển iOS.

@MainActor là gì?

@MainActor là một tác tử toàn cục trong Swift kết hợp các thuộc tính của tác tử với sự đảm bảo thực thi trên luồng chính của ứng dụng. Nó là một phần của hệ thống đồng thời Swift được giới thiệu trong Swift 5.5 cùng với async/await và đồng thời có cấu trúc. Chú thích cho phép nhà phát triển không phải lo lắng về việc chuyển đổi luồng thủ công và giảm số lượng lỗi UI.

Định nghĩa và vị trí trong Swift Concurrency

Tác tử trong Swift là một kiểu tham chiếu cô lập trạng thái của nó và đảm bảo chỉ một luồng có thể sửa đổi nó. @MainActor là một tác tử toàn cục đặc biệt có bộ thực thi là luồng chính. Bất kỳ mã nào được đánh dấu bằng @MainActor đều thực thi trên luồng chính — ngay cả khi được gọi từ một tác vụ nền.

Trước @MainActor, các nhà phát triển chuyển thủ công sang luồng chính thông qua DispatchQueue.main.async. Đây là nguồn gốc của các lỗi thường xuyên: nhà phát triển quên chuyển đổi, dẫn đến sập do cập nhật UI không trên luồng chính. @MainActor giải quyết vấn đề này ở cấp hệ thống kiểu.

Lý do tạo ra

Nguồn gốc của hầu hết lỗi trong ứng dụng iOS là sự không an toàn của UI — cập nhật giao diện từ luồng nền. Apple đã tích hợp @MainActor vào Swift Concurrency để việc chuyển sang luồng chính trở nên tự động và có thể kiểm tra bởi trình biên dịch, loại bỏ toàn bộ một lớp lỗi runtime.

@MainActor hoạt động như thế nào?

Nguyên lý hoạt động của @MainActor dựa trên hệ thống thực thi Swift Concurrency. Khi một luồng gọi hàm được đánh dấu bằng @MainActor, bộ lập lịch sẽ tạm dừng nó trên bộ thực thi hiện tại và tiếp tục trên luồng chính. Trình biên dịch theo dõi ranh giới cuộc gọi và đảm bảo an toàn.

Bộ thực thi luồng chính

Việc thực thi @MainActor được quản lý bởi MainActor.shared — một bộ thực thi được liên kết với luồng chính của ứng dụng. Khi một hàm bất đồng bộ được đánh dấu bằng @MainActor, nó luôn tiếp tục trên bộ thực thi này, bất kể tác vụ gốc được bắt đầu trên luồng nào.

swift
import SwiftUI

class ViewModel: ObservableObject {
    @Published var items: [String] = []

    @MainActor
    func loadData() async {
        let result = await fetchRemoteData()
        items = result  // một cách an toàn, MainActor đảm bảo luồng chính
    }
}

Kế thừa ngữ cảnh tác tử

Nếu một hàm được đánh dấu bằng @MainActor và gọi một hàm bất đồng bộ khác, nó sẽ kế thừa ngữ cảnh tác tử theo mặc định. Điều này có nghĩa là tất cả các cuộc gọi lồng nhau cũng thực thi trên luồng chính, trừ khi được chỉ định khác. Trình biên dịch theo dõi điều này và đưa ra lỗi khi cố gắng truyền một closure không nhất quán.

@MainActor vs DispatchQueue.main

So sánh @MainActor và DispatchQueue.main giúp hiểu tại sao cơ chế mới được coi là an toàn và tiện lợi hơn, mặc dù cả hai đều giải quyết cùng một nhiệm vụ — thực thi mã trên luồng chính.

An toàn ở cấp kiểu

@MainActor là sự kiểm tra ở cấp trình biên dịch. Nếu bạn cố gắng gọi hàm @MainActor từ ngữ cảnh không an toàn, trình biên dịch sẽ đưa ra cảnh báo hoặc lỗi. DispatchQueue.main.async là một cuộc gọi runtime: mã sẽ biên dịch nhưng có thể sập tại runtime khi cố gắng cập nhật UI từ luồng nền.

Hiệu suất và chi phí

DispatchQueue.main.async thêm một khối vào hàng đợi có thể được thực thi với độ trễ. @MainActor với async/await thực hiện chuyển đổi bộ thực thi trực tiếp mà không tạo closure không cần thiết. Điều này giảm chi phí và làm cho thời gian thực thi dễ dự đoán hơn.

swift
// Cách tiếp cận cũ
DispatchQueue.main.async {
    self.updateUI()
}

// Cách tiếp cận mới với @MainActor
@MainActor
func updateUI() {
    // thực thi trên luồng chính
    self.label.text = "Đã cập nhật"
}
Tiêu chí@MainActorDispatchQueue.main
Kiểm tratrình biên dịchruntime
Cú phápchú thích (khai báo)gọi (mệnh lệnh)
Chi phíthấp (chuyển đổi bộ thực thi)trung bình (closure + hàng đợi)
Khả năng kiểm thửcao (có thể thay thế MainActor.shared)thấp (khó giả lập)

Sử dụng @MainActor trong các dự án iOS

Trong các dự án iOS thực tế, @MainActor được sử dụng trong các lớp ViewModel, SwiftUI views và UIKit controllers. Chú thích có thể được áp dụng cho cả các phương thức riêng lẻ và toàn bộ kiểu.

Chú thích lớp hoặc cấu trúc

Bằng cách đánh dấu một lớp với @MainActor, bạn đảm bảo rằng tất cả các phương thức và thuộc tính của nó chỉ có thể truy cập trên luồng chính. Điều này đặc biệt thuận tiện cho SwiftUI views và các lớp ObservableObject: bạn chỉ cần thêm @MainActor trước class, và tất cả các thuộc tính @Published được cập nhật an toàn.

swift
@MainActor
final class UserListViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false

    func fetchUsers() async {
        isLoading = true
        users = await api.getUsers()
        isLoading = false
    }
}

Bọc mã kế thừa

Khi làm việc với mã UIKit cũ nơi việc chuyển đổi luồng là thủ công, bạn có thể sử dụng MainActor.run để chuyển đổi rõ ràng. Điều này thuận tiện cho quá trình chuyển đổi dần dần sang Swift Concurrency mà không cần viết lại toàn bộ cơ sở mã.

swift
await MainActor.run {
    self.tableView.reloadData()
}

Hạn chế của @MainActor

Mặc dù có tất cả các lợi ích, @MainActor có một số hạn chế cần xem xét khi thiết kế kiến trúc ứng dụng. Hiểu ranh giới áp dụng giúp tránh sử dụng không đúng.

Hiệu suất khi sử dụng nhiều

Nếu toàn bộ chuỗi cuộc gọi được đánh dấu bằng @MainActor, thì bất kỳ công việc nặng nào sẽ được thực hiện trên luồng chính, gây ra đơ UI. Khuyến nghị chỉ đánh dấu lớp UI bằng @MainActor, để logic kinh doanh và yêu cầu mạng trong các tác tử nền hoặc bộ thực thi toàn cục.

Không tương thích với một số API

Các API cũ dựa trên callback (ví dụ: URLSession không có async/await) không hỗ trợ ngữ cảnh tác tử. Tích hợp yêu cầu một wrapper với CheckedContinuation. Ngoài ra, @MainActor không tương thích với performSelector, target-action và các mẫu không bất đồng bộ khác của UIKit.

Gỡ lỗi đa luồng

Khi gỡ lỗi ứng dụng với @MainActor, việc tái tạo điều kiện cạnh tranh khó hơn vì trình biên dịch ngăn chặn nhiều điều kiện tại thời điểm xây dựng thay vì runtime. Tuy nhiên, điều này có thể tạo ra cảm giác an toàn sai lầm: làm việc không đúng với các đối tượng có thể thay đổi dùng chung (ví dụ: NSCache hoặc biến toàn cục dùng chung) vẫn có thể xảy ra nếu chúng không được đánh dấu bằng @MainActor và được sử dụng mà không có đồng bộ hóa rõ ràng.

Kiểm thử @MainActor

@MainActor đơn giản hóa đáng kể việc kiểm thử logic UI vì nó loại bỏ nhu cầu chuyển đổi luồng thủ công trong các bài kiểm thử. Tuy nhiên, có những điểm đặc thù cần xem xét khi viết kiểm thử đơn vị và kiểm thử UI.

Kiểm thử đơn vị với MainActor

Trong XCTest, môi trường kiểm thử tự động thiết lập bộ thực thi luồng chính. Khi một phương thức kiểm thử chạy trên luồng chính, việc gọi các hàm @MainActor không yêu cầu thiết lập bổ sung — chúng thực thi trong cùng ngữ cảnh. Để kiểm thử các kịch bản nền, hãy sử dụng MainActor.run bên trong Task với ưu tiên và bộ thực thi rõ ràng, xác minh riêng rằng mã hoạt động chính xác khi được gọi từ nền.

Một cách tiếp cận phổ biến là kiểm thử ViewModel với @MainActor, nơi xác minh rằng các thuộc tính @Published cập nhật chính xác sau các hoạt động bất đồng bộ. Nhờ kế thừa ngữ cảnh tác tử, việc gọi await bên trong kiểm thử đảm bảo thực thi trên luồng chính mà không cần thêm các đảm bảo DispatchQueue hay chuyển đổi ngữ cảnh thủ công, giúp đơn giản hóa việc viết kiểm thử.

Kiểm tra sự cô lập khi tái cấu trúc

Khi tái cấu trúc mã hiện có sang Swift Concurrency, hãy kiểm tra sự cô lập @MainActor thông qua trình biên dịch: bất kỳ cuộc gọi nào đến phương thức đồng bộ không có @MainActor từ ngữ cảnh @MainActor được đánh dấu là lỗi. Thuộc tính này được sử dụng để di chuyển dần dự án sang async/await: bạn đánh dấu lớp ViewModel là @MainActor, và trình biên dịch làm nổi bật tất cả các cuộc gọi không an toàn cần được di chuyển sang các tác tử nền.

Giả lập và ngữ cảnh tác tử

Khi tạo giả lập cho các phụ thuộc @MainActor, hãy sử dụng giao thức với phương thức async khai báo các hàm bất đồng bộ với kiểu trả về. Điều này cho phép thay thế các dịch vụ mạng, cơ sở dữ liệu và các phụ thuộc bên ngoài khác mà không phá vỡ sự cô lập tác tử. Trình biên dịch xác minh rằng giả lập thực hiện tất cả các yêu cầu cô lập, ngăn chặn truy cập vô tình vào mã @MainActor từ các luồng kiểm thử nền.

Chờ đợi các hoạt động bất đồng bộ

Khi kiểm thử mã @MainActor một cách đồng bộ, hãy sử dụng XCTestExpectation để chờ đợi hoàn thành các hoạt động bất đồng bộ. Đặt kỳ vọng trong kiểm thử và gọi fulfillment bên trong một closure thực thi trên luồng chính. Nếu kiểm thử treo vô thời hạn — có thể cuộc gọi trên luồng chính không xảy ra, và bạn cần kiểm tra sự cô lập tác tử. Để gỡ lỗi ngữ cảnh thực thi, hãy thêm kiểm tra Thread.isMainThread bên trong mã kiểm thử.

Câu hỏi thường gặp

Có cần thiết phải đánh dấu toàn bộ lớp bằng @MainActor không?

Không, chỉ cần đánh dấu các phương thức cập nhật UI là đủ. Tuy nhiên, nếu một lớp có nhiều phương thức như vậy, việc thêm @MainActor vào toàn bộ lớp sẽ đơn giản hơn. Điều này đảm bảo rằng tất cả các thành viên của nó thực thi trên luồng chính và đơn giản hóa việc bảo trì mã.

@MainActor khác gì so với @globalActor?

@MainActor là một thể hiện cụ thể của tác tử toàn cục được gắn với luồng chính. @globalActor là một giao thức để tạo các tác tử toàn cục của riêng bạn. Ví dụ, bạn có thể tạo @BackgroundActor để thực thi mã trên luồng nền nếu kiến trúc dự án yêu cầu.

Có thể sử dụng @MainActor mà không có async/await không?

Có, các hàm đồng bộ với @MainActor cũng thực thi trên luồng chính. Tuy nhiên, giá trị chính của @MainActor được thể hiện với async/await, khi một hàm bất đồng bộ tự động tiếp tục trên luồng chính mà không cần chuyển đổi thủ công qua DispatchQueue.main.

Làm thế nào để hủy một tác vụ @MainActor?

Task.cancel() hoạt động với các tác vụ @MainActor giống như với các tác vụ thông thường. Một tác vụ @MainActor có thể kiểm tra Task.isCancelled hoặc ném CancellationError. Khi hủy, luồng chính không bị chặn — tác vụ chỉ đơn giản dừng thực thi tại điểm tạm dừng gần nhất.

Điều gì xảy ra nếu @MainActor được gọi từ luồng nền?

Trình biên dịch đảm bảo an toàn: nếu bạn gọi hàm @MainActor từ ngữ cảnh nền, trình biên dịch sẽ chỉ ra lỗi. Đối với các cuộc gọi bất đồng bộ, chỉ cần đánh dấu mã gọi bằng await, và bộ thực thi sẽ tự chuyển sang luồng chính. Đối với các cuộc gọi đồng bộ, cần chuyển đổi rõ ràng qua MainActor.run.

Tóm tắt

  • @MainActor — tác tử toàn cục Swift đảm bảo thực thi trên luồng chính.
  • Kiểm tra của trình biên dịch loại bỏ toàn bộ một lớp lỗi an toàn UI.
  • Chú thích toàn bộ lớp tự động đặt tất cả các phương thức của nó lên luồng chính.
  • MainActor.run — chuyển đổi rõ ràng cho mã kế thừa và ngữ cảnh đồng bộ.
  • Khác với DispatchQueue.main, @MainActor không tạo closure và sử dụng chuyển đổi bộ thực thi.
  • Tính toán nặng không nên thực thi dưới @MainActor để tránh đơ UI.
  • Kế thừa ngữ cảnh tác tử đơn giản hóa chuỗi các cuộc gọi bất đồng bộ và làm cho mã nhất quán, dễ dự đoán và an toàn cho UI.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm