1. async/await
1. async/await
Teori: Masalah Callback Hell dan Cooperative Multitasking
Sebelum async/await, concurrency di Swift (dan platform Apple) dibangun di atas completion handlers — closure yang dipanggil ketika operasi async selesai. Ini menciptakan beberapa masalah fundamental:
1. Callback Hell — Nesting yang Tidak Terkendali
// Completion handler: setiap level async = satu level nesting
func loadUserProfile(userID: String) {
fetchUser(userID: userID) { user in
guard let user = user else { return }
fetchAvatar(url: user.avatarURL) { image in
guard let image = image else { return }
fetchPosts(userID: user.id) { posts in
guard let posts = posts else { return }
// Logika sesungguhnya berada di kedalaman 3 closure
// Error handling di setiap level
// Sulit dibaca, sulit di-maintain
DispatchQueue.main.async {
self.display(user: user, image: image, posts: posts)
}
}
}
}
}
2. Error Handling yang Tersebar
// Setiap callback harus handle error sendiri — tidak ada propagation
fetchUser(id: id) { result in
switch result {
case .failure(let error):
// handle error 1
return
case .success(let user):
fetchAvatar(url: user.avatarURL) { result in
switch result {
case .failure(let error):
// handle error 2 — duplikasi
return
case .success(let image):
// ... terus
}
}
}
}
3. Kehilangan Thread Context
// Callback bisa dipanggil di thread mana saja
// Tidak ada jaminan thread-safety
fetchData { result in
// Thread apa ini? Main? Background? URLSession queue?
self.updateUI(result) // UNSAFE jika bukan main thread
}
Mengapa async/await Lebih Baik
async/await bukan hanya syntactic sugar. Di baliknya, Swift menggunakan cooperative multitasking — model yang berbeda fundamental dari thread-based concurrency:
Thread-based (GCD): Cooperative (async/await):
───────────────────── ─────────────────────────────
Thread 1: Task A runs Thread 1: Task A runs
Thread 1: Task A blocks Task A hits 'await' → SUSPENDS
(thread idle, wasted) Thread 1: picks up Task B
Thread 2: needed for Task A resumes when ready
continuation Thread 1: Task A continues
(context switch overhead) (tidak ada wasted threads)
Ketika kamu menulis await, fungsi di-suspend — bukan di-block. Thread yang digunakan dibebaskan untuk mengerjakan task lain. Ini memungkinkan ribuan concurrent operations dengan hanya beberapa thread (sesuai jumlah CPU core).
Sintaks dan Cara Kerja
// Setelah: linear, mudah dibaca
func loadUserProfile(userID: String) async throws -> UserProfile {
let user = try await fetchUser(userID: userID) // suspend 1
let image = try await fetchAvatar(url: user.avatarURL) // suspend 2
let posts = try await fetchPosts(userID: user.id) // suspend 3
return UserProfile(user: user, image: image, posts: posts)
}
// Error propagates dengan throws — tidak perlu handle di setiap level
// Thread safety dijamin oleh Swift runtime (kembali ke caller's executor)
Suspension Point — Apa yang Terjadi di Balik await:
1. Kode sebelum await berjalan
2. await: fungsi di-suspend, eksekusi dikembalikan ke Swift runtime
3. Runtime: pilih task lain yang siap berjalan
4. Ketika I/O selesai: runtime jadwalkan ulang suspended function
5. Kode setelah await berjalan (mungkin di thread berbeda, tapi isolation terjaga)
Memanggil async dari Synchronous Context
// async function tidak bisa dipanggil langsung dari sync — harus dalam Task
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Task {} adalah "jembatan" dari sync ke async
Task {
do {
let profile = try await loadUserProfile(userID: "123")
await MainActor.run { updateUI(with: profile) }
} catch {
await MainActor.run { showError(error) }
}
}
}
}
Kapan Menggunakan async/await
Gunakan ketika:
- Operasi yang melibatkan I/O: network, disk, database
- Operasi yang membutuhkan waktu: compute-intensive background tasks
- Rangkaian operasi yang saling bergantung (sequential async)
- Mengganti completion handler yang sudah ada
Jangan gunakan ketika:
- Operasi synchronous yang cepat (< 1ms) — overhead task tidak sebanding
- Hot path / tight loop numerik — gunakan synchronous code biasa
- Kode yang harus berjalan di context tanpa Swift runtime (misalnya C interop layer)
Real Use Case: Network Request Chain
// Layanan authentication dengan token refresh otomatis
actor AuthenticatedAPIClient {
private var accessToken: String?
private var refreshToken: String?
private let baseURL: URL
init(baseURL: URL) { self.baseURL = baseURL }
func authenticatedRequest<T: Decodable>(
endpoint: String,
type: T.Type
) async throws -> T {
// 1. Pastikan punya token valid
let token = try await ensureValidToken()
// 2. Buat request
var request = URLRequest(url: baseURL.appendingPathComponent(endpoint))
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
// 3. Kirim request
let (data, response) = try await URLSession.shared.data(for: request)
// 4. Handle 401 — refresh token dan coba lagi
if let http = response as? HTTPURLResponse, http.statusCode == 401 {
try await refreshAccessToken()
return try await authenticatedRequest(endpoint: endpoint, type: type)
}
return try JSONDecoder().decode(T.self, from: data)
}
private func ensureValidToken() async throws -> String {
if let token = accessToken { return token }
try await refreshAccessToken()
return accessToken!
}
private func refreshAccessToken() async throws {
guard let refresh = refreshToken else { throw AuthError.notLoggedIn }
var request = URLRequest(url: baseURL.appendingPathComponent("/auth/refresh"))
request.httpMethod = "POST"
request.httpBody = try JSONEncoder().encode(["refresh_token": refresh])
let (data, _) = try await URLSession.shared.data(for: request)
let tokens = try JSONDecoder().decode(TokenResponse.self, from: data)
accessToken = tokens.accessToken
refreshToken = tokens.refreshToken
}
}
struct TokenResponse: Decodable { let accessToken, refreshToken: String }
enum AuthError: Error { case notLoggedIn }