Pertama, penting untuk mengetahui perbedaan antara untaian dan antrean dan apa yang sebenarnya dilakukan GCD. Saat kami menggunakan antrean pengiriman (melalui GCD), kami benar-benar mengantri, bukan melakukan threading. Kerangka kerja Dispatch dirancang khusus untuk menjauhkan kita dari threading, karena Apple mengakui bahwa "menerapkan solusi threading yang benar [dapat] menjadi sangat sulit, jika tidak [terkadang] mustahil untuk dicapai". Oleh karena itu, untuk melakukan tugas secara bersamaan (tugas yang kami tidak ingin membekukan UI), yang perlu kita lakukan hanyalah membuat antrean tugas tersebut dan menyerahkannya ke GCD. Dan GCD menangani semua penguliran terkait. Oleh karena itu, yang sebenarnya kami lakukan hanyalah mengantri.
Hal kedua yang harus segera diketahui adalah apa itu tugas. Sebuah tugas adalah semua kode di dalam blok antrian itu (tidak di dalam antrian, karena kita bisa menambahkan sesuatu ke antrian sepanjang waktu, tapi di dalam closure tempat kita menambahkannya ke antrian). Sebuah tugas terkadang disebut sebagai blok dan blok terkadang disebut sebagai tugas (tetapi mereka lebih dikenal sebagai tugas, khususnya di komunitas Swift). Dan tidak peduli seberapa banyak atau sedikit kode, semua kode di dalam kurung kurawal dianggap sebagai satu tugas:
serialQueue.async {
// this is one task
// it can be any number of lines with any number of methods
}
serialQueue.async {
// this is another task added to the same queue
// this queue now has two tasks
}
Dan jelas menyebutkan bahwa konkuren berarti pada saat yang sama dengan hal-hal lain dan arti serial satu demi satu (tidak pernah pada waktu yang sama). Menyerialisasikan sesuatu, atau memasukkan sesuatu ke dalam serial, berarti menjalankannya dari awal sampai akhir dalam urutannya dari kiri ke kanan, atas ke bawah, tanpa gangguan.
Ada dua jenis antrean, serial dan konkuren, tetapi semua antrean serentak relatif satu sama lain . Fakta bahwa Anda ingin menjalankan kode apa pun "di latar belakang" berarti Anda ingin menjalankannya secara bersamaan dengan utas lain (biasanya utas utama). Oleh karena itu, semua antrian pengiriman, serial atau bersamaan, menjalankan tugas mereka secara bersamaan relatif terhadap antrian lainnya . Setiap serialisasi yang dilakukan oleh antrian (dengan antrian serial), hanya berkaitan dengan tugas-tugas dalam antrian pengiriman [serial] tunggal itu (seperti dalam contoh di atas di mana ada dua tugas dalam antrian serial yang sama; tugas-tugas itu akan dijalankan satu demi satu yang lain, tidak pernah secara bersamaan).
ANTRIAN SERIAL (sering dikenal sebagai antrean pengiriman pribadi) menjamin pelaksanaan tugas satu per satu dari awal hingga selesai dalam urutan yang ditambahkan ke antrean tertentu. Ini adalah satu-satunya jaminan serialisasi di mana pun dalam diskusi antrian pengiriman--bahwa tugas tertentu dalam antrian serial tertentu dijalankan dalam serial. Antrian serial dapat, bagaimanapun, berjalan secara bersamaan dengan antrian serial lainnya jika mereka adalah antrian terpisah karena, sekali lagi, semua antrian adalah bersamaan relatif satu sama lain. Semua tugas berjalan di utas berbeda tetapi tidak setiap tugas dijamin berjalan di utas yang sama (tidak penting, tetapi menarik untuk diketahui). Dan kerangka kerja iOS tidak dilengkapi dengan antrian serial yang siap digunakan, Anda harus membuatnya. Antrian privat (non-global) adalah serial secara default, jadi untuk membuat antrian serial:
let serialQueue = DispatchQueue(label: "serial")
Anda dapat membuatnya bersamaan melalui properti atributnya:
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])
Tetapi pada titik ini, jika Anda tidak menambahkan atribut lain ke antrean privat, Apple merekomendasikan agar Anda hanya menggunakan salah satu antrean global siap pakai mereka (yang semuanya bersamaan). Di bagian bawah jawaban ini, Anda akan melihat cara lain untuk membuat antrian serial (menggunakan properti target), seperti yang disarankan Apple untuk melakukannya (untuk pengelolaan sumber daya yang lebih efisien). Tapi untuk saat ini, memberi label saja sudah cukup.
ANTRIAN BERTENTANG (sering dikenal sebagai antrian pengiriman global) dapat menjalankan tugas secara bersamaan; Namun, tugas dijamin dimulai dalam urutan yang ditambahkan ke antrean tertentu, tetapi tidak seperti antrean serial, antrean tidak menunggu hingga tugas pertama selesai sebelum memulai tugas kedua. Tugas (seperti antrean serial) dijalankan pada utas berbeda dan (seperti antrean serial) tidak setiap tugas dijamin berjalan di utas yang sama (tidak penting, tetapi menarik untuk diketahui). Dan kerangka kerja iOS hadir dengan empat antrean bersamaan yang siap digunakan. Anda dapat membuat antrean bersamaan menggunakan contoh di atas atau dengan menggunakan salah satu antrean global Apple (yang biasanya disarankan):
let concurrentQueue = DispatchQueue.global(qos: .default)
RETAIN-CYCLE RESISTANT: Antrean pengiriman adalah objek yang dihitung berdasarkan referensi, tetapi Anda tidak perlu menyimpan dan melepaskan antrean global karena antrean tersebut bersifat global, sehingga dipertahankan dan rilis akan diabaikan. Anda dapat mengakses antrian global secara langsung tanpa harus menetapkannya ke properti.
Ada dua cara untuk mengirim antrian: sinkron dan asinkron.
SYNC DISPATCHING berarti bahwa thread tempat antrian dikirim (thread pemanggil) berhenti setelah mengirim antrian dan menunggu tugas di blok antrian itu selesai dieksekusi sebelum melanjutkan. Untuk mengirim secara sinkron:
DispatchQueue.global(qos: .default).sync {
// task goes in here
}
ASYNC DISPATCHING berarti bahwa thread pemanggil terus berjalan setelah pengiriman antrian dan tidak menunggu tugas dalam blok antrian tersebut untuk menyelesaikan eksekusi. Untuk mengirim secara asinkron:
DispatchQueue.global(qos: .default).async {
// task goes in here
}
Sekarang orang mungkin berpikir bahwa untuk menjalankan tugas dalam serial, antrian serial harus digunakan, dan itu tidak sepenuhnya benar. Untuk menjalankan beberapa tugas secara serial, antrian serial harus digunakan, tetapi semua tugas (diisolasi sendiri) dijalankan dalam serial. Pertimbangkan contoh ini:
whichQueueShouldIUse.syncOrAsync {
for i in 1...10 {
print(i)
}
for i in 1...10 {
print(i + 100)
}
for i in 1...10 {
print(i + 1000)
}
}
Tidak peduli bagaimana Anda mengonfigurasi (serial atau bersamaan) atau mengirim (sinkronisasi atau asinkron) antrean ini, tugas ini akan selalu dijalankan dalam serial. Loop ketiga tidak akan pernah berjalan sebelum loop kedua dan loop kedua tidak akan pernah berjalan sebelum loop pertama. Hal ini berlaku dalam antrean apa pun yang menggunakan pengiriman apa pun. Ini adalah saat Anda memperkenalkan banyak tugas dan / atau antrean di mana serial dan konkurensi benar-benar berperan.
Pertimbangkan dua antrian ini, satu serial dan satu bersamaan:
let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)
Katakanlah kita mengirimkan dua antrian bersamaan di async:
concurrentQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
103
3
104
4
105
5
Outputnya campur aduk (seperti yang diharapkan) tetapi perhatikan bahwa setiap antrian menjalankan tugasnya sendiri dalam serial. Ini adalah contoh konkurensi paling dasar - dua tugas yang berjalan secara bersamaan di latar belakang dalam antrean yang sama. Sekarang mari kita buat serial yang pertama:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
101
1
2
102
3
103
4
104
5
105
Bukankah antrian pertama seharusnya dijalankan secara serial? Itu (dan begitu juga yang kedua). Apa pun yang terjadi di latar belakang tidak menjadi masalah antrian. Kami memberi tahu antrian serial untuk dieksekusi dalam serial dan itu berhasil ... tetapi kami hanya memberikannya satu tugas. Sekarang mari kita berikan dua tugas:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
Dan ini adalah contoh serialisasi yang paling dasar (dan hanya mungkin) - dua tugas yang berjalan secara serial (satu demi satu) di latar belakang (ke utas utama) dalam antrian yang sama. Tetapi jika kita menjadikannya dua antrian serial yang terpisah (karena dalam contoh di atas mereka adalah antrian yang sama), keluarannya akan bercampur aduk lagi:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue2.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
3
103
4
104
5
105
Dan inilah yang saya maksud ketika saya mengatakan semua antrean berbarengan relatif satu sama lain. Ini adalah dua antrian serial yang menjalankan tugasnya pada saat yang sama (karena antriannya terpisah). Antrian tidak tahu atau peduli dengan antrian lainnya. Sekarang mari kembali ke dua antrian serial (dari antrian yang sama) dan tambahkan antrian ketiga, yang bersamaan:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 1000)
}
}
1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005
Itu agak tidak terduga, mengapa antrian bersamaan menunggu antrian serial selesai sebelum dieksekusi? Itu bukan konkurensi. Taman bermain Anda mungkin menunjukkan hasil yang berbeda tetapi tempat bermain saya menunjukkan ini. Dan ini menunjukkan ini karena prioritas antrean serentak saya tidak cukup tinggi bagi GCD untuk menjalankan tugasnya lebih cepat. Jadi jika saya menjaga semuanya tetap sama tetapi mengubah QoS antrian global (kualitas layanannya, yang merupakan tingkat prioritas antrian) let concurrentQueue = DispatchQueue.global(qos: .userInteractive)
, maka hasilnya seperti yang diharapkan:
1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105
Dua antrian serial menjalankan tugas mereka dalam serial (seperti yang diharapkan) dan antrian bersamaan mengeksekusi tugasnya lebih cepat karena diberikan tingkat prioritas tinggi (QoS tinggi, atau kualitas layanan).
Dua antrian bersamaan, seperti pada contoh cetakan pertama kita, menunjukkan hasil cetakan campur aduk (seperti yang diharapkan). Untuk membuatnya dicetak dengan rapi dalam serial, kita harus membuat keduanya dalam antrian serial yang sama (contoh antrian yang sama, juga, bukan hanya label yang sama) . Kemudian setiap tugas dijalankan secara serial sehubungan dengan yang lain. Namun, cara lain untuk membuatnya dicetak secara serial adalah dengan membuatnya tetap bersamaan tetapi mengubah metode pengirimannya:
concurrentQueue.sync {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
Ingat, sinkronisasi pengiriman hanya berarti bahwa utas panggilan menunggu sampai tugas dalam antrian selesai sebelum melanjutkan. Peringatan di sini, jelas, adalah bahwa utas panggilan dibekukan sampai tugas pertama selesai, yang mungkin atau mungkin tidak seperti yang Anda inginkan untuk dilakukan UI.
Dan karena alasan inilah kami tidak dapat melakukan hal berikut:
DispatchQueue.main.sync { ... }
Ini adalah satu-satunya kombinasi antrian dan metode pengiriman yang tidak dapat kami lakukan — pengiriman sinkron pada antrean utama. Dan itu karena kami meminta antrean utama untuk dibekukan hingga kami menjalankan tugas dalam kurung kurawal ... yang kami kirimkan ke antrean utama, yang baru saja kami bekukan. Ini disebut kebuntuan. Untuk melihatnya beraksi di taman bermain:
DispatchQueue.main.sync { // stop the main queue and wait for the following to finish
print("hello world") // this will never execute on the main queue because we just stopped it
}
// deadlock
Satu hal terakhir yang perlu disebutkan adalah sumber daya. Saat kami memberikan tugas ke antrean, GCD menemukan antrean yang tersedia dari kumpulan yang dikelola secara internal. Sejauh penulisan jawaban ini, ada 64 antrian yang tersedia per qos. Itu mungkin terlihat banyak tetapi mereka dapat dengan cepat dikonsumsi, terutama oleh pustaka pihak ketiga, terutama kerangka kerja database. Untuk alasan ini, Apple memiliki rekomendasi tentang manajemen antrian (disebutkan dalam tautan di bawah); satu makhluk:
Daripada membuat antrean serentak pribadi, kirimkan tugas ke salah satu antrean pengiriman serentak global. Untuk tugas serial, setel target antrian serial Anda ke salah satu antrian serentak global.
Dengan begitu, Anda dapat mempertahankan perilaku antrian serial sambil meminimalkan jumlah antrian terpisah yang membuat utas.
Untuk melakukan ini, alih-alih membuatnya seperti yang kami lakukan sebelumnya (yang masih bisa Anda lakukan), Apple merekomendasikan membuat antrean serial seperti ini:
let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [], autoreleaseFrequency: .inherit, target: .global(qos: .default))
Untuk bacaan lebih lanjut, saya merekomendasikan yang berikut ini:
https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091-CH1-SW1
https://developer.apple.com/documentation/dispatch/dispatchqueue