Pytania rekrutacyjne Swift Structured Concurrency: async/await, TaskGroup, Actors
Techniczne pytania rekrutacyjne Swift Structured Concurrency: async/await, TaskGroup, actors i wzorce współbieżności dla iOS 2026

Structured Concurrency wprowadzona w Swift 5.5 zrewolucjonizowała programowanie asynchroniczne na iOS. Rekruterzy testują obecnie biegłość w async/await, TaskGroup i actors podczas rozmów technicznych. Oto kluczowe pytania i oczekiwane odpowiedzi pozwalające wyróżnić się na rozmowach kwalifikacyjnych.
Rekruterzy oceniają trzy kompetencje: rozumienie podstawowych koncepcji (async/await, Task), opanowanie wzorców współbieżności (TaskGroup, izolacja actorów) oraz umiejętność diagnozowania częstych błędów (data races, deadlocki).
Jaka jest różnica między async/await a DispatchQueue?
Oczekiwana odpowiedź: async/await zapewnia structured concurrency z czytelnym kodem sekwencyjnym, podczas gdy DispatchQueue używa callbacków i może prowadzić do tzw. "callback hell". Swift automatycznie zarządza wątkami przy 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)
}Kluczowe punkty: async/await eliminuje piramidy callbacków, redukuje błędy wątków (brak potrzeby DispatchQueue.main.async) i pozwala runtime'owi Swift optymalizować wykonanie na dostępnych rdzeniach CPU.
Runtime Swift wykorzystuje zoptymalizowaną pulę wątków, która unika nadmiernego tworzenia wątków. W przeciwieństwie do DispatchQueue, gdzie każde .async może utworzyć nowy wątek, async/await inteligentnie wykorzystuje istniejące wątki.
Jak zarządzać wieloma operacjami asynchronicznymi równolegle?
Oczekiwana odpowiedź: Używać async let dla 2-3 prostych zadań lub TaskGroup dla dynamicznej liczby równoległych zadań ze zbieraniem wyników.
// 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 }
}
}
}Częsty błąd: Używanie await sekwencyjnie zamiast async let zrównoleglającego wywołania. let user = await fetchUser(); let posts = await fetchPosts() wykonuje się sekwencyjnie (wolno), podczas gdy async let uruchamia oba jednocześnie.
Czym jest actor i dlaczego go używać?
Oczekiwana odpowiedź: Actor to typ chroniący swój stan mutowalny przed data races, gwarantując dostęp sekwencyjny. Zastępuje ręczne locki (NSLock, DispatchQueue) w celu zabezpieczenia dostępu współbieżnego.
// 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")Kluczowe punkty: Actor gwarantuje, że tylko jeden wątek uzyskuje dostęp do jego stanu w danym momencie. Kompilator wymusza użycie await dla wywołań zewnętrznych, czyniąc potencjalne punkty zawieszenia jawnymi.
@MainActor to globalny actor dla operacji UI. Oznaczenie klasy @MainActor zmusza wszystkie jej metody do wykonywania na głównym wątku. Uwaga na blokujące wywołania, które mogą zamrozić interfejs.
Gotowy na rozmowy o iOS?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Jak obsługiwać błędy w TaskGroup?
Oczekiwana odpowiedź: withThrowingTaskGroup propaguje pierwszy napotkany błąd i automatycznie anuluje pozostałe zadania. Aby zebrać wszystkie błędy, używać Result w 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)
}
}Kluczowy punkt: withThrowingTaskGroup zatrzymuje się przy pierwszym błędzie (przydatne dla operacji atomowych), natomiast withTaskGroup + Result pozwala kontynuować mimo błędów (przydatne dla przetwarzania wsadowego).
Jaka jest różnica między Task, Task.detached i async let?
Oczekiwana odpowiedź: Task dziedziczy kontekst rodzica (priorytet, izolacja actorów), Task.detached tworzy niezależne zadanie bez dziedziczenia, a async let tworzy zadanie potomne automatycznie oczekiwane na końcu zakresu.
// 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] { [] }
}Przypadki użycia:
Task: Operacje powiązane z bieżącym kontekstem (np. aktualizacja UI z ViewModel)Task.detached: Niezależne zadania w tle (np. logi, analytics)async let: Operacje równoległe z wynikami potrzebnymi w bieżącym zakresie
Jak zaimplementować timeout dla operacji asynchronicznej?
Oczekiwana odpowiedź: Używać Task.sleep w wyścigu między głównym zadaniem a zadaniem timeoutu z withThrowingTaskGroup, lub utworzyć narzędzie 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)
}
}Nowoczesna alternatywa: Od iOS 16 można używać URLSession z timeoutInterval skonfigurowanym przez URLSessionConfiguration specjalnie dla wywołań HTTP.
group.cancelAll() jest kluczowe dla zwolnienia zasobów. Bez tego przegrywające zadanie kontynuowałoby pracę w tle aż do naturalnego zakończenia, marnując CPU i pamięć.
Jak bezpiecznie współdzielić stan mutowalny między wieloma zadaniami?
Oczekiwana odpowiedź: Użyć actor dla współdzielonego stanu lub AsyncStream do komunikacji między zadaniami przez strumień wartości.
// 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)
}Wybór architektoniczny: Actor dla scentralizowanego stanu z logiką biznesową, AsyncStream dla komunikacji opartej na zdarzeniach między rozłączonymi komponentami.
Gotowy na rozmowy o iOS?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Czym jest anulowanie Task i jak je obsługiwać?
Oczekiwana odpowiedź: Anulowanie Task pozwala anulować trwające operacje asynchroniczne. Zadania muszą okresowo sprawdzać Task.isCancelled lub używać Task.checkCancellation(), które rzuca błąd.
// 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
}
}Kluczowe punkty:
Task.isCancelled: nieblokujące sprawdzenie (zwraca bool)Task.checkCancellation(): rzucaCancellationError, jeśli anulowane- Modyfikator SwiftUI
.task { }: automatyczne anulowanie przy zniknięciu widoku
Swift wykorzystuje model anulowania kooperacyjnego: zadania nie są wymuszanie zabijane. Kod musi aktywnie sprawdzać Task.isCancelled lub checkCancellation(), aby reagować na anulowanie. Bez tych sprawdzeń zadanie kontynuuje pracę w nieskończoność.
Jak prawidłowo używać MainActor w aplikacji SwiftUI?
Oczekiwana odpowiedź: Adnotować ViewModele atrybutem @MainActor, aby zagwarantować, że wszystkie aktualizacje stanu UI odbywają się na głównym wątku. Używać @MainActor na pojedynczych funkcjach, jeśli tylko niektóre operacje dotyczą 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()
}
}Częsty błąd: Oznaczanie całej klasy jako @MainActor, gdy tylko niektóre metody dotyczą UI. Zmusza to cały kod do działania na głównym wątku, w tym ciężkie operacje, które powinny być w tle.
Jak obsługiwać data races za pomocą Sendable?
Oczekiwana odpowiedź: Protokół Sendable gwarantuje, że typ może być współdzielony między zadaniami bez ryzyka data race. Typy wartościowe (struct, enum) są automatycznie Sendable, klasy muszą być final z niezmiennymi lub chronionymi właściwościami.
// 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()
}
}Reguły Sendable:
- Struct/Enum z właściwościami Sendable: automatycznie Sendable
- Klasy: muszą być
final+ niezmienne, lub używać@unchecked Sendablez ręczną ochroną (locki, actory) - Domknięcia: automatycznie Sendable, jeśli przechwytują tylko typy Sendable
@unchecked Sendable wyłącza kontrole kompilatora. Używać tylko jeśli thread safety jest zagwarantowane ręcznie (locki, kolejki szeregowe). Odpowiedzialność programisty za unikanie data races.
Gotowy na rozmowy o iOS?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Podsumowanie
Opanowanie Swift Structured Concurrency stało się niezbędne dla rozmów rekrutacyjnych iOS w 2026 roku. Rekruterzy oceniają trzy poziomy: rozumienie koncepcji (async/await vs callbacki), opanowanie wzorców (TaskGroup, izolacja actorów) i debugowanie (anulowanie, Sendable).
Lista kontrolna przygotowania:
- ✅ Wyjaśnić async/await vs DispatchQueue na konkretnym przykładzie
- ✅ Zademonstrować użycie TaskGroup do operacji równoległych
- ✅ Zaimplementować thread-safe actor do ochrony stanu mutowalnego
- ✅ Obsłużyć błędy w kontekście współbieżnym (Result, throwing)
- ✅ Rozróżnić Task, Task.detached i async let z przypadkami użycia
- ✅ Zaimplementować timeout dla operacji asynchronicznej
- ✅ Prawidłowo używać MainActor w architekturze SwiftUI
- ✅ Zrozumieć Sendable i unikać data races
Najlepsi kandydaci łączą teorię z praktyką: wyjaśniają "dlaczego" (unikanie data races, poprawa czytelności) i "jak" (kod funkcjonalny z obsługą błędów). Praktyka na realnych projektach utrwala te wzorce.
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Tagi
Udostępnij
Powiązane artykuły

Combine vs async/await w Swift: Wzorce Progresywnej Migracji
Kompletny przewodnik po migracji z Combine do async/await w Swift: progresywne strategie, wzorce mostkowania i współistnienie paradygmatów w bazach kodu iOS.

Rozmowa Kwalifikacyjna StoreKit 2: Zarządzanie Subskrypcjami i Walidacja Paragonów
Opanuj pytania na rozmowy iOS dotyczące StoreKit 2, zarządzania subskrypcjami, walidacji paragonów i implementacji zakupów w aplikacji z praktycznymi przykładami kodu Swift.

Swift Testing Framework Rozmowa kwalifikacyjna 2026: Makra #expect i #require vs XCTest
Opanuj nowy Swift Testing Framework na rozmowy iOS: makra #expect i #require, migracja z XCTest, zaawansowane wzorce i typowe pułapki.