@MainActor یک اکشنر سراسری در زبان Swift است که اجرای کد را روی نخ اصلی تضمین میکند. بر اساس Apple Developer, 2024، @MainActor جابهجایی به نخ اصلی را هنگام کار با UI خودکار میکند و برنامهنویس را از فراخوانی دستی DispatchQueue.main.async بینیاز میکند. این حاشیهنویسی در Swift 5.5 همراه با سیستم async/await معرفی شد.
نکات کلیدی
@MainActor یک اکشنر سراسری (global actor) در Swift است که ویژگیهای اکشنرها را با تضمین اجرا روی نخ اصلی برنامه ترکیب میکند. این بخشی از سیستم همروندی Swift است که در Swift 5.5 همراه با async/await و همروندی ساختاریافته معرفی شد. این حاشیهنویسی به برنامهنویس اجازه میدهد به جابهجایی نخها بهصورت دستی فکر نکند و تعداد خطاهای UI را کاهش میدهد.
اکشنر در Swift یک نوع ارجاعی است که حالت خود را ایزوله میکند و تضمین میکند فقط یک نخ میتواند آن را تغییر دهد. @MainActor یک اکشنر سراسری ویژه است که اجراکننده آن نخ اصلی است. هر کدی که با @MainActor مشخص شده باشد روی نخ اصلی اجرا میشود — حتی اگر از یک وظیفه پسزمینه فراخوانی شود.
پیش از ظهور @MainActor، توسعهدهندگان بهصورت دستی از طریق DispatchQueue.main.async به نخ اصلی جابهجا میشدند. این منبع خطاهای مکرر بود: توسعهدهندگان فراموش میکردند جابهجا شوند که به crash به دلیل بهروزرسانی UI در نخ غیراصلی منجر میشد. @MainActor این مشکل را در سطح سیستم انواع حل میکند.
منبع بیشتر باگها در برنامههای iOS، ناامنی UI است — بهروزرسانی رابط از نخ پسزمینه. اپل @MainActor را در Swift Concurrency تعبیه کرد تا جابهجایی به نخ اصلی را خودکار و قابل بررسی توسط کامپایلر کند و یک کلاس کامل از خطاهای runtime را از بین ببرد.
اصل کار @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 مشخص شده باشد و تابع ناهمزمان دیگری را فراخوانی کند، بهطور پیشفرض زمینه اکشنر را به ارث میبرد. این یعنی همه فراخوانیهای تو در تو نیز روی نخ اصلی اجرا میشوند، مگر اینکه خلاف آن مشخص شود. کامپایلر این را دنبال میکند و هنگام تلاش برای انتقال یک closure ناسازگار خطا میدهد.
مقایسه @MainActor و DispatchQueue.main به درک اینکه چرا مکانیزم جدید ایمنتر و راحتتر تلقی میشود کمک میکند، اگرچه هر دو یک کار را حل میکنند — اجرای کد روی نخ اصلی.
@MainActor بررسی در سطح کامپایلر است. اگر بخواهید تابع @MainActor را از یک زمینه ناامن فراخوانی کنید، کامپایلر هشدار یا خطا صادر میکند. DispatchQueue.main.async یک فراخوانی runtime است: کد کامپایل میشود، اما ممکن است در هنگام بهروزرسانی UI از نخ پسزمینه در runtime سقوط کند.
DispatchQueue.main.async یک بلوک به صف اضافه میکند که ممکن است با تأخیر اجرا شود. @MainActor با async/await جابهجایی مستقیم اجراکننده را بدون ایجاد closure اضافی انجام میدهد. این سربار را کاهش میدهد و کد را از نظر زمان اجرا قابل پیشبینیتر میکند.
// رویکرد قدیمی
DispatchQueue.main.async {
self.updateUI()
}
// رویکرد جدید با @MainActor
@MainActor
func updateUI() {
// روی نخ اصلی اجرا میشود
self.label.text = "بهروزرسانی شد"
}
| معیار | @MainActor | DispatchQueue.main |
|---|---|---|
| بررسی | کامپایلر | runtime |
| نحو | حاشیهنویسی (اعلانی) | فراخوانی (امری) |
| سربار | کم (جابهجایی اجراکننده) | متوسط (closure + صف) |
| قابلیت تست | بالا (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 بازتولید شرایط رقابت (race conditions) دشوارتر است، زیرا کامپایلر بسیاری از آنها را در مرحله ساخت جلوگیری میکند، نه در runtime. با این حال، این ممکن است احساس امنیت کاذب ایجاد کند: کار نادرست با اشیای mutable مشترک (مثلاً NSCache یا متغیرهای سراسری مشترک) همچنان ممکن است، اگر با @MainActor مشخص نشده باشند و بدون همگامسازی صریح استفاده شوند.
@MainActor تست منطق UI را بهطور قابل توجهی ساده میکند، زیرا نیاز به جابهجایی دستی نخها در تستها را از بین میبرد. با این حال ویژگیهایی وجود دارد که هنگام نوشتن تستهای واحد و تستهای UI باید در نظر گرفته شوند.
در XCTest محیط اجرا بهطور خودکار اجراکننده نخ اصلی را پیکربندی میکند. وقتی متد تست روی نخ اصلی اجرا میشود، فراخوانی تابعهای @MainActor به تنظیمات اضافی نیاز ندارد — آنها در همان زمینه اجرا میشوند. برای تست سناریوهای پسزمینه، از MainActor.run داخل Task با مشخص کردن صریح اولویت و اجراکننده استفاده کنید و جداگانه بررسی کنید که کد هنگام فراخوانی از پسزمینه بهدرستی کار میکند.
یکی از رویکردهای رایج، تست ViewModel با @MainActor است، که در آن بررسی میشود ویژگیهای @Published پس از عملیات ناهمزمان بهدرستی بهروزرسانی میشوند. به لطف ارثبری زمینه اکشنر، فراخوانی await داخل تست اجرا روی نخ اصلی را بدون تضمینهای اضافی DispatchQueue و جابهجایی دستی زمینهها تضمین میکند، که نوشتن تستها را ساده میکند.
هنگام بازسازی کد موجود به Swift Concurrency، ایزولهسازی @MainActor را از طریق کامپایلر بررسی کنید: هر فراخوانی متد همزمان بدون @MainActor از زمینه @MainActor بهعنوان خطا علامتگذاری میشود. این ویژگی برای انتقال تدریجی پروژه به async/await استفاده میشود: لایه ViewModel را با @MainActor مشخص میکنید و کامپایلر همه فراخوانیهای ناامن را که باید به اکشنرهای پسزمینه منتقل شوند برجسته میکند.
هنگام ایجاد mock برای وابستگیهای @MainActor، از پروتکلهایی با متدهای async استفاده کنید که تابعهای ناهمزمان با انواع بازگشتی اعلام میکنند. این امکان جایگزینی سرویسهای شبکهای، پایگاههای داده و سایر وابستگیهای خارجی را بدون نقض ایزولهسازی اکشنر فراهم میکند. کامپایلر بررسی میکند که mock همه الزامات ایزولهسازی را پیادهسازی کرده باشد و از دسترسی تصادفی به کد @MainActor از نخهای تست پسزمینه جلوگیری میکند.
در تست همزمان کد @MainActor، از XCTestExpectation برای انتظار اتمام عملیات ناهمزمان استفاده کنید. انتظار را در تست تنظیم کنید و fulfillment را داخل closure که روی نخ اصلی اجرا میشود انجام دهید. اگر تست بینهایت معلق بماند — احتمالاً فراخوانی روی نخ اصلی رخ نمیدهد و باید ایزولهسازی اکشنر را بررسی کنید. برای اشکالزدایی زمینه اجرا، مفید است بررسی Thread.isMainThread را داخل کد تست اضافه کنید.
سؤالات متداول
خیر، کافی است فقط متدهایی را مشخص کنید که UI را بهروزرسانی میکنند. با این حال، اگر در کلاس چند متد از این قبیل وجود داشته باشد، افزودن @MainActor به کل کلاس سادهتر است. این تضمین میکند همه اعضای آن روی نخ اصلی اجرا شوند و نگهداری کد را ساده میکند.
@MainActor یک نمونه مشخص از اکشنر سراسری است که به نخ اصلی وابسته است. @globalActor پروتکلی برای ایجاد اکشنرهای سراسری خودتان است. مثلاً میتوانید @BackgroundActor را برای اجرای کد روی نخ پسزمینه ایجاد کنید، اگر معماری پروژه آن را ایجاب کند.
بله، تابعهای همزمان با @MainActor نیز روی نخ اصلی اجرا میشوند. با این حال ارزش اصلی @MainActor دقیقاً با async/await نمایان میشود، وقتی تابع ناهمزمان بهطور خودکار بدون جابهجایی دستی از طریق DispatchQueue.main روی نخ اصلی از سر گرفته میشود.
Task.cancel() با وظایف @MainActor همانند وظایف معمولی کار میکند. وظیفه @MainActor میتواند Task.isCancelled را بررسی کند یا CancellationError را throw کند. هنگام لغو، نخ اصلی مسدود نمیشود — وظیفه فقط در نزدیکترین نقطه تعلیق متوقف میشود.
کامپایلر ایمنی را تضمین میکند: اگر تابع @MainActor را از زمینه پسزمینه فراخوانی کنید، کامپایلر خطا را نشان میدهد. برای فراخوانیهای ناهمزمان کافی است کد فراخواننده را با await مشخص کنید و اجراکننده خودش به نخ اصلی جابهجا میشود. برای فراخوانیهای همزمان، جابهجایی صریح از طریق MainActor.run لازم است.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید