Cara membuat metode dibatalkan tanpa itu menjadi jelek?

Saat ini saya dalam proses perkuatan kami berjalan lama metode yang akan dibatalkan. Saya berencana menggunakan Sistem.Threading.Tugas-tugas.CancellationToken untuk melaksanakan itu.

Metode kami biasanya melakukan beberapa lama berjalan beberapa langkah saja (mengirim perintah dan kemudian menunggu untuk hardware sebagian besar), misalnya

void Run()
{
    Step1();
    Step2();    
    Step3();
}

Pertama saya (mungkin bodoh) berpikir tentang pembatalan akan mengubah ini menjadi

bool Run(CancellationToken cancellationToken)
{
    Step1(cancellationToken);

    if (cancellationToken.IsCancellationRequested)
        return false;

    Step2(cancellationToken);

    if (cancellationToken.IsCancellationRequested)
        return false;    

    Step3(cancellationToken);

    if (cancellationToken.IsCancellationRequested)
        return false;

    return true;
}

yang terus terang tampak mengerikan. Ini "pola" akan terus dalam langkah tunggal, terlalu (dan mereka tentu agak gondrong sudah). Hal ini akan membuat Thread.Abort() terlihat agak seksi, meskipun aku tahu tidak dianjurkan.

Apakah ada cleaner pola untuk mencapai hal ini yang tidak menyembunyikan aplikasi logika di bawah banyak kode boilerplate?

Edit

Sebagai contoh untuk sifat dari langkah-langkah, Run metode bisa baca

void Run()
{
    GiantRobotor.MoveToBase();
    Oven.ThrowBaguetteTowardsBase();    
    GiantRobotor.CatchBaguette();
    // ...
}

Kami mengendalikan perangkat keras yang berbeda dengan unit-unit yang membutuhkan untuk dapat disinkronisasi untuk bekerja bersama-sama.

Mengomentari pertanyaan (10)
Larutan

Jika langkah-langkah yang entah bagaimana independend mengenai dataflow dalam metode, tetapi dapat't akan dieksekusi secara paralel masalah, pendekatan berikut mungkin lebih baik dibaca:

void Run()
{
    // list of actions, defines the order of execution
    var actions = new List<action>() {
       ct => Step1(ct),
       ct => Step2(ct),
       ct => Step3(ct) 
    };

    // execute actions and check for cancellation token
    foreach(var action in actions)
    {
        action(cancellationToken);

        if (cancellationToken.IsCancellationRequested)
            return false;
    }

    return true;
}

Jika langkah-langkah don't perlu pembatalan token karena anda dapat membaginya dalam unit kecil, anda bahkan dapat menulis lebih kecil daftar definisi:

var actions = new List<action>() {
    Step1, Step2, Step3
};
Komentar (4)

bagaimana kelanjutan?

var t = Task.Factory.StartNew(() => Step1(cancellationToken), cancellationToken)
   .ContinueWith(task => Step2(cancellationToken), cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current)
   .ContinueWith(task => Step3(cancellationToken), cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current);
Komentar (2)

Saya akui itu isn't cukup, tetapi bimbingan yang lebih baik untuk melakukan apa yang telah anda lakukan:

if (cancellationToken.IsCancellationRequested) { /* Stop */ }

...atau sedikit lebih pendek:

cancellationToken.ThrowIfCancellationRequested()

Umumnya, jika anda dapat lulus pembatalan token untuk langkah-langkah individu, anda dapat menyebarkan cek keluar seperti yang mereka don't jenuh kode. Anda mungkin juga memilih untuk tidak memeriksa untuk pembatalan constantly; jika operasi anda melakukan idempotent dan tidak't intensif sumber daya, anda don't perlu memeriksa untuk pembatalan pada setiap tahap. Waktu yang paling penting untuk memeriksa sebelum kembali hasilnya.

Jika anda're lewat token ke semua langkah anda, anda bisa melakukan sesuatu seperti ini:

public static CancellationToken VerifyNotCancelled(this CancellationToken t) {
    t.ThrowIfCancellationRequested();
    return t;
}

...

Step1(token.VerifyNotCancelled());
Step2(token.VerifyNotCancelled());
Step3(token.VerifyNotCancelled());
Komentar (2)

Ketika saya harus melakukan sesuatu seperti itu, saya membuat sebuah delegasi untuk melakukan hal ini:

bool Run(CancellationToken cancellationToken)
{
    var DoIt = new Func<action,bool>((f) =>
    {
        f(cancellationToken);
        return cancellationToken.IsCancellationRequested;
    });

    if (!DoIt(Step1)) return false;
    if (!DoIt(Step2)) return false;
    if (!DoIt(Step3)) return false;

    return true;
}

Atau, jika tidak pernah ada kode antara langkah-langkah, anda dapat menulis:

return DoIt(Step1) && DoIt(Step2) && DoIt(Step3);
Komentar (0)

Versi pendek:

Penggunaan kunci() sinkronisasi Benang.Abort() call dengan bagian penting.


Mari saya jelaskan versi:

Umumnya, ketika membatalkan benang's'd harus mempertimbangkan dua jenis kode:

Selama menjalankan kode anda don't benar-benar peduli jika selesai

  • Kode yang benar-benar telah menjalankan itu's saja

Jenis pertama adalah jenis yang kita don't peduli jika pengguna diminta batalkan. Mungkin kita menghitung sampai seratus miliar dan dia doesn't peduli lagi?

Jika kita'd gunakan sentinel seperti CancellationToken, kita tidak akan menguji mereka dalam setiap iterasi penting kode yang akan kita?

for(long i = 0; i < bajillion; i++){
    if(cancellationToken.IsCancellationRequested)
        return false;
    counter++;
}

Jadi jelek. Jadi untuk kasus ini, Benang.Abort() adalah anugerah.

Sayangnya, karena beberapa orang mengatakan, anda bisa't menggunakan Benang.Abort() karena atom kode yang benar-benar memiliki untuk menjalankan! Kode telah dikurangi uang bentuk akun anda, sekarang anda harus menyelesaikan transaksi dan transfer uang ke akun target. Ada yang suka uang menghilang.

Untungnya, kami telah saling pengecualian untuk membantu kita dengan hal semacam ini. Dan C# membuatnya cukup:

//unimportant long task code
lock(_lock)
{
    //atomic task code
}

Dan di tempat lain

lock(_lock) //same lock
{
    _thatThread.Abort();
}

Jumlah kunci yang akan selalu menjadi <= jumlah penjaga, karena anda'd ingin sentinel pada kode penting juga (untuk membuatnya membatalkan lebih cepat). Hal ini membuat kode yang sedikit lebih cantik dari sentinel versi, tetapi juga membuat aborting baik, karena itu doesn't harus menunggu untuk hal yang tidak penting.


Juga untuk dicatat adalah bahwa ThreadAbortException bisa timbul di mana saja, bahkan di akhirnya blok. Ini ketidakpastian membuat Benang.Abort() jadi kontroversial.

Dengan kunci anda'd hindari hal ini dengan hanya mengunci seluruh coba-menangkap-akhirnya blok. Itu pembersihan di akhirnya adalah penting, maka seluruh blok dapat dikunci. coba-coba blok pada umumnya dibuat sesingkat mungkin, satu baris sebaiknya, jadi kita tidak't mengunci setiap kode yang tidak perlu.

Hal ini membuat Benang.Abort(), seperti yang anda katakan, sedikit kurang jahat. Anda mungkin tidak ingin menyebutnya di UI benang, meskipun, seperti yang anda're sekarang menguncinya.

Komentar (0)

Jika refactor diperbolehkan, anda bisa refactor Langkah metode seperti yang ada merupakan salah satu metode Langkah(int nomor).

Anda kemudian dapat loop melalui dari 1 sampai N dan memeriksa jika pembatalan token diminta hanya sekali.

bool Run(CancellationToken cancellationToken) {
    for (int i = 1; i < 3 && !cancellationToken.IsCancellationRequested; i++) 
        Step(i, cancellationToken);

    return !cancellationToken.IsCancellationRequested;
}

Atau, ekuivalen: (Mana yang anda sukai)

bool Run(CancellationToken cancellationToken) {
    for (int i = 1; i < 3; i++) {
        Step(i, cancellationToken);
        if (cancellationToken.IsCancellationRequested)
            return false;
    }
    return true;
}
Komentar (2)

Anda mungkin ingin mengambil melihat Apple's NSOperation pattern sebagai contoh. It's lebih rumit daripada hanya membuat satu metode dibatalkan, tapi itu's cukup kuat.

Komentar (0)

Saya agak heran, bahwa tidak ada yang telah mengusulkan standar, built-in, cara penanganan ini:

bool Run(CancellationToken cancellationToken)
{        
    //cancellationToke.ThrowIfCancellationRequested();

    try
    {
        Step1(cancellationToken);
        Step2(cancellationToken);
        Step3(cancellationToken);
    }
    catch(OperationCanceledException ex)
    {
        return false;
    }

    return true;
}

void Step1(CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();
    ...
}

Sementara biasanya anda don't ingin mengandalkan mendorong cek ke tingkat yang lebih dalam, dalam hal ini langkah-langkah yang sudah menerima CancellationToken dan harus cek pula (sebagai harus setiap non-trivial metode menerima CancellationToken).

Hal ini juga memungkinkan anda untuk menjadi seperti butiran atau non-granular dengan pemeriksaan yang diperlukan, yaitu sebelum berjalan lama / operasi intensif.

Komentar (0)