Seri Konkurensi Swift 04 | Batasan penggunaan tugas
Tugas bukanlah pintu masuk universal ke kode asinkron. Yang terpenting adalah siapa yang menciptakannya, siapa yang membatalkannya, dan siapa yang bertanggung jawab atas hasilnya.
Kebiasaan buruk paling umum yang dikembangkan banyak tim saat pertama kali menemukan Swift Concurrency dalam skala besar adalah menyalahgunakan Task.
Karena itu sangat nyaman.
Anda tidak bisa langsung await di tombol callback, lalu menulis Task. Anda tidak bisa langsung await dalam metode proxy UIKit, lalu menulis Task. Jika Anda ingin menyelinap ke dunia asinkron dengan metode sinkronisasi tertentu, cara termudah adalah dengan menulis Task.
Seiring waktu, Task akan berubah dari “gerbang yang menjembatani sinkronisasi dan asinkron” menjadi “lapisan pita konkurensi yang mencakup masalah apa pun”.
Apa yang sebenarnya ingin dijawab oleh artikel ini adalah tiga pertanyaan yang lebih dekat dengan teknik:
- Jenis masalah apa yang dipecahkannya?
- Dalam keadaan apa itu seleksi alam, dan dalam keadaan apa hanya menyembunyikan masalah struktural.
- Pertanyaan apa yang harus Anda tanyakan pada diri sendiri terlebih dahulu ketika memutuskan untuk membuka
Task.
1. Pertama, mari kita perjelas posisinya: Task adalah titik pembuatan tugas asinkron, bukan alat desain proses.
Ketika saya pertama kali melihat Task {}, yang saya pikirkan adalah “eksekusi sepotong kode yang tidak sinkron”.
Pemahaman ini tidak salah, namun tidak cukup.
Lebih tepatnya, apa yang dilakukan Task adalah:
- Buat tugas bersamaan baru
- Masukkan sepotong kode ke dalam konteks asinkron
- Ikat tanggung jawab seperti pelaksanaan, pembatalan, prioritas, hasil, dll. ke tugas ini
Jadi Task tidak hanya tentang “melemparkan kode ke latar belakang dan menjalankannya”.
Setelah saya menuliskannya, saya sebenarnya membuat beberapa keputusan sekaligus:
- Karya ini sekarang mulai berdiri sendiri
- Ini mungkin berakhir lebih lambat dari titik panggilan saat ini
- Ini mungkin atau mungkin tidak dibatalkan
- Hasilnya dikonsumsi atau dibuang
- Ini membangun hubungan tertentu dengan objek saat ini, halaman saat ini, dan tindakan pengguna saat ini
Hal ini juga menunjukkan bahwa Task tidak dapat dipahami hanya secara gramatikal saja.
Secara sintaksis ini hanyalah sebuah blok kode, tetapi secara teknis ini berarti “siklus hidup tugas dibuat”.
2. Skenario yang paling sesuai untuk Task: sangat penting untuk berpindah dari dunia sinkron ke dunia asinkron
Skenario penggunaan Task yang paling alami sebenarnya sangat sederhana:
Konteks saat ini bukan
async, namun proses asinkron perlu dipicu.
Seperti situasi seperti ini.
1. Panggilan balik interaksi pengguna
Button("保存") {
Task {
await viewModel.save()
}
}
Masuk akal untuk menggunakan Task di sini, karena tindakan Button itu sendiri bukan async, tetapi tindakan penyimpanan jelas merupakan proses asinkron.
2. Metode proxy sinkronisasi UIKit/AppKit
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
Task {
await viewModel.search(keyword: searchText)
}
}
Tanda tangan panggilan balik proksi ditentukan oleh kerangka kerja, bukan apakah dapat diubah ke async. Untuk memasuki proses asinkron, diperlukan titik penghubung.
3. Siklus hidup aplikasi atau panggilan balik notifikasi
NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { _ in
Task {
await refreshIfNeeded()
}
}
Nilai Task di sini masih sama: mengubah peristiwa sinkron menjadi tugas asinkron.
Jika Anda melihat contoh-contoh ini bersama-sama, Anda akan menemukan satu kesamaan:
- Acara berasal dari API sinkronisasi
- Pemrosesan bisnis diharapkan tidak sinkron
Taskhanyalah pintu masuk, bukan bodi utama
Saat ini Task adalah alat yang bagus.
3. Bahaya sebenarnya: Perlakukan Task sebagai metode perbaikan “perbaiki kesalahan apa pun yang dilaporkan”
Masalah paling umum dalam tim adalah “menggunakannya terlalu alami”.
Postur tubuh salah yang paling umum adalah sebagai berikut.
1. Jika kompiler tidak mengizinkan await, kompiler akan menyertakan lapisan Task
func refresh() {
Task {
await loadData()
}
}
Kode ini mungkin tidak salah jika dilihat secara terpisah. Masalahnya adalah dalam banyak kasus, refresh() itu sendiri dapat dirancang sebagai async, dan kemudian lapisan atas memutuskan kapan harus memanggilnya.
Setelah pemikiran default menjadi “tidak bisa await, lalu buka Task”, Anda akan kehilangan kendali atas batasan tugas.
2. Sudah ada di fungsi async, kita perlu menambahkan Task lagi
func loadPage() async {
Task {
await loadUser()
}
Task {
await loadFeed()
}
}
Masalah dengan kode jenis ini adalah ia memecah aliran kontrol yang awalnya dimiliki dalam suatu fungsi.
Anda akan menemui beberapa masalah sekaligus:
- Siapa yang menjamin urutan akhir dari kedua tugas ini.
- Bagaimana menangani kegagalan secara seragam.
- Bagaimana penelepon mengetahui kapan seluruh
loadPage()benar-benar selesai. - Jika tugas luar dibatalkan, apakah kedua subtugas akan berhenti bersamaan?
Jika tujuannya adalah untuk mengeksekusi secara paralel, biasanya lebih jelas untuk menuliskannya sebagai async let atau grup tugas, daripada membuat dua Task buram tambahan.
3. Saat menghadapi persaingan status, saya ingin “menggeser puncak” dengan membuka beberapa Task lagi
Beberapa kode akan ditulis seperti ini:
Task {
self.loading = true
}
Task {
let data = await service.fetch()
self.items = data
}
Task {
self.loading = false
}
Di permukaan, hal ini terlihat seperti membongkar sesuatu, namun pada kenyataannya hal ini menyerahkan konsistensi negara secara langsung kepada keberuntungan.
Saya tidak tahu apakah tugas ketiga akan selesai sebelum tugas kedua, dan saya tidak tahu di kondisi mana antarmuka akan berhenti ketika pembatalan terjadi.
Akar penyebab masalah seperti ini biasanya adalah aliran status yang harus dihubungkan terputus.
4. Untuk menilai apakah Anda harus membuka Task, tanyakan empat pertanyaan ini terlebih dahulu
Ini adalah kumpulan daftar periksa teknik yang paling berguna. Jauh lebih berguna daripada menghafal tata bahasa.
1. Siapa yang membuat tugas ini?
Apakah itu dibuat dengan mengklik tombol? Dibuat saat halaman muncul? Dibuat selama inisialisasi ViewModel? Atau apakah itu dibuat secara diam-diam oleh lapisan layanan?
Jika jawaban atas “siapa yang menciptakannya” tidak jelas, hampir mustahil untuk mengetahui “siapa yang harus membatalkannya” di kemudian hari.
2. Siapa pemilik tugas ini?
Jika misinya hanya sekedar tembak-dan-lupakan, biasanya itu berarti tidak ada seorang pun yang benar-benar mengelolanya.
Namun banyak bisnis yang tidak cocok untuk api-dan-lupakan.
Misalnya, pencarian, paging, penyimpanan, pengunggahan, dan polling, tugas-tugas ini sering kali harus secara eksplisit dipegang oleh suatu objek:
final class SearchViewModel: ObservableObject {
private var searchTask: Task<Void, Never>?
func updateKeyword(_ keyword: String) {
searchTask?.cancel()
searchTask = Task {
await search(keyword)
}
}
}
Yang sangat berharga di sini adalah:
-Hanya ada satu pintu masuk untuk tugas serupa
- Ketika tugas baru muncul, tugas lama akan dibatalkan
- Tugas itu milik
SearchViewModel
Task yang tidak “dimiliki” biasanya akan menjadi misi hantu nantinya.
3. Jika pengguna meninggalkan halaman, haruskah halaman terus berjalan?
Masalah ini sangat penting karena secara langsung menentukan di mana siklus hidup tugas harus dibatasi.
Misalnya:
- Permintaan halaman paro atas: Pengguna biasanya tidak perlu melanjutkan setelah meninggalkan halaman
- Pengiriman pesanan: mungkin perlu terus diselesaikan meskipun halaman ditutup
- Pengambilan gambar terlebih dahulu: prioritasnya bisa sangat rendah, dan harus dibatalkan saat meninggalkan halaman
Jenis tugas yang berbeda memiliki desain yang sangat berbeda.
Tanpa menjawab pertanyaan ini terlebih dahulu, mudah untuk menulis semua tugas seperti ini:
Task {
await doSomething()
}
Di permukaan mereka bersatu, namun kenyataannya semantiknya benar-benar kacau.
4. Siapa yang akan mengkonsumsi hasil tugas ini?
Hasil dari beberapa tugas akan ditulis kembali ke UI, hasil dari beberapa tugas perlu memperbarui cache, dan beberapa tugas hanya akan dilaporkan.
Jika hasilnya tidak jelas tujuannya, biasanya muncul dua jenis bau tak sedap:
- Tugas dibuka, tetapi tidak ada yang menjawab kesalahannya
- Tugas telah selesai, tetapi tidak ada yang menggunakannya
Jadi saya bukan penggemar api-dan-lupa yang tak terkendali. Sebagian besar tugas bisnis bukan “hanya mengirimkannya”.
5. Saat sudah berada di dunia async, berikan prioritas pada konkurensi terstruktur daripada membuat Task tambahan
Ini adalah poin yang banyak artikelnya gagal untuk menjawabnya dengan jujur.
Sudah ada di fungsi async, artinya sudah ada aliran kontrol asynchronous. Menulis Task tambahan saat ini sering kali melewati batasan yang diberikan oleh konkurensi terstruktur.
Lihatlah kedua perbandingan tersebut.
Kecenderungan kesalahan: Gunakan beberapa penghancuran keras Task secara paralel
func loadDashboard() async {
let userTask = Task { await api.loadUser() }
let statsTask = Task { await api.loadStats() }
let noticesTask = Task { await api.loadNotices() }
let user = await userTask.value
let stats = await statsTask.value
let notices = await noticesTask.value
self.state = .loaded(user, stats, notices)
}
Kode ini tidak sepenuhnya salah, namun tidak cukup eksplisit. Karena yang dilihat penelepon adalah “Saya aktif membuat tiga tugas” bukannya “Ada tiga ketergantungan paralel di sini”.
Ekspresi yang lebih baik: async let
func loadDashboard() async throws {
async let user = api.loadUser()
async let stats = api.loadStats()
async let notices = api.loadNotices()
self.state = try .loaded(user: user, stats: stats, notices: notices)
}
Kelebihan tulisan jenis ini adalah semantiknya lebih jelas:
- Pekerjaan ini termasuk dalam fungsi saat ini
- Mereka dirantai ke struktur yang sama dengan panggilan saat ini
- Fungsi saat ini akan menunggu hasilnya sebelum berakhir
- Ketika lapisan luar dibatalkan, konkurensi dalam juga dibatalkan.
Dengan kata lain, perbedaan antara Task dan konkurensi terstruktur terletak pada siapa yang bertanggung jawab atas siklus hidupnya.
6. Bencana paling umum di tingkat halaman: buka satu Task untuk setiap pintu masuk
Mengambil halaman daftar yang sangat nyata sebagai contoh, biasanya terdapat titik pemicu berikut:
- Pemuatan halaman entri pertama
- Tarik ke bawah untuk menyegarkan
- Perubahan kata kunci pencarian
- Ganti filter
- Klik “Coba Lagi”
- Buka halaman berikutnya untuk memuat lebih banyak secara otomatis
Jika setiap entri ditulis sendiri:
Task {
await load()
}
Setelah satu atau dua iterasi, halaman tersebut kemungkinan besar akan mengalami fenomena berikut:
- Beberapa permintaan keluar secara bersamaan
- Hasil lama menimpa hasil baru
- Jelas kata kunci terbaru adalah
swift, tetapi antarmuka menunjukkan hasilswi - Setelah pengguna keluar dari halaman, panggilan balik masih tertulis
loading,isRefreshing,errorbertarung satu sama lain
Situasi umum pada tahap ini adalah salah mengira bahwa apa yang Anda temui adalah “konkurensi kompleks”.
Faktanya, masalahnya lebih spesifik: **Pintu masuk tugas terlalu tersebar, dan tidak ada penutupan perubahan status yang terpadu. **
Pendekatan yang lebih stabil biasanya memusatkan “tugas apa yang harus dibuka” ke dalam objek keadaan, daripada membiarkan lapisan tampilan membuat tugas baru di mana saja.
Misalnya:
@MainActor
final class ArticlesViewModel: ObservableObject {
@Published private(set) var state: ViewState = .idle
private var reloadTask: Task<Void, Never>?
func reload() {
reloadTask?.cancel()
reloadTask = Task {
state = .loading
do {
let articles = try await repository.fetchArticles()
guard !Task.isCancelled else { return }
state = .loaded(articles)
} catch is CancellationError {
// 忽略取消
} catch {
state = .failed(error)
}
}
}
}
Ada tiga masalah yang benar-benar dipecahkan oleh kode ini:
- Ada pintu masuk unik untuk tugas serupa
- Hubungan substitusi antara tugas serupa jelas -Penulisan status di satu tempat
Task masih digunakan di sini, tetapi sudah menjadi “pintu masuk terkontrol ke manajemen tugas”.
7. Kapan waktu yang tepat untuk menyimpan referensi Task dan kapan tidak diperlukan?
Ini juga merupakan sinyal untuk menilai kematangan kode tersebut.
Cocok untuk skenario di mana referensi disimpan
- Cari masukan anti goyang
- Tugas penyegaran halaman
- Permintaan yang dapat dipicu berulang kali dan tugas baru harus menggantikan tugas lama
- Proses polling, pendengaran, dan sinkronisasi yang berjalan lama
Karena skenario ini tentu saja melibatkan pembatalan atau penggantian.
Tidak perlu mengadakan adegan yang direferensikan
- Tugas singkat yang hanya dilakukan pengguna satu kali setelah mengklik satu kali
- Ini jelas merupakan titik penguburan, pembersihan log dan cache dari api-dan-lupakan
- Tugas yang siklus hidupnya telah dikelola oleh kerangka luar tim
Fokusnya bukan pada “apakah tugas tersebut harus lebih maju atau tidak”, namun pada apakah tugas tersebut dikelola dengan benar.
Jika suatu tugas dapat dibatalkan, diganti, atau dapat memengaruhi visibilitas pengguna, kemungkinan besar tugas tersebut tidak boleh dianggap sebagai kembang api anonim.
8. Task.detached adalah pernyataan isolasi yang lebih kuat
Meskipun artikel ini terutama membahas tentang Task, banyak tim akan segera menggunakan Task.detached setelah membuka Task secara acak.
Berikut pengingat singkatnya:
Task {}akan mewarisi sebagian dari konteks saat iniTask.detached {}lebih seperti "terpisah dari konteks saat ini dan dijalankan secara mandiri"Oleh karena itu, jika atribusi dan pembatalanTaskbiasa tidak diluruskan, makadetachedtidak boleh digunakan untuk memperbesar derajat kebebasan.
Banyak Task.detached yang akhirnya menjadi pelarian dari tanggung jawab.
9. Kriteria penilaian praktis: Apakah Anda membuat tugas atau menghindari pemodelan?
Ini adalah pertanyaan yang paling sering ditanyakan dalam ulasan saya.
Saat siap untuk menulis:
Task {
...
}
Berhentilah selama dua detik dan tanyakan pada diri Anda:
- Apakah saya membuat tugas dengan siklus hidup yang jelas?
- Atau hanya karena terlalu merepotkan untuk menggantinya ke
asyncsehingga untuk sementara ditutup dengan lapisan? - Tahukah saya kapan berakhirnya, siapa yang membatalkannya, dan kepada siapa berakhirnya?
Jika Anda tidak dapat menjawab pertanyaan-pertanyaan ini, dalam banyak kasus, tingkat abstraksi saat ini belum diluruskan.
10. Kesimpulan: Task layak digunakan secara sering, namun tidak layak digunakan dengan santai.
Task tentunya penting dalam Swift Concurrency dan sering digunakan.
Namun nilai sebenarnya adalah:
- Masuk ke proses asinkron dengan aman di pintu masuk sinkron
- Secara eksplisit membuat tugas ketika siklus hidup mandiri diperlukan
- Memberikan batasan konkurensi yang jelas ketika pembatalan, penggantian, dan isolasi diperlukan
Jadi saya lebih suka memahaminya seperti ini:
Taskadalah deklarasi eksplisit siklus hidup tugas.
Ketika digunakan sebagai pernyataan, kodenya menjadi semakin jelas. Saat menggunakannya sebagai patch, cepat atau lambat kodenya akan menjadi “setiap lapisan bisa berjalan, tapi tidak ada yang tahu kenapa berjalan seperti ini”.
What to read next
Want more posts about Swift Concurrency?
Posts in the same category are usually the best next step for reading more on this topic.
View same categoryWant to keep following #Swift Concurrency?
Tags are useful for related tools, specific problems, and similar troubleshooting notes.
View same tagWant to explore another direction?
If you are not sure what to read next, return to the homepage and start from categories, topics, or latest updates.
Back home