همزمانی در Swift 6: راهنمای جامع async/await، اکتورها و Sendable
اگه یه توسعهدهنده iOS باشید، احتمالاً حداقل یک بار با باگهای عجیب و غریب همزمانی دست و پنجه نرم کردید. مسابقه داده (Data Race)، بنبست (Deadlock)، رفتارهای غیرقابل پیشبینی و اون باگهایی که فقط وقتی رئیس داره دمو رو نگاه میکنه ظاهر میشن — اینا کابوسهایی هستن که هر توسعهدهندهای حداقل یک بار تجربهشون کرده. اپل سالها از Grand Central Dispatch (GCD) به عنوان ابزار اصلی مدیریت چندنخی استفاده میکرد، اما با انتشار Swift 5.5 و تکامل زبان تا نسخه 6.x، دنیای همزمانی در Swift برای همیشه تغییر کرد.
در این راهنما، تمام مکانیزمهای کلیدی همزمانی مدرن در Swift رو بررسی میکنیم — از مبانی async/await تا اکتورها، همزمانی ساختاریافته، پروتکل Sendable و جدیدترین تغییرات در Swift 6.2. هدف اینه که به عنوان یک توسعهدهنده iOS، درک عمیق و عملی از این مفاهیم به دست بیارید.
چرا به همزمانی مدرن در Swift نیاز داشتیم؟
مشکلات رویکرد قدیمی با GCD
قبل از معرفی سیستم همزمانی مدرن Swift، رویکرد اصلی ما برای کارهای غیرهمزمان استفاده از GCD و کلوژرهای تکمیل (Completion Handlers) بود. بیایید یک مثال واقعی ببینیم:
// رویکرد قدیمی با GCD و Completion Handlers
func fetchUserProfile(userId: Int, completion: @escaping (Result<UserProfile, Error>) -> Void) {
URLSession.shared.dataTask(with: userURL) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NetworkError.noData))
return
}
do {
let profile = try JSONDecoder().decode(UserProfile.self, from: data)
// فراموش کردن برگشت به ترد اصلی - یک باگ رایج!
DispatchQueue.main.async {
completion(.success(profile))
}
} catch {
completion(.failure(error))
}
}.resume()
}
// استفاده - جهنم Callback
fetchUserProfile(userId: 42) { result in
switch result {
case .success(let profile):
fetchUserPosts(userId: profile.id) { postsResult in
switch postsResult {
case .success(let posts):
fetchComments(for: posts.first!) { commentsResult in
// هر لایه عمیقتر میشود...
}
case .failure(let error):
print(error)
}
}
case .failure(let error):
print(error)
}
}
مشکلات این رویکرد واقعاً زیاد بودن:
- جهنم Callback: هر عملیات متوالی یک سطح تودرتو اضافه میکرد و کد رو عملاً غیرقابل خوندن میکرد. صادقانه بگم، دیباگ کردن این کدها یه عذاب واقعی بود.
- مدیریت خطا پراکنده: هر سطح نیاز به مدیریت خطای جداگانه داشت و فراموش کردن یه مورد خیلی آسون بود.
- عدم ایمنی در زمان کامپایل: کامپایلر هیچ تضمینی درباره ایمنی نخها نمیداد. شما میتونستید به راحتی مسابقه داده ایجاد کنید بدون اینکه هشداری دریافت کنید.
- فراموشی برگشت به ترد اصلی: یکی از رایجترین باگها بود که منجر به کرش اپلیکیشن میشد.
- لغو عملیات دشوار: پیادهسازی مکانیزم لغو برای زنجیرهای از عملیات غیرهمزمان واقعاً پیچیده بود.
ورود Swift Concurrency
اپل در سال ۲۰۲۱ با انتشار Swift 5.5 یک مدل کاملاً جدید همزمانی رو معرفی کرد که بر سه ستون اصلی بنا شده بود: async/await، اکتورها (Actors) و همزمانی ساختاریافته (Structured Concurrency).
این مدل صرفاً یک سینتکس زیباتر نبود. برای اولین بار در تاریخ Swift، کامپایلر میتونست صحت کد همزمان رو در زمان کامپایل بررسی کنه.
در ادامه Swift 6.0 این بررسیها رو از حالت هشدار به خطا تبدیل کرد، Swift 6.1 تشخیصها و پیامهای خطا رو بهبود داد و Swift 6.2 با معرفی مفهوم «همزمانی قابلدسترس» (Approachable Concurrency) ارگونومی و سهولت استفاده رو به طرز چشمگیری ارتقا بخشید.
مبانی async/await
خب، بریم سراغ اصل مطلب. پایه و اساس همزمانی مدرن در Swift مکانیزم async/await هست. تابعی که با کلمه کلیدی async علامتگذاری شده، میتونه اجرای خودش رو معلق (suspend) کنه بدون اینکه نخ جاری رو مسدود کنه. این به سیستم اجازه میده از تعداد محدودی نخ برای پردازش تعداد زیادی عملیات غیرهمزمان استفاده کنه.
تعریف و فراخوانی توابع غیرهمزمان
یک تابع غیرهمزمان با کلمه کلیدی async بعد از لیست پارامترها و قبل از نوع بازگشتی تعریف میشه. اگه تابع بتونه خطا پرتاب کنه، ترتیب کلمات کلیدی async throws خواهد بود:
struct User: Codable, Sendable {
let id: Int
let name: String
let email: String
}
enum NetworkError: Error {
case invalidResponse
case decodingFailed
case notFound
}
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.invalidResponse
}
return try JSONDecoder().decode(User.self, from: data)
}
// فراخوانی ساده و خوانا
func loadUserProfile() async {
do {
let user = try await fetchUser(id: 42)
print("کاربر بارگذاری شد: \(user.name)")
} catch {
print("خطا در بارگذاری: \(error)")
}
}
ببینید چقدر تمیزتره! هر فراخوانی await یک نقطه تعلیق (Suspension Point) هست. در این نقطه، اجرای تابع ممکنه معلق بشه و نخ برای انجام کار دیگهای آزاد بشه. وقتی عملیات غیرهمزمان تکمیل شد، اجرای تابع از سر گرفته میشه — احتمالاً روی نخ دیگهای.
تبدیل APIهای قدیمی با Continuation
یکی از سوالات رایجی که خیلی از توسعهدهندهها میپرسن اینه: «کدهای قدیمی مبتنی بر Callback رو چطور به async/await تبدیل کنم؟» جواب: با مکانیزم Continuation:
// API قدیمی مبتنی بر Callback
func downloadImage(url: URL, completion: @escaping (UIImage?) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, _ in
if let data = data {
completion(UIImage(data: data))
} else {
completion(nil)
}
}.resume()
}
// تبدیل به async/await با Continuation
func downloadImage(url: URL) async -> UIImage? {
await withCheckedContinuation { continuation in
downloadImage(url: url) { image in
continuation.resume(returning: image)
}
}
}
// حالا میتوانیم از آن به صورت ساده استفاده کنیم
func updateAvatar() async {
let url = URL(string: "https://example.com/avatar.jpg")!
if let image = await downloadImage(url: url) {
avatarImageView.image = image
}
}
نکته خیلی مهم: هر Continuation باید دقیقاً یک بار resume بشه. فراخوانی بیش از یک بار یا عدم فراخوانی اون منجر به رفتار نامشخص (Undefined Behavior) یا نشت حافظه میشه. این یکی از اون جاهاییه که باید حواستون خیلی جمع باشه.
همزمانی ساختاریافته: Task و TaskGroup
همزمانی ساختاریافته (Structured Concurrency) به نظر من یکی از مهمترین نوآوریهای سیستم همزمانی Swift هست. ایده اصلی خیلی زیباست: عمر هر وظیفه همزمان به محدوده (scope) والد خودش گره خورده باشه — درست مثل متغیرهای محلی که با خروج از محدوده آزاد میشن.
Task: واحد پایه اجرای غیرهمزمان
Task نقطه ورود به دنیای غیرهمزمان از کد همزمان هست. وقتی در یک محیط همزمان (مثل viewDidLoad) هستید و میخواید یک تابع async رو فراخوانی کنید، از Task استفاده میکنید:
class ProfileViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// ایجاد یک Task برای ورود به محیط async
Task {
do {
let user = try await fetchUser(id: 42)
// چون Task ویژگیهای اکتور والد را ارث میبرد،
// اینجا روی MainActor هستیم و میتوانیم UI را بهروز کنیم
nameLabel.text = user.name
emailLabel.text = user.email
} catch {
showError(error)
}
}
}
}
Task.detached: وظایف جدا از والد
Task.detached وظیفهای ایجاد میکنه که هیچ ارتباطی با محیط والد نداره — نه اولویت اون رو ارث میبره و نه ایزولیشن اکتور اون رو:
// این وظیفه روی MainActor اجرا نمیشود
Task.detached(priority: .background) {
let processedData = await heavyComputation(rawData)
// برای بهروزرسانی UI باید صریحاً به MainActor برگردیم
await MainActor.run {
self.updateUI(with: processedData)
}
}
TaskGroup: اجرای موازی چندین وظیفه
وقتی نیاز دارید چندین عملیات رو به صورت موازی اجرا کنید و منتظر تکمیل همهشون بمونید، TaskGroup ابزار مناسب شماست. این یکی از اون قابلیتهاییه که وقتی باهاش آشنا بشید، دیگه نمیتونید بدونش کار کنید:
func fetchAllUserData(userId: Int) async throws -> UserDashboard {
// withThrowingTaskGroup اجازه اجرای موازی وظایف را میدهد
try await withThrowingTaskGroup(of: DashboardComponent.self) { group in
group.addTask {
let profile = try await fetchUserProfile(userId: userId)
return .profile(profile)
}
group.addTask {
let posts = try await fetchUserPosts(userId: userId)
return .posts(posts)
}
group.addTask {
let notifications = try await fetchNotifications(userId: userId)
return .notifications(notifications)
}
var dashboard = UserDashboard()
for try await component in group {
switch component {
case .profile(let profile):
dashboard.profile = profile
case .posts(let posts):
dashboard.posts = posts
case .notifications(let notifications):
dashboard.notifications = notifications
}
}
return dashboard
}
}
enum DashboardComponent: Sendable {
case profile(UserProfile)
case posts([Post])
case notifications([Notification])
}
چرخه حیات TaskGroup تضمین میکنه که تمام وظایف فرزند قبل از خروج از بلاک تکمیل بشن. اگه یکی از وظایف خطا پرتاب کنه، بقیه وظایف به صورت خودکار لغو میشن.
همچنین اگه وظیفه والد لغو بشه، تمام وظایف فرزند هم لغو خواهند شد. این رفتار «همزمانی ساختاریافته» نام داره — وظایف همزمان مانند متغیرهای محلی، عمری محدود و قابل پیشبینی دارن.
async let: سینتکس سادهتر برای موازیسازی
وقتی تعداد وظایف موازی مشخص و محدوده، async let یه سینتکس خیلی سادهتر ارائه میده:
func loadScreenData() async throws -> ScreenData {
// هر سه درخواست همزمان شروع میشوند
async let user = fetchUser(id: 42)
async let posts = fetchLatestPosts()
async let settings = fetchSettings()
// منتظر تکمیل همه آنها میمانیم
return try await ScreenData(
user: user,
posts: posts,
settings: settings
)
}
اکتورها و جلوگیری از مسابقه داده
اکتورها (Actors) یکی از بنیادیترین نوآوریهای سیستم همزمانی Swift هستن. یک اکتور نوع مرجعی (Reference Type) هست که دسترسی به وضعیت داخلی خودش رو سریالی میکنه — یعنی در هر لحظه فقط یک قطعه کد میتونه به حالت قابل تغییر (Mutable State) اکتور دسترسی داشته باشه.
تعریف و استفاده از اکتور
actor BankAccount {
let accountId: String
private(set) var balance: Double
init(accountId: String, initialBalance: Double) {
self.accountId = accountId
self.balance = initialBalance
}
func deposit(amount: Double) {
balance += amount
}
func withdraw(amount: Double) throws -> Double {
guard balance >= amount else {
throw BankError.insufficientFunds
}
balance -= amount
return amount
}
// خصوصیات غیرقابل تغییر (let) نیاز به await ندارند
// اما هر چیز قابل تغییر فقط از داخل اکتور قابل دسترسی است
}
// استفاده از اکتور - هر دسترسی خارجی نیاز به await دارد
let account = BankAccount(accountId: "ACC001", initialBalance: 1000)
let currentBalance = await account.balance // await لازم است
try await account.withdraw(amount: 200) // await لازم است
print(account.accountId) // await لازم نیست (let)
نکته کلیدی اینجاست: کامپایلر تضمین میکنه که هیچ مسابقه دادهای رخ نده. اگه سعی کنید بدون await به وضعیت قابل تغییر اکتور دسترسی پیدا کنید، خطای کامپایل میگیرید. این حفاظت در زمان کامپایله، نه زمان اجرا. و این خیلی مهمه.
کش همزمان با اکتور — یک مثال عملی
بیایید یک مثال واقعی ببینیم. فرض کنید میخواید یک سیستم کش تصویر thread-safe بسازید:
actor ImageCache {
private var cache: [URL: UIImage] = [:]
private var activeDownloads: [URL: Task<UIImage, Error>] = [:]
func image(for url: URL) async throws -> UIImage {
// اگر تصویر در کش هست، برگردان
if let cached = cache[url] {
return cached
}
// اگر دانلود فعالی هست، منتظر آن بمان
if let activeTask = activeDownloads[url] {
return try await activeTask.value
}
// در غیر این صورت، دانلود جدید شروع کن
let task = Task {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
activeDownloads[url] = task
do {
let image = try await task.value
cache[url] = image
activeDownloads[url] = nil
return image
} catch {
activeDownloads[url] = nil
throw error
}
}
func clearCache() {
cache.removeAll()
}
}
مشکل بازگشتپذیری اکتور (Actor Reentrancy)
بازگشتپذیری اکتور (Actor Reentrancy) یکی از ظریفترین مفاهیمیه که باید درباره اکتورها بدونید. خیلی از توسعهدهندهها اول کار اینو نادیده میگیرن و بعداً با باگهای عجیب مواجه میشن.
بازگشتپذیری عمدیه — این طراحی آگاهانهایه برای جلوگیری از بنبست (Deadlock)، اما نیاز به مدیریت دقیق حالت در نقاط await داره.
مشکل چیست؟
وقتی یک متد اکتور در نقطه await معلق میشه، قفل اکتور آزاد میشه و سایر متدهای اون اکتور میتونن اجرا بشن. یعنی وضعیت اکتور ممکنه بین دو نقطه await تغییر کرده باشه:
actor TicketSeller {
var availableTickets: Int = 100
func buyTicket() async throws -> Ticket {
// بررسی موجودی
guard availableTickets > 0 else {
throw TicketError.soldOut
}
// ⚠️ خطر! در این نقطه await، ممکن است متد دیگری
// بقیه بلیطها را بفروشد!
let ticket = try await processPayment()
// این خط ممکن است availableTickets را منفی کند!
availableTickets -= 1
return ticket
}
}
راهحل: بررسی مجدد حالت بعد از await
actor TicketSeller {
var availableTickets: Int = 100
private var reservedTickets: Set<UUID> = []
func buyTicket() async throws -> Ticket {
let reservationId = UUID()
// رزرو بلیط قبل از await
guard availableTickets > reservedTickets.count else {
throw TicketError.soldOut
}
reservedTickets.insert(reservationId)
do {
let ticket = try await processPayment()
// تأیید نهایی بعد از await
reservedTickets.remove(reservationId)
availableTickets -= 1
return ticket
} catch {
// در صورت خطا، رزرو را آزاد کن
reservedTickets.remove(reservationId)
throw error
}
}
}
قانون طلایی: هرگز فرض نکنید که وضعیت اکتور بعد از یک نقطه await همون چیزیه که قبل از اون بود. همیشه فرضیاتتون رو بعد از هر await مجدداً بررسی کنید. اکتورها ایمنی ساختاری رو تضمین میکنن (دو قطعه کد همزمان نمیتونن به وضعیت دسترسی داشته باشن)، اما ایمنی منطقی بر عهده خودتونه.
پروتکل Sendable و ایمنی نوع در مرزهای همزمانی
پروتکل Sendable نشوندهنده نوعهایی هست که میشه اونا رو با امنیت بین دامنههای همزمانی مختلف انتقال داد. وقتی دادهای رو از یک اکتور به اکتور دیگه یا از یک Task به Task دیگه ارسال میکنید، کامپایلر بررسی میکنه که اون نوع Sendable باشه.
نوعهای Sendable خودکار
برخی نوعها به صورت خودکار Sendable هستن:
- Value Types ساده:
Int،String،Bool،Doubleو سایر نوعهای پایه - Struct و Enum: اگه تمام اعضایشون
Sendableباشن - Tupleها: اگه تمام عناصرشون
Sendableباشن - اکتورها: همیشه
Sendableهستن (چون دسترسی بهشون سریالیه)
// این struct به صورت خودکار Sendable است
// چون تمام اعضای آن Sendable هستند
struct UserProfile: Sendable {
let id: Int
let name: String
let email: String
let joinDate: Date
}
// این enum هم Sendable است
enum AppTheme: Sendable {
case light
case dark
case system
}
// ⚠️ این کلاس Sendable نیست چون کلاسها مرجعی هستند
// و حالت قابل تغییر دارند
class UserSession {
var token: String
var isLoggedIn: Bool
// ...
}
// برای Sendable کردن کلاس، باید final و immutable باشد
final class AppConfiguration: Sendable {
let apiBaseURL: URL
let appVersion: String
let environment: Environment
init(apiBaseURL: URL, appVersion: String, environment: Environment) {
self.apiBaseURL = apiBaseURL
self.appVersion = appVersion
self.environment = environment
}
}
@unchecked Sendable: استفاده با احتیاط
گاهی نوعهایی دارید که میدونید ایمن هستن اما کامپایلر نمیتونه تأییدش کنه. در این موارد میتونید از @unchecked Sendable استفاده کنید، اما مسئولیت ایمنی بر عهده خودتونه:
// این کلاس از قفل برای ایمنی نخ استفاده میکند
// اما کامپایلر نمیتواند آن را تشخیص دهد
final class ThreadSafeCache<Key: Hashable, Value>: @unchecked Sendable {
private var storage: [Key: Value] = [:]
private let lock = NSLock()
func get(_ key: Key) -> Value? {
lock.lock()
defer { lock.unlock() }
return storage[key]
}
func set(_ key: Key, value: Value) {
lock.lock()
defer { lock.unlock() }
storage[key] = value
}
}
یه هشدار جدی: از @unchecked Sendable فقط زمانی استفاده کنید که واقعاً میدونید چیکار دارید میکنید. استفاده بیرویه از اون تمام مزایای بررسی ایمنی کامپایلر رو از بین میبره. من شخصاً در پروژههام خیلی محتاطانه ازش استفاده میکنم.
@MainActor و اکتورهای سراسری
@MainActor یک اکتور سراسری (Global Actor) هست که تضمین میکنه کد روی نخ اصلی اجرا بشه. این مفهوم برای توسعه UI فوقالعاده حیاتیه چون تمام بهروزرسانیهای رابط کاربری باید روی نخ اصلی انجام بشن.
استفاده از @MainActor
// علامتگذاری کل کلاس با @MainActor
@MainActor
class ProfileViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var errorMessage: String?
func loadUser() async {
isLoading = true
errorMessage = nil
do {
// این await ممکن است روی نخ دیگری اجرا شود
// اما بعد از آن، به MainActor برمیگردیم
user = try await fetchUser(id: 42)
} catch {
errorMessage = error.localizedDescription
}
isLoading = false // تضمین اجرا روی نخ اصلی
}
}
// میتوانید فقط یک متد را علامتگذاری کنید
class DataProcessor {
func processData(_ data: Data) -> ProcessedResult {
// پردازش سنگین
return heavyProcessing(data)
}
@MainActor
func updateUI(with result: ProcessedResult) {
// تضمین اجرا روی نخ اصلی
resultLabel.text = result.summary
progressView.isHidden = true
}
}
تعریف اکتور سراسری سفارشی
شما میتونید اکتورهای سراسری خودتون رو هم تعریف کنید، هرچند در اکثر موارد @MainActor کافیه:
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
}
// حالا میتوانیم از آن مانند @MainActor استفاده کنیم
@DatabaseActor
class DatabaseManager {
private var connection: SQLiteConnection?
func executeQuery(_ query: String) -> [Row] {
// تضمین سریالی بودن دسترسی به دیتابیس
return connection?.execute(query) ?? []
}
}
ویژگیهای جدید Swift 6.2: همزمانی قابلدسترس
حالا بریم سراغ هیجانانگیزترین بخش. Swift 6.2 که همراه با WWDC 2025 و Xcode 26 معرفی شد، بزرگترین تحول در مدل همزمانی Swift پس از معرفی اولیه async/await رو به همراه آورد.
تیم Swift با انتشار سند چشمانداز «بهبود دسترسیپذیری ایمنی مسابقه داده» در فوریه ۲۰۲۵، مسیر این تغییرات رو مشخص کرده بود. مفهوم کلیدی اینجا «افشای تدریجی» (Progressive Disclosure) هست: توسعهدهنده فقط به اندازهای که واقعاً از همزمانی استفاده میکنه، نیاز به درک مفاهیم اون داره.
ایزولیشن پیشفرض MainActor (SE-0466)
یکی از مهمترین تغییرات Swift 6.2 امکان تنظیم MainActor به عنوان ایزولیشن پیشفرض برای کل ماژوله. این یعنی تمام نوعها و توابع ماژول به صورت ضمنی @MainActor خواهند بود مگه اینکه خلاف اون مشخص بشه.
نکته مهم: در Xcode 26، تمام تارگتهای اپلیکیشن جدید به صورت پیشفرض MainActor رو به عنوان ایزولیشن پیشفرض دارن.
// فعالسازی در Package.swift
.target(
name: "MyApp",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
// با این تنظیم، کد زیر بدون هیچ annotation اضافی کار میکند
// تمام این کلاس به صورت ضمنی @MainActor است
class SettingsViewModel: ObservableObject {
@Published var username = ""
@Published var isDarkMode = false
@Published var notificationsEnabled = true
func saveSettings() async {
let settings = Settings(
username: username,
isDarkMode: isDarkMode,
notificationsEnabled: notificationsEnabled
)
try? await settingsService.save(settings)
}
func resetToDefaults() {
username = ""
isDarkMode = false
notificationsEnabled = true
}
}
در Xcode 26 هم میتونید از طریق تنظیمات Build Settings پروژه و بخش «Default Actor Isolation»، مقدار اون رو به MainActor تغییر بدید.
nonisolated(nonsending) به عنوان رفتار پیشفرض
یکی از گیجکنندهترین رفتارهای قبلی Swift این بود که توابع nonisolated async به صورت خودکار از اکتور فراخوانکننده جدا میشدن و روی executor سراسری (Global Executor) اجرا میشدن. صادقانه بگم، این رفتار منجر به یه عالمه باگ ظریف در بهروزرسانی UI میشد.
در Swift 6.2، رفتار پیشفرض تغییر کرده: توابع nonisolated async حالا روی اکتور فراخوانکننده اجرا میشن. این تغییر با عنوان nonisolated(nonsending) شناخته میشه:
// در Swift 6.2 - رفتار پیشفرض جدید
// این تابع nonisolated است اما روی اکتور فراخوانکننده اجرا میشود
func processUserInput(_ input: String) async -> String {
// اگر از MainActor فراخوانی شود، اینجا هم روی MainActor هستیم
let cleaned = input.trimmingCharacters(in: .whitespacesAndNewlines)
return cleaned.lowercased()
}
@MainActor
class SearchViewModel: ObservableObject {
@Published var results: [SearchResult] = []
func search(query: String) async {
// processUserInput روی MainActor اجرا میشود
// چون از درون MainActor فراخوانی شده
let cleanedQuery = await processUserInput(query)
results = try await searchService.search(cleanedQuery)
}
}
صفت @concurrent: درخواست صریح اجرای موازی
با تغییر رفتار پیشفرض، حالا وقتی میخواید یک تابع واقعاً روی نخ جداگانه اجرا بشه، باید صریحاً اون رو با صفت @concurrent مشخص کنید. این یک تصمیم آگاهانه و عمدیه — و به نظرم تصمیم خیلی خوبیه:
// این تابع صریحاً درخواست اجرای موازی دارد
@concurrent
func compressImage(_ image: UIImage, quality: CGFloat) async -> Data? {
// این کد روی نخ پسزمینه اجرا میشود
// حتی اگر از MainActor فراخوانی شده باشد
return image.jpegData(compressionQuality: quality)
}
@concurrent
func performHeavyComputation(data: [Double]) async -> ComputationResult {
// محاسبات سنگین - باید خارج از MainActor باشد
var result = 0.0
for value in data {
result += complexMathOperation(value)
}
return ComputationResult(value: result)
}
@MainActor
class ImageEditorViewModel: ObservableObject {
@Published var compressedData: Data?
func compress(image: UIImage) async {
// compressImage روی نخ پسزمینه اجرا میشود
// به لطف @concurrent
compressedData = await compressImage(image, quality: 0.8)
// اینجا دوباره روی MainActor هستیم
}
}
مدل سهمرحلهای Swift 6.2 به این صورت خلاصه میشه:
- مرحله ۱ — کد ساده تکنخی: همه چیز روی MainActor اجرا میشه. نیازی به دانش همزمانی نیست.
- مرحله ۲ — کد غیرهمزمان بدون مسابقه داده: از
async/awaitاستفاده کنید. توابع nonisolated روی اکتور فراخوانکننده اجرا میشن. - مرحله ۳ — بهینهسازی با موازیسازی: از
@concurrentبرای انتقال کار سنگین به نخ پسزمینه استفاده کنید.
سازگاریهای ایزولهشده (Isolated Conformances)
Swift 6.2 همچنین مفهوم «سازگاریهای ایزولهشده» رو معرفی کرده که اجازه میده یک نوع فقط در محیط یک اکتور خاص با پروتکل سازگار باشه:
// سازگاری فقط در محیط MainActor
@MainActor
class MyViewController: UIViewController, @MainActor MyCustomProtocol {
func doSomething() {
// این متد فقط روی MainActor قابل فراخوانی است
}
}
protocol MyCustomProtocol {
func doSomething()
}
نوع Observations: دنباله غیرهمزمان مشاهدهای
یکی دیگه از ویژگیهای جالب Swift 6.2، نوع Observations از فریمورک Observation هست که یک AsyncSequence برای مشاهده تغییرات فراهم میکنه:
@Observable
class ShoppingCart {
var items: [CartItem] = []
var totalPrice: Double = 0.0
}
// استفاده از Observations برای مشاهده تغییرات
func observeCartChanges(cart: ShoppingCart) async {
for await changes in Observations(of: cart) {
print("سبد خرید تغییر کرد: \(changes.totalPrice) تومان")
await updateBadgeCount(cart.items.count)
}
}
نکات عملی مهاجرت پروژههای موجود
اگه پروژهای دارید که با Swift قبل از نسخه 6 نوشته شده، مهاجرت به سیستم همزمانی مدرن میتونه چالشبرانگیز باشه. (من خودم این مسیر رو طی کردم و میدونم چقدر میتونه سخت باشه!) اینجا یک برنامه عملی و مرحلهبندی شده ارائه میدم:
مرحله ۱: فعالسازی تدریجی بررسیها
به جای فعالسازی StrictConcurrency به صورت کامل، از حالت targeted شروع کنید:
// در Package.swift
.target(
name: "MyModule",
swiftSettings: [
.swiftLanguageMode(.v5), // فعلاً در حالت Swift 5 بمانید
.enableExperimentalFeature("StrictConcurrency=targeted")
]
)
مرحله ۲: نوعها را Sendable کنید
از مدلهای داده شروع کنید و به تدریج نوعهای بیشتری رو Sendable کنید:
// قبل
struct UserDTO {
let id: Int
let name: String
var avatar: UIImage? // UIImage Sendable نیست!
}
// بعد - جداسازی داده از UI
struct UserDTO: Sendable {
let id: Int
let name: String
let avatarURL: URL? // URL ایمن است
}
// پردازش تصویر جداگانه
@MainActor
func loadAvatar(from url: URL?) async -> UIImage? {
guard let url else { return nil }
return try? await downloadImage(url: url)
}
مرحله ۳: ViewModelها را به @MainActor منتقل کنید
// قبل - ViewModel با DispatchQueue
class ProductListViewModel: ObservableObject {
@Published var products: [Product] = []
func loadProducts() {
URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
guard let data = data,
let products = try? JSONDecoder().decode([Product].self, from: data) else { return }
DispatchQueue.main.async {
self?.products = products
}
}.resume()
}
}
// بعد - ViewModel مدرن با async/await
@MainActor
class ProductListViewModel: ObservableObject {
@Published var products: [Product] = []
@Published var isLoading = false
func loadProducts() async {
isLoading = true
defer { isLoading = false }
do {
let (data, _) = try await URLSession.shared.data(from: url)
products = try JSONDecoder().decode([Product].self, from: data)
} catch {
print("خطا: \(error)")
}
}
}
فرق قبل و بعد رو ببینید. واقعاً روز و شبه!
مرحله ۴: از ویژگیهای Swift 6.2 بهره ببرید
// فعالسازی ایزولیشن پیشفرض MainActor
// در Package.swift یا Build Settings
.target(
name: "MyApp",
swiftSettings: [
.swiftLanguageMode(.v6),
.defaultIsolation(MainActor.self)
]
)
// حالا اکثر @MainActorها را میتوانید حذف کنید!
// این ViewModel به صورت ضمنی @MainActor است
class OrderViewModel: ObservableObject {
@Published var orders: [Order] = []
func loadOrders() async {
orders = (try? await orderService.fetchAll()) ?? []
}
}
// فقط توابع سنگین نیاز به @concurrent دارند
@concurrent
func generateReport(orders: [Order]) async -> Report {
// پردازش سنگین خارج از MainActor
return Report(orders: orders)
}
بهترین شیوهها و اشتباهات رایج
بیایید مهمترین نکات و اشتباهاتی رو مرور کنیم که در کار با همزمانی Swift باید بدونید. اینا رو از تجربه عملی میگم و مطمئنم خیلیهاتون حداقل یکیشون رو تجربه کردید.
اشتباه ۱: استفاده نادرست از Task
// ❌ اشتباه: ایجاد Task بدون ذخیره مرجع
class SearchViewController: UIViewController {
func viewDidLoad() {
super.viewDidLoad()
// اگر ViewController از بین برود، Task ادامه پیدا میکند!
Task {
await loadData()
}
}
}
// ✅ صحیح: ذخیره مرجع Task و لغو آن
class SearchViewController: UIViewController {
private var loadTask: Task<Void, Never>?
func viewDidLoad() {
super.viewDidLoad()
loadTask = Task {
await loadData()
}
}
func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
loadTask?.cancel() // لغو Task هنگام خروج
}
}
اشتباه ۲: نادیده گرفتن لغو Task
// ❌ اشتباه: عدم بررسی لغو
func fetchAllPages() async throws -> [Page] {
var allPages: [Page] = []
var currentPage = 1
while true {
let page = try await fetchPage(currentPage)
if page.items.isEmpty { break }
allPages.append(contentsOf: page.items)
currentPage += 1
}
return allPages
}
// ✅ صحیح: بررسی لغو در هر تکرار
func fetchAllPages() async throws -> [Page] {
var allPages: [Page] = []
var currentPage = 1
while true {
// بررسی لغو قبل از هر درخواست
try Task.checkCancellation()
let page = try await fetchPage(currentPage)
if page.items.isEmpty { break }
allPages.append(contentsOf: page.items)
currentPage += 1
}
return allPages
}
اشتباه ۳: مسدود کردن اکتور با عملیات سنگین
// ❌ اشتباه: عملیات سنگین CPU-bound داخل اکتور
actor DataAnalyzer {
func analyze(data: [DataPoint]) -> AnalysisResult {
// این عملیات سنگین اکتور را مسدود میکند
// و سایر متدها نمیتوانند اجرا شوند
return performHeavyAnalysis(data)
}
}
// ✅ صحیح: انتقال عملیات سنگین به Task جداگانه
actor DataAnalyzer {
func analyze(data: [DataPoint]) async -> AnalysisResult {
// عملیات سنگین را به نخ پسزمینه منتقل کنید
await Task.detached(priority: .userInitiated) {
performHeavyAnalysis(data)
}.value
}
}
اشتباه ۴: فراموش کردن Sendable در TaskGroup
// ❌ خطای کامپایل: نوع بازگشتی باید Sendable باشد
func processImages(_ urls: [URL]) async -> [UIImage] {
await withTaskGroup(of: UIImage.self) { group in
// UIImage ممکن است Sendable نباشد
for url in urls {
group.addTask { try await downloadImage(url) }
}
// ...
}
}
// ✅ صحیح: استفاده از دادههای Sendable
func processImages(_ urls: [URL]) async -> [Data] {
await withTaskGroup(of: (Int, Data?).self) { group in
for (index, url) in urls.enumerated() {
group.addTask {
let data = try? await URLSession.shared.data(from: url).0
return (index, data)
}
}
var results = Array<Data?>(repeating: nil, count: urls.count)
for await (index, data) in group {
results[index] = data
}
return results.compactMap { $0 }
}
}
بهترین شیوهها: خلاصه
- از ساده شروع کنید: در Swift 6.2 از ایزولیشن پیشفرض MainActor استفاده کنید. فقط زمانی
@concurrentاضافه کنید که واقعاً به موازیسازی نیاز دارید. - وضعیت اکتور رو بعد از await بررسی کنید: هرگز فرض نکنید حالت اکتور بین دو نقطه await ثابت مونده.
- Task رو مدیریت و لغو کنید: همیشه مرجع Task رو ذخیره و در زمان مناسب لغو کنید.
- لغو رو بررسی کنید: در حلقهها و عملیات طولانی،
Task.checkCancellation()رو فراموش نکنید. - مدلهای داده رو Sendable کنید: structهای ساده با اعضای immutable بهترین گزینه هستن.
- از @unchecked Sendable پرهیز کنید: مگه اینکه واقعاً دلیل موجهی داشته باشید و مکانیزم ایمنی خودتون رو پیاده کرده باشید.
- عملیات سنگین رو از اکتور خارج کنید: اکتورها برای هماهنگسازی دسترسی هستن، نه برای محاسبات سنگین.
- مهاجرت تدریجی: از حالت
targetedشروع و به تدریج بهcompleteبرید.
جمعبندی
سیستم همزمانی Swift مسیر طولانیای رو طی کرده — از معرفی اولیه async/await در Swift 5.5، تا سختگیریهای Swift 6.0 که بررسیهای همزمانی رو به خطای کامپایل تبدیل کرد، و حالا Swift 6.2 که با فلسفه «همزمانی قابلدسترس» تعادلی عالی بین ایمنی و سهولت استفاده ایجاد کرده.
مهمترین تغییرات Swift 6.2 رو مرور کنیم:
- ایزولیشن پیشفرض MainActor: در تارگتهای اپلیکیشن جدید Xcode 26، تمام کد به صورت پیشفرض روی MainActor اجرا میشه.
- nonisolated(nonsending) پیشفرض: توابع غیرهمزمان nonisolated حالا روی اکتور فراخوانکننده باقی میمونن.
- صفت @concurrent: برای درخواست صریح اجرای موازی استفاده میشه.
- سازگاریهای ایزولهشده: امکان محدود کردن سازگاری پروتکل به یک اکتور خاص.
- نوع Observations: دنباله غیرهمزمان جدید برای مشاهده تغییرات.
توصیه نهاییمون اینه: اگه پروژه جدیدی شروع میکنید، از Swift 6.2 و ایزولیشن پیشفرض MainActor استفاده کنید. اگه پروژه قدیمی دارید، مهاجرت تدریجی رو دنبال کنید و از ابزارهای تشخیصی بهبودیافته Swift 6.1 و 6.2 بهره ببرید.
سیستم همزمانی Swift حالا به نقطهای رسیده که هم برای مبتدیان قابلدسترسه و هم برای حرفهایها قدرت کافی داره. آینده برنامهنویسی همزمان در Swift روشنتر از همیشهست و با درک عمیق این مفاهیم، شما به عنوان یک توسعهدهنده iOS میتونید اپلیکیشنهایی بسازید که هم سریع هستن و هم ایمن — و کامپایلر Swift در هر قدم، پشتیبان شماست.