Питання співбесіди Swift Structured Concurrency: async/await, TaskGroup, Actors
Технічні питання співбесіди Swift Structured Concurrency: async/await, TaskGroup, actors та шаблони конкурентності для iOS 2026

Structured Concurrency, представлена у Swift 5.5, революціонізувала асинхронне програмування на iOS. Рекрутери тепер перевіряють володіння async/await, TaskGroup і actors під час технічних співбесід. Ось основні питання та очікувані відповіді, щоб виділитися на співбесідах.
Рекрутери оцінюють три компетенції: розуміння фундаментальних концепцій (async/await, Task), володіння шаблонами конкурентності (TaskGroup, ізоляція actors) та здатність діагностувати поширені помилки (data races, deadlocks).
Яка різниця між async/await та DispatchQueue?
Очікувана відповідь: async/await забезпечує структуровану конкурентність із читабельним послідовним кодом, тоді як DispatchQueue використовує колбеки і може призвести до так званого "callback hell". Swift автоматично керує потоками з async/await.
// Comparison: async/await vs DispatchQueue
// ❌ Old style: DispatchQueue with callbacks
func fetchUserOld(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
DispatchQueue.global().async {
// Simulated network call
let result = self.performNetworkRequest(id: id)
DispatchQueue.main.async {
completion(result)
}
}
}
// ✅ New style: async/await more readable
func fetchUser(id: Int) async throws -> User {
// Swift runtime handles threads automatically
// No need to manually switch between queues
return try await performNetworkRequest(id: id)
}Ключові моменти: async/await усуває піраміди колбеків, зменшує помилки потоків (не потрібен DispatchQueue.main.async) і дозволяє Swift runtime оптимізувати виконання на доступних ядрах CPU.
Swift runtime використовує оптимізований пул потоків, який уникає надмірного створення потоків. На відміну від DispatchQueue, де кожен .async може створити новий потік, async/await розумно повторно використовує існуючі потоки.
Як керувати кількома асинхронними операціями паралельно?
Очікувана відповідь: Використовувати async let для 2-3 простих завдань або TaskGroup для динамічної кількості паралельних завдань зі збором результатів.
// Parallel async operations strategies
struct DataFetcher {
// Strategy 1: async let for fixed tasks (2-4 operations)
func loadDashboard() async throws -> Dashboard {
// Launch 3 requests in parallel
async let user = fetchUser()
async let posts = fetchPosts()
async let notifications = fetchNotifications()
// Wait for results (parallel, not sequential)
let (userData, postsData, notificationsData) = try await (user, posts, notifications)
return Dashboard(user: userData, posts: postsData, notifications: notificationsData)
}
// Strategy 2: TaskGroup for dynamic number of tasks
func downloadImages(urls: [URL]) async throws -> [UIImage] {
// TaskGroup allows managing N tasks with result collection
try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
// Launch one task per URL
for (index, url) in urls.enumerated() {
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return (index, image) // Return index to preserve order
}
}
// Collect results in order
var images = [UIImage?](repeating: nil, count: urls.count)
for try await (index, image) in group {
images[index] = image
}
return images.compactMap { $0 }
}
}
}Поширена помилка: Використання await послідовно замість async let розпаралелює виклики. let user = await fetchUser(); let posts = await fetchPosts() виконується послідовно (повільно), тоді як async let запускає обидва одночасно.
Що таке actor і навіщо його використовувати?
Очікувана відповідь: Actor — це тип, який захищає свій змінний стан від data races, гарантуючи послідовний доступ. Він замінює ручні блокування (NSLock, DispatchQueue) для забезпечення безпеки конкурентного доступу.
// Actor for thread-safe state management
// ❌ Classic class: data race risk
class UnsafeCache {
private var cache: [String: Data] = [:] // Not thread-safe!
func store(_ data: Data, for key: String) {
cache[key] = data // ⚠️ Race condition with concurrent access
}
}
// ✅ Actor: automatic protection against races
actor SafeCache {
private var cache: [String: Data] = [:]
// Sequential access guaranteed by actor isolation
func store(_ data: Data, for key: String) {
cache[key] = data // ✅ Thread-safe automatically
}
func retrieve(for key: String) -> Data? {
return cache[key] // ✅ Protected read
}
// Internal synchronous method (no await needed)
nonisolated func clearAll() async {
// nonisolated allows calling from any context
await self.clear()
}
private func clear() {
cache.removeAll()
}
}
// Usage: await required to access actor
let cache = SafeCache()
await cache.store(data, for: "user_123") // Await mandatory
let cachedData = await cache.retrieve(for: "user_123")Ключові моменти: Actor гарантує, що тільки один потік звертається до його стану одночасно. Компілятор примусово вимагає використання await для зовнішніх викликів, роблячи потенційні точки призупинення явними.
@MainActor — це глобальний actor для UI-операцій. Позначення класу @MainActor змушує всі його методи виконуватися на головному потоці. Обережно з блокуючими викликами, які можуть заморозити інтерфейс.
Готовий до співбесід з iOS?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Як обробляти помилки в TaskGroup?
Очікувана відповідь: withThrowingTaskGroup поширює першу зустрінуту помилку та автоматично скасовує решту завдань. Щоб зібрати всі помилки, використовуйте Result у TaskGroup.
// Error handling strategies in TaskGroup
struct BatchProcessor {
// Strategy 1: First error propagation (fail-fast)
func processItemsFastFail(items: [Item]) async throws -> [ProcessedItem] {
try await withThrowingTaskGroup(of: ProcessedItem.self) { group in
for item in items {
group.addTask {
// If one task throws, group cancels others
try await self.process(item)
}
}
// Collect results until first error
var results: [ProcessedItem] = []
for try await result in group {
results.append(result)
}
return results
}
// ⚠️ If one task fails, others are cancelled
}
// Strategy 2: Collect all errors (resilience)
func processItemsResilient(items: [Item]) async -> ([ProcessedItem], [Error]) {
await withTaskGroup(of: Result<ProcessedItem, Error>.self) { group in
for item in items {
group.addTask {
// Wrap in Result to capture errors
do {
let result = try await self.process(item)
return .success(result)
} catch {
return .failure(error)
}
}
}
// Separate successes/failures
var successes: [ProcessedItem] = []
var errors: [Error] = []
for await result in group {
switch result {
case .success(let item):
successes.append(item)
case .failure(let error):
errors.append(error)
}
}
return (successes, errors)
}
}
private func process(_ item: Item) async throws -> ProcessedItem {
// Processing with possible error
try await Task.sleep(nanoseconds: 100_000_000)
return ProcessedItem(from: item)
}
}Ключовий момент: withThrowingTaskGroup зупиняється на першій помилці (корисно для атомарних операцій), тоді як withTaskGroup + Result дозволяє продовжувати попри помилки (корисно для пакетної обробки).
Яка різниця між Task, Task.detached та async let?
Очікувана відповідь: Task успадковує батьківський контекст (пріоритет, ізоляція actor), Task.detached створює незалежне завдання без успадкування, а async let створює дочірнє завдання, яке автоматично очікується наприкінці області видимості.
// Understanding Task creation patterns
@MainActor
class ViewModel {
var isLoading = false
// Scenario 1: Task inherits context (@MainActor here)
func loadDataWithTask() {
Task {
// ✅ Inherits @MainActor from parent
// No need for await MainActor.run
self.isLoading = true
let data = try await fetchData()
self.isLoading = false // ✅ Always on MainActor
}
}
// Scenario 2: Task.detached creates independent task
func loadDataDetached() {
Task.detached {
// ⚠️ Does NOT inherit @MainActor
let data = try await self.fetchData()
// ❌ Error: isLoading not directly accessible
// await MainActor.run {
// self.isLoading = false
// }
}
}
// Scenario 3: async let creates structured child task
func loadMultipleData() async throws {
// async let tasks are bound to current scope
async let users = fetchUsers()
async let posts = fetchPosts()
// ⚠️ If leaving function before await, compilation error
let (usersData, postsData) = try await (users, posts)
// async let tasks automatically cancelled
// if exiting scope (e.g., throw before await)
}
private func fetchData() async throws -> Data {
try await URLSession.shared.data(from: URL(string: "https://api.example.com")!).0
}
private func fetchUsers() async throws -> [User] { [] }
private func fetchPosts() async throws -> [Post] { [] }
}Випадки використання:
Task: Операції, прив'язані до поточного контексту (наприклад, оновлення UI з ViewModel)Task.detached: Незалежні фонові завдання (наприклад, логи, аналітика)async let: Паралельні операції з результатами, потрібними в поточній області
Як реалізувати таймаут для асинхронної операції?
Очікувана відповідь: Використовувати Task.sleep у перегонах між основним завданням і завданням таймауту з withThrowingTaskGroup або створити утиліту withTimeout.
// Timeout implementation for async operations
enum TimeoutError: Error {
case timedOut
}
// Generic utility to add timeout
func withTimeout<T>(
seconds: TimeInterval,
operation: @escaping () async throws -> T
) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
// Task 1: main operation
group.addTask {
try await operation()
}
// Task 2: timeout
group.addTask {
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
throw TimeoutError.timedOut
}
// First task to finish wins
guard let result = try await group.next() else {
throw TimeoutError.timedOut
}
// Cancel losing task (important for cleanup)
group.cancelAll()
return result
}
}
// Usage example
struct NetworkService {
func fetchUserWithTimeout(id: Int) async throws -> User {
// 5-second timeout on network call
try await withTimeout(seconds: 5) {
try await self.fetchUser(id: id)
}
}
private func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
}Сучасна альтернатива: Починаючи з iOS 16, використовуйте URLSession з timeoutInterval, налаштованим через URLSessionConfiguration спеціально для HTTP-викликів.
group.cancelAll() критично важливе для звільнення ресурсів. Без нього програшне завдання продовжуватиметься у фоні до природного завершення, марно витрачаючи CPU та пам'ять.
Як безпечно ділити змінний стан між кількома завданнями?
Очікувана відповідь: Використовувати actor для спільного стану або AsyncStream для комунікації між завданнями через потік значень.
// Safe state sharing between concurrent tasks
// Approach 1: Actor for shared state with sequential access
actor DownloadManager {
private var activeDownloads: [String: Task<Data, Error>] = [:]
private var cache: [String: Data] = [:]
// Start download or return existing task
func download(url: String) async throws -> Data {
// Check cache first
if let cachedData = cache[url] {
return cachedData
}
// Check if download already in progress
if let existingTask = activeDownloads[url] {
return try await existingTask.value
}
// Create new download task
let task = Task<Data, Error> {
let data = try await self.performDownload(url: url)
// Update cache (thread-safe via actor)
await self.completeDownload(url: url, data: data)
return data
}
activeDownloads[url] = task
return try await task.value
}
private func performDownload(url: String) async throws -> Data {
let urlObject = URL(string: url)!
let (data, _) = try await URLSession.shared.data(from: urlObject)
return data
}
private func completeDownload(url: String, data: Data) {
cache[url] = data
activeDownloads.removeValue(forKey: url)
}
}
// Approach 2: AsyncStream for inter-task communication
struct EventStream {
private let continuation: AsyncStream<Event>.Continuation
let stream: AsyncStream<Event>
init() {
var continuation: AsyncStream<Event>.Continuation!
stream = AsyncStream { cont in
continuation = cont
}
self.continuation = continuation
}
func emit(_ event: Event) {
continuation.yield(event)
}
func finish() {
continuation.finish()
}
}
// Example: shared progress monitoring
func processItemsWithProgress(items: [Item]) async {
let eventStream = EventStream()
// Task 1: Process items
Task {
for item in items {
await processItem(item)
eventStream.emit(.itemProcessed(item.id))
}
eventStream.finish()
}
// Task 2: Update UI with progress
Task { @MainActor in
for await event in eventStream.stream {
switch event {
case .itemProcessed(let id):
print("Item \(id) processed")
}
}
}
}
enum Event {
case itemProcessed(String)
}Архітектурний вибір: Actor для централізованого стану з бізнес-логікою, AsyncStream для комунікації, керованої подіями, між роз'єднаними компонентами.
Готовий до співбесід з iOS?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Що таке скасування Task і як його обробляти?
Очікувана відповідь: Скасування Task дозволяє скасувати асинхронні операції, що тривають. Завдання повинні періодично перевіряти Task.isCancelled або використовувати Task.checkCancellation(), який кидає помилку.
// Implementing proper task cancellation
struct ImageProcessor {
// Cancellable processing with explicit checks
func processImages(_ images: [UIImage]) async throws -> [ProcessedImage] {
var results: [ProcessedImage] = []
for (index, image) in images.enumerated() {
// Check 1: Boolean check (continue or skip)
if Task.isCancelled {
print("Cancelled after \(index) images")
break // Graceful stop
}
let processed = try await processImage(image)
results.append(processed)
// Check 2: Automatic throw if cancelled
try Task.checkCancellation()
}
return results
}
private func processImage(_ image: UIImage) async throws -> ProcessedImage {
// Simulate long processing
for _ in 0..<10 {
try await Task.sleep(nanoseconds: 100_000_000)
// ✅ Check cancellation in long loops
try Task.checkCancellation()
}
return ProcessedImage(from: image)
}
}
// SwiftUI: Automatic cancellation when view disappears
struct ImageGalleryView: View {
@State private var images: [ProcessedImage] = []
var body: some View {
ScrollView {
// Display images
}
.task {
// ✅ Task cancelled automatically when view disappears
let processor = ImageProcessor()
do {
images = try await processor.processImages(sourceImages)
} catch is CancellationError {
print("Processing cancelled")
}
}
}
}
// Manual cancellation of stored task
class DownloadViewModel {
private var downloadTask: Task<Void, Never>?
func startDownload() {
downloadTask = Task {
do {
try await performLongDownload()
} catch is CancellationError {
print("Download cancelled by user")
}
}
}
func cancelDownload() {
// Explicit cancellation of stored task
downloadTask?.cancel()
downloadTask = nil
}
private func performLongDownload() async throws {
try Task.checkCancellation()
// Download logic
}
}Ключові моменти:
Task.isCancelled: неблокуюча перевірка (повертає bool)Task.checkCancellation(): кидаєCancellationError, якщо скасовано- Модифікатор SwiftUI
.task { }: автоматичне скасування при зникненні view
Swift використовує модель кооперативного скасування: завдання не примусово припиняються. Код повинен активно перевіряти Task.isCancelled або checkCancellation(), щоб реагувати на скасування. Без цих перевірок завдання продовжуватиметься нескінченно.
Як правильно використовувати MainActor у SwiftUI-додатку?
Очікувана відповідь: Анотувати ViewModels з @MainActor, щоб гарантувати, що всі оновлення стану UI відбуваються на головному потоці. Використовувати @MainActor на окремих функціях, якщо лише певні операції стосуються UI.
// Proper MainActor usage in SwiftUI architecture
// Pattern 1: Entire ViewModel @MainActor
@MainActor
class UserViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
@Published var errorMessage: String?
private let repository: UserRepository
init(repository: UserRepository) {
self.repository = repository
}
// ✅ All methods implicitly @MainActor
func loadUser(id: Int) async {
isLoading = true // No need for await or MainActor.run
errorMessage = nil
do {
// Network call done on background thread by runtime
user = try await repository.fetchUser(id: id)
} catch {
errorMessage = error.localizedDescription
}
isLoading = false // Always on MainActor
}
// Synchronous method also on MainActor
func clearUser() {
user = nil
errorMessage = nil
}
}
// Pattern 2: Selective methods with @MainActor
class DataSyncService {
// ❌ Not @MainActor on class (no UI here)
func syncData() async throws {
// Background processing
let data = try await fetchRemoteData()
let processed = processData(data)
// ✅ Switch to MainActor only for UI
await updateUI(with: processed)
}
@MainActor
private func updateUI(with data: ProcessedData) {
// Update observable property
NotificationCenter.default.post(
name: .dataDidSync,
object: data
)
}
// Background work (not @MainActor)
private func fetchRemoteData() async throws -> Data {
// Network call
Data()
}
private func processData(_ data: Data) -> ProcessedData {
// CPU-intensive processing in background
ProcessedData()
}
}
// Pattern 3: Closure annotation
class ImageLoader {
func loadImage(url: URL, completion: @MainActor @escaping (UIImage?) -> Void) async {
let image = try? await downloadImage(from: url)
// ✅ Completion guaranteed on MainActor
await completion(image)
}
private func downloadImage(from url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
return UIImage(data: data) ?? UIImage()
}
}Поширена помилка: Позначення всього класу як @MainActor, коли лише певні методи стосуються UI. Це змушує весь код працювати на головному потоці, включаючи важкі операції, які повинні бути у фоні.
Як обробляти data races за допомогою Sendable?
Очікувана відповідь: Протокол Sendable гарантує, що тип може бути спільно використаний між завданнями без ризику data race. Типи-значення (struct, enum) автоматично є Sendable, класи повинні бути final з незмінними або захищеними властивостями.
// Making types safe for concurrent access
// ✅ Struct: automatically Sendable (value type)
struct UserData: Sendable {
let id: Int
let name: String
let email: String
}
// ✅ Enum: automatically Sendable
enum LoadingState: Sendable {
case idle
case loading
case loaded(UserData)
case failed(Error) // ⚠️ Error must also be Sendable
}
// ❌ Class with mutable state: not Sendable by default
class UnsafeCounter {
var count = 0 // Mutable, unprotected
func increment() {
count += 1 // Data race possible
}
}
// ✅ Immutable class: explicit Sendable
final class SafeConfig: @unchecked Sendable {
let apiKey: String
let timeout: TimeInterval
init(apiKey: String, timeout: TimeInterval) {
self.apiKey = apiKey
self.timeout = timeout
}
}
// ✅ Class with actor-protected state
actor SafeCounter: Sendable {
private var count = 0 // Protected by actor isolation
func increment() {
count += 1 // Thread-safe automatically
}
func getValue() -> Int {
return count
}
}
// ✅ Class with manually protected state
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
func store(_ data: Data, for key: String) {
lock.lock()
defer { lock.unlock() }
storage[key] = data
}
func retrieve(for key: String) -> Data? {
lock.lock()
defer { lock.unlock() }
return storage[key]
}
}
// Usage: compiler checks Sendable
func processInBackground(data: UserData) { // ✅ UserData is Sendable
Task.detached {
// No warning: UserData is Sendable value type
print("Processing user: \(data.name)")
}
}
func processUnsafe(counter: UnsafeCounter) {
Task.detached {
// ⚠️ Warning: UnsafeCounter is not Sendable
// counter.increment()
}
}Правила Sendable:
- Struct/Enum з властивостями Sendable: автоматично Sendable
- Класи: повинні бути
final+ незмінні або використовувати@unchecked Sendableз ручним захистом (locks, actors) - Замикання: автоматично Sendable, якщо захоплюють лише типи Sendable
@unchecked Sendable вимикає перевірки компілятора. Використовувати лише якщо thread-safety гарантовано вручну (locks, послідовні черги). Відповідальність розробника — уникати data races.
Готовий до співбесід з iOS?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Висновок
Володіння Swift Structured Concurrency стало необхідним для iOS-співбесід у 2026 році. Рекрутери оцінюють три рівні: розуміння концепцій (async/await проти колбеків), володіння шаблонами (TaskGroup, ізоляція actor) та налагодження (скасування, Sendable).
Контрольний список підготовки:
- ✅ Пояснити async/await проти DispatchQueue на конкретному прикладі
- ✅ Продемонструвати використання TaskGroup для паралельних операцій
- ✅ Реалізувати thread-safe actor для захисту змінного стану
- ✅ Обробляти помилки в конкурентному контексті (Result, throwing)
- ✅ Розрізняти Task, Task.detached і async let з випадками використання
- ✅ Реалізувати таймаут для асинхронної операції
- ✅ Правильно використовувати MainActor в архітектурі SwiftUI
- ✅ Розуміти Sendable і уникати data races
Найкращі кандидати поєднують теорію та практику: пояснюють "чому" (уникнути data races, покращити читабельність) та "як" (функціональний код з обробкою помилок). Практика на реальних проєктах закріплює ці шаблони.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
Пов'язані статті

Combine vs async/await у Swift: Шаблони Прогресивної Міграції
Повний посібник з міграції з Combine на async/await у Swift: прогресивні стратегії, шаблони мостування та співіснування парадигм у iOS-кодових базах.

Співбесіда StoreKit 2: Управління Підписками та Валідація Чеків
Опануйте питання співбесіди iOS щодо StoreKit 2, управління підписками, валідації чеків та реалізації покупок у застосунку з практичними прикладами коду на Swift.

Swift Testing Framework Співбесіда 2026: Макроси #expect та #require проти XCTest
Опанування нового Swift Testing Framework для iOS-співбесід: макроси #expect і #require, міграція з XCTest, складні шаблони та поширені помилки.