Nilai semaphore maksimal?

Nov 08 2020

Misalnya, ada 1000 kali loop. Berapa nilai maksimal agar cepat, efektif, dan tidak buntu?

let group = DispatchGroup()
let queue = DispatchQueue(label: "com.num.loop", attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 4)
for i in 1...1000 {
    semaphore.wait()
    group.enter()
    queue.async(group: group, execute: {
        doWork(i)                                    
        group.leave()
        semaphore.signal()
    })            
}

group.notify(queue: DispatchQueue.main) {
    // go on...
}

Jawaban

4 Rob Nov 08 2020 at 22:16

Beberapa pengamatan:

  1. Anda tidak ingin melebihi jumlah maksimum utas pekerja GCD per QoS. Jika Anda melebihi ini, Anda mungkin mengalami pemblokiran dalam aplikasi Anda. Terakhir saya periksa, batas ini 64 utas.

  2. Karena itu, umumnya ada sedikit manfaat melebihi jumlah inti pada perangkat Anda.

  3. Seringkali, kami membiarkan GCD mengetahui jumlah maksimum utas bersamaan untuk kami gunakan concurrentPerform, yang secara otomatis dioptimalkan untuk perangkat. Ini juga menghilangkan kebutuhan akan semaphore atau grup apa pun, yang sering mengarah ke kode yang tidak terlalu berantakan:

    DispatchQueue.global().async {
        DispatchQueue.concurrentPerform(iterations: 1000) { i in
            doWork(i)                                    
        }
    
        DispatchQueue.main.async {
            // go on...
        }
    }
    

    Ini concurrentPerformakan menjalankan 1.000 iterasi secara paralel, tetapi membatasi jumlah utas bersamaan ke tingkat yang sesuai untuk perangkat Anda, menghilangkan kebutuhan akan semaphore. Tetapi concurrentPerform, itu sendiri, sinkron, tidak melanjutkan sampai semua iterasi selesai, menghilangkan kebutuhan untuk grup pengiriman. Jadi, kirimkan keseluruhan concurrentPerformke beberapa antrian latar belakang, dan ketika selesai, cukup lakukan "kode penyelesaian" Anda (atau, dalam kasus Anda, kirim kode itu kembali ke antrian utama).

  4. Sementara saya telah concurrentPerformmenjelaskannya di atas, itu hanya berfungsi jika doWorkmenjalankan tugasnya secara sinkron (misalnya beberapa operasi komputasi). Jika itu memulai sesuatu yang, itu sendiri, asinkron, maka kita harus kembali ke teknik semaphore / grup ini. (Atau, mungkin lebih baik, gunakan Operationsubclass asynchronous dengan antrian dengan wajar maxConcurrentOperationCountatau Gabungkan flatMap(maxPublishers:_:)dengan batas hitungan yang wajar).

    Mengenai nilai ambang yang wajar dalam kasus ini, tidak ada angka ajaib. Anda hanya perlu melakukan beberapa pengujian empiris, untuk menemukan keseimbangan yang wajar antara jumlah inti dan hal lain yang mungkin terjadi dalam aplikasi Anda. Misalnya, untuk permintaan jaringan, kami sering menggunakan 4 atau 6 sebagai jumlah maksimum, tidak hanya mempertimbangkan berkurangnya manfaat dalam melebihi jumlah itu, tetapi juga implikasi dari dampaknya pada server kami jika ribuan pengguna kebetulan mengirimkan terlalu banyak secara bersamaan permintaan pada saat yang sama.

  5. Dalam istilah "membuatnya cepat", pilihan "berapa banyak iterasi yang harus dijalankan secara bersamaan" hanyalah bagian dari proses pengambilan keputusan. Masalah yang lebih kritis dengan cepat menjadi memastikan bahwa doWorkpekerjaan yang cukup untuk membenarkan overhead sederhana yang diperkenalkan oleh pola konkuren.

    Misalnya, jika memproses gambar berukuran 1.000 × 1.000 piksel, Anda dapat melakukan 1.000.000 iterasi, masing-masing memproses satu piksel. Tetapi jika Anda melakukannya, Anda mungkin mendapati bahwa itu sebenarnya lebih lambat daripada rendisi non-konkuren Anda. Sebaliknya, Anda mungkin memiliki 1.000 iterasi, setiap iterasi memproses 1.000 piksel. Atau Anda mungkin memiliki 100 iterasi, masing-masing memproses 10.000 piksel. Teknik ini, yang disebut "melangkah", sering kali memerlukan sedikit penelitian empiris untuk menemukan keseimbangan yang tepat antara berapa banyak iterasi yang akan dilakukan dan berapa banyak pekerjaan yang dilakukan pada masing-masing. (Dan, omong-omong, seringkali pola langkah ini juga dapat mencegah cache sloshing, sebuah skenario yang dapat muncul jika beberapa utas bersaing untuk alamat memori yang berdekatan.)

  6. Terkait dengan poin sebelumnya, kami sering ingin berbagai utas ini menyinkronkan akses mereka ke sumber daya bersama (agar tetap aman untuk utas). Sinkronisasi tersebut dapat menimbulkan perselisihan di antara utas ini. Jadi, Anda pasti ingin memikirkan tentang bagaimana dan kapan Anda melakukan sinkronisasi ini.

    Misalnya, daripada memiliki beberapa sinkronisasi di dalamnya doWork, Anda mungkin meminta setiap iterasi memperbarui variabel lokal (di mana tidak diperlukan sinkronisasi) dan melakukan pembaruan yang disinkronkan ke sumber daya bersama hanya ketika penghitungan lokal selesai. Sulit untuk menjawab pertanyaan ini secara abstrak, karena ini akan sangat bergantung pada apa doWorkyang dilakukannya, tetapi dapat dengan mudah memengaruhi kinerja keseluruhan.