Pemrograman async di .NET: praktik terbaik

Munculnya async / menunggu di C # telah menyebabkan redefinisi tentang cara menulis kode paralel yang sederhana dan benar. Seringkali, menggunakan pemrograman asinkron, programmer tidak hanya tidak menyelesaikan masalah yang ada di utas, tetapi juga memperkenalkan yang baru. Kebuntuan dan penerbangan tidak pergi ke mana pun - mereka hanya menjadi lebih sulit untuk didiagnosis.



Dmitry Ivanov - Analisis Perangkat Lunak TeamLead di Huawei, mantan techlide JetBrains Rider dan pengembang inti ReSharper: struktur data, cache, multithreading, dan pembicara reguler di konferensi DotNext .

Di bawah cutscene - rekaman video dan transkrip teks dari laporan Dmitry dari konferensi DotNext 2019 Piter.



Narasi lebih lanjut atas nama pembicara.

Dalam kode multi-utas atau asinkron, sesuatu sering rusak. Alasannya bisa jadi jalan buntu dan ras. Sebagai aturan, sebuah perlombaan crash sekali dari seribu, seringkali tidak secara lokal, tetapi hanya pada server build, dan dibutuhkan beberapa hari untuk menangkapnya. Saya yakin bagi banyak orang ini adalah situasi yang akrab.

Selain itu, melihat kode asinkron bahkan oleh pengembang berpengalaman, saya mendapati diri saya berpikir bahwa beberapa hal dapat ditulis tiga kali lebih pendek dan lebih tepat.

Ini menunjukkan bahwa masalahnya bukan pada orang, tetapi pada instrumen. Orang-orang hanya menggunakan alat dan ingin menyelesaikan masalah mereka. Alat itu sendiri memiliki sejumlah besar kemampuan (kadang-kadang bahkan berlebihan), pengaturan, konteks tersirat, yang mengarah pada fakta bahwa sangat mudah digunakan secara tidak benar. Mari kita coba mencari tahu cara menggunakan async / menunggu dan bekerja dengan kelas Taskdi .NET.

Rencana


  • Masalah dengan pendekatan yang diselesaikan dengan async / menunggu.
  • Contoh desain kontroversial.
  • Sebuah tugas dari kehidupan nyata yang akan kita selesaikan secara tidak sinkron.


Async / menunggu dan masalah yang harus diselesaikan




Mengapa kita perlu async / menunggu? Katakanlah kita memiliki kode yang berfungsi dengan memori bersama yang dibagikan.

Pada awal pekerjaan, kami membaca permintaan, dalam hal ini, file dari antrian pemblokiran (misalnya, dari Internet atau dari disk), menggunakan permintaan pemblokiran Dequeue (permintaan pemblokiran akan ditandai dengan warna merah pada gambar dengan contoh).

Pendekatan ini membutuhkan banyak utas, dan setiap utas membutuhkan sumber daya, membuat beban pada penjadwal. Tapi ini bukan masalah utama. Misalkan orang dapat menulis ulang sistem operasi sehingga sistem ini mendukung seratus ribu dan sejuta utas. Tetapi masalah utama adalah bahwa beberapa utas tidak dapat diambil. Misalnya, Anda memiliki utas antarmuka pengguna. Tidak ada kerangka kerja UI normal yang memadai di mana akses ke data tidak hanya dari satu utas, belum. Utas UI tidak dapat diblokir. Dan agar tidak memblokirnya, kita memerlukan kode asinkron.

Sekarang mari kita bicara tentang tugas kedua. Setelah kami membaca file, itu perlu diproses. Kami akan melakukannya secara paralel.

Banyak dari Anda telah mendengar bahwa paralelisme tidak sama dengan asinkron. Dalam hal ini, muncul pertanyaan: bisakah asynchrony membantu menulis kode paralel yang lebih kompak, cantik, dan lebih cepat?

Tugas terakhir adalah bekerja dengan memori bersama. Apakah kita perlu menyeret mekanisme ini dengan kunci, sinkronisasi ke kode asinkron, atau bisakah ini dihindari? Bisakah async / menunggu bantuan dengan ini?

Jalan ke async / tunggu


Mari kita lihat evolusi pemrograman asinkron secara umum di dunia dan .NET.

Telepon balik


Void Foo(params, Action callback) {…}
 

Void OurMethod() {//synchronous code
 
    Foo(params,() =>{//asynchronous code;continuation
    });
}

Pemrograman asinkron dimulai dengan callback. Artinya, pertama Anda perlu memanggil beberapa bagian dari kode secara sinkron, dan bagian kedua - secara tidak sinkron. Misalnya, Anda membaca dari file, dan ketika data siap, entah bagaimana akan dikirimkan kepada Anda. Bagian asinkron ini dilewatkan sebagai panggilan balik .

Lebih banyak panggilan balik


void Foo(params, Action callback) {...} 
void Bar(Action callback) {...}
void Baz(Action callback) {...}

void OurMethod() {
    ... //synchronous code
    
    Foo(params, () => { 
      ... //continuation 1 
      Bar(() => {
        //continuation 2
        Baz(() => {
          //continuation 3
        }); 
      });
    });
}

Dengan demikian, dari satu panggilan balik Anda dapat mendaftarkan panggilan balik lain , dari mana Anda dapat mendaftarkan panggilan balik ketiga, dan pada akhirnya semuanya berubah menjadi Neraka Panggilan Balik .



Panggilan balik: pengecualian



void Foo(params, Action onSuccess, Action onFailure) {...}


void OurMethod() {
    ... //synchronous code 
    Foo(params, () => {
      ... //asynchronous code on success 
    },
    () => {
        ... //asynchronous code on failure
    }); 
}

Bagaimana cara bekerja dengan pengecualian? Misalnya, ReSharper, ketika secara terpisah merespons pengecualian dan eksekusi yang baik, tidak menunjukkan bagian kode yang paling indah - ada panggilan balik terpisah untuk situasi yang luar biasa dan untuk kelanjutan yang sukses. Hasilnya hanya seperti panggilan balik neraka , tetapi tidak linier, tetapi seperti pohon, yang bisa sangat membingungkan.



Dalam .NET, pendekatan panggilan balik pertama disebut Asynchronous Programming Model (APM). Metode ini akan dipanggil AsyncCallback, yang pada dasarnya sama dengan Action, tetapi pendekatan tersebut memiliki beberapa fitur. Pertama-tama, metode harus dimulai dengan kata "Mulai" (membaca dari file adalah BeginRead), yang mengembalikan beberapa AsyncResult. DiriAsyncResult- Ini adalah pawang yang tahu bahwa operasi telah selesai dan yang memiliki mekanisme WaitHandle. Anda WaitHandledapat menunggu, menunggu operasi selesai secara tidak sinkron. Di sisi lain, Anda dapat menelepon EndOperation, yaitu, membuat EndReaddan menggantung secara bersamaan (yang sangat mirip dengan properti Task.Result).

Pendekatan ini memiliki sejumlah masalah. Pertama, itu tidak melindungi kita dari neraka panggilan balik . Kedua, masih sepenuhnya tidak jelas apa yang harus dilakukan dengan pengecualian. Ketiga, tidak jelas di utas mana panggilan balik ini akan dipanggil - kami tidak memiliki kendali atas panggilan itu. Keempat, muncul pertanyaan, bagaimana menggabungkan potongan kode dengan panggilan balik?



Model kedua disebut Pola Asynchronous Berbasis Acara. Ini adalah pendekatan panggilan balik reaktif. Ide dari metode ini adalah bahwa kita meneruskan ke metode OperationNameAsyncbeberapa objek yang memiliki acara Selesai dan berlangganan ke acara ini. Seperti yang Anda perhatikan, BeginOperationNameberubah menjadi OperationNameAsync. Kebingungan dapat terjadi ketika Anda masuk ke kelas Socket, di mana dua pola dicampur: ConnectAsyncdan BeginConnect.

Harap dicatat bahwa Anda harus menelepon untuk membatalkan OperationNameAsyncCancel. Karena dalam .NET ini tidak ditemukan di tempat lain, biasanya semua orang mengirim CancellingT s . Jadi, jika Anda secara tidak sengaja menemukan metode di pustaka yang berakhir dengan Async, Anda perlu memahami bahwa itu tidak selalu kembali Task, tetapi dapat mengembalikan konstruksi yang sama.



Pertimbangkan model yang dikenal di Jawa sebagaiBerjangka , dalam JavaScript, sebagai Janji , dan dalam. NET, sebagai Pola Asinkron Tugas , dengan kata lain, "tugas." Metode ini mengasumsikan bahwa Anda memiliki beberapa objek perhitungan, dan Anda dapat melihat status objek ini (berjalan atau selesai). Dalam. NET, ada RnToCompletionpemisahan dua status yang disebut nyaman: awal tugas dan penyelesaian tugas. Kesalahan umum terjadi ketika suatu metode dipanggil pada tugas IsCompletedyang mengembalikan kelanjutan tidak berhasil, tetapi RnToCompletion, Canceleddan Faulted. Dengan demikian, hasil mengklik "Batalkan" di aplikasi UI harus berbeda dari pengembalian pengecualian (eksekusi). Di .NET, perbedaan telah dibuat: jika eksekusi adalah kesalahan Anda yang ingin Anda amankan, maka Batalkan- operasi paksa.

Dalam. NET, sebuah konsep juga diperkenalkan TaskScheduler- itu adalah semacam abstraksi di atas utas yang memberi tahu di mana harus menjalankan tugas. Dalam hal ini, dukungan pembatalan dirancang pada tingkat desain. Hampir semua operasi di pustaka di .NET memiliki CancellationTokenyang dapat diteruskan. Ini tidak berfungsi untuk semua bahasa: misalnya, di Kotlin Anda dapat membatalkan tugas, tetapi di .NET Anda tidak bisa. Solusinya mungkin adalah pembagian tanggung jawab antara mereka yang membatalkan tugas, dan tugas itu sendiri. Ketika Anda menerima tugas, Anda tidak dapat membatalkannya selain secara eksplisit - Anda harus meneruskannya CancellationToken.

Objek khusus TaskCompletionSourememungkinkan Anda untuk dengan mudah menyesuaikan API lama yang terkait dengan Pola Asinkron Berbasis Kejadian atau Model Pemrograman Asinkron. Ada dokumen yang harus Anda baca jika Anda memprogram dalam tugas. Ini menggambarkan semua perjanjian tentang tasas. Misalnya, metode apa pun, mengembalikan tugas, harus mengembalikannya dalam keadaan berjalan, yang berarti tidak bisa Created, sementara semua operasi tersebut harus berakhir Async.

Menggabungkan kelanjutan


Task ourMethod() {
  return Task.RunSynchronously(() =>{
    ... //synchronous code
  })
  .ContinueWith(_ =>{
    Foo(); //continuation 1
  })
  .ContinueWith(_ =>{
    Bar(); //continuation 2
  })
  .ContinueWith(_ =>{
    Baz(); //continuation 3
  })
}

Adapun kombinasi, dengan mempertimbangkan neraka panggilan balik , itu dapat muncul dalam bentuk yang lebih linier, meskipun ada potongan kode berulang dengan perubahan minimal. Tampaknya kodenya membaik dengan cara ini, tetapi ada jebakan di sini juga.

Mulai & lanjutkan tugas


Task.Factory.StartNew(Action, 
  TaskCreationOptions, 
  TaskScheduler, 
  CancellationToken
)
Task.ContinueWith(Action<Task>, 
  TaskContinuationOptions, 
  TaskScheduler, 
  CancellationToken
)

Mari kita beralih ke tiga parameter selama peluncuran tugas standar: yang pertama adalah opsi untuk memulai tugas, yang kedua adalah yang schedulerdi mana tugas diluncurkan, dan yang ketiga - CancellationToken.



TaskScheduler memberi tahu di mana tugas dimulai dan merupakan objek yang bisa Anda timpa secara independen. Misalnya, Anda dapat mengganti metode Queue. Jika Anda melakukannya TaskScheduleruntuk thread pool, metode Queuemengambil benang dari thread pooldan mengirimkan tugas Anda di sana.

Jika Anda mengambil scheduleralih utas utama, ia menempatkan semuanya dalam satu antrian, dan tugas dieksekusi secara berurutan pada utas utama. Namun, masalahnya adalah. NET Anda dapat menjalankan tugas tanpa melewati TaskScheduler. Timbul pertanyaan: bagaimana. NET menghitung tugas apa yang diberikan padanya? Saat tugas dimulai dari StartNewdalamAction, ThreadStatic. Currentdipamerkan di salah satu TaskScheduleryang kami berikan padanya.

Desain ini tampaknya agak kontroversial karena konteksnya yang implisit. Ada kasus ketika itu TaskSchedulerberisi kode asinkron yang mewarisi suatu tempat yang sangat dalam TaskScheduler.Currentdan tumpang tindih dengan penjadwal lain, yang menyebabkan kebuntuan. Dalam hal ini, Anda dapat menggunakan opsi TaskCreationOption.HideScheduler. Ini adalah bel alarm yang mengatakan bahwa kita memiliki beberapa opsi yang mengesampingkan ThreadStaticpengaturan.

Semuanya sama dengan kelanjutan. Timbul pertanyaan: dari mana datangnya TaskScheduleruntuk kelanjutan? Pertama-tama, ini diambil dalam metode di mana Anda memulai Continuation. Itu juga TaskSchedulerdiambil dari ThreadStatic. Adalah penting bahwa untuk menunggu / menunggu, kelanjutan bekerja dengan sangat berbeda.



Kami beralih ke parameter TaskCreationOptionsdan TaskContinuationOptions. Masalah utama mereka adalah ada banyak dari mereka. Beberapa parameter ini saling membatalkan, beberapa lainnya saling eksklusif. Semua parameter ini dapat digunakan dalam semua kombinasi yang memungkinkan, jadi sulit untuk mengingat semua yang dapat terjadi dengan kerinduan. Beberapa opsi ini bekerja sangat tidak bisa dimengerti.



Misalnya, parameter ExecuteSynchronouslydan RunContinuationsAsynchronouslymewakili dua opsi aplikasi yang mungkin, tetapi apakah kelanjutan akan diluncurkan secara serempak atau tidak serempak bergantung pada begitu banyak hal yang tidak akan Anda ketahui.



Contoh lain: kami meluncurkan tugas, meluncurkan kelanjutan dan secara bersamaan memberikan dua parameterTaskContinuations.ExecuteSynchronously, setelah itu mereka mulai melanjutkan secara tidak sinkron. Apakah akan dieksekusi di tumpukan yang sama di mana tugas sebelumnya berakhir, atau akankah itu ditransfer ke thread pool? Dalam hal ini, akan ada opsi ketiga: itu tergantung.



TaskCompletionSource


Pertimbangkan TaskCompletionSource. Saat Anda membuat tugas, Anda mengatur hasilnya SetResultuntuk menyesuaikan pola asinkron sebelumnya dengan dunia tugas. Anda TaskCompletionSourcedapat meminta tcs.Task, dan tugas ini akan masuk ke kondisi finishsaat Anda menelepon tcs.SetResult. Namun, jika Anda menjalankan ini di kumpulan utas , Anda akan menemui jalan buntu . Pertanyaannya adalah, mengapa jika kita tidak menulis apa pun secara serempak?



Kami membuat TaskCompletionSource, memulai tugas baru, dan kami memiliki utas kedua yang memulai sesuatu dalam tugas ini. Ia pergi dan jatuh ke dalam harapan selama seratus milidetik. Lalu utas utama kami - hijau - menunggu dan hanya itu. Dia melepaskan tumpukan, tumpukan hang, menunggu untuk dipanggil dalam kelanjutantask.Waitsaat tcsterpapar.

Di utas biru kita sampai tcs, dan kemudian yang paling menarik. Berdasarkan pertimbangan internal .NET, ia TaskCompletionSourcepercaya bahwa kelanjutan dari ini tcsdapat dilakukan secara serempak, yaitu, langsung pada tumpukan yang sama, maka ini task.Waitdilakukan secara serempak pada tumpukan yang sama. Ini sangat aneh, meskipun faktanya kita belum menulis di mana pun ExecuteSynchronously. Ini mungkin masalah dengan pencampuran kode sinkron dan asinkron.



Masalah lain dengan ini TaskCompletionSourceadalah bahwa ketika kami memanggil di SetResultbawah kunci , Anda tidak dapat memanggil kode arbitrer, karena di bawah kunci Anda hanya dapat melakukan beberapa aktivitas granular kecil. Jalankan di bawah beberapa aksi-s, mustahil datang dari tempat asalnya. Bagaimana cara mengatasi masalah ini?

var  tcs  =  new   TaskCompletionSource<int>(
       TaskContinuationsOptions.RunContinuationsAsynchronously  
) ;
lock(mylock)
{  
    tcs.SetResult(O); 
});

TaskCompletionSourceLayak digunakan hanya untuk adaptasi kode Tugas tidak di perpustakaan. Hampir semuanya bisa diselesaikan melalui menunggu. Dalam hal ini, selalu sangat disarankan untuk meresepkan parameter "TaskCompletionSource.RunContinuationsAsynchronously" . Anda hampir selalu perlu menjalankan kelanjutan secara tidak sinkron. Dalam hal ini, Anda tcs.SetResultmemiliki sesuatu yang tidak dapat meluncurkan apa pun.



Mengapa kelanjutan harus dilakukan secara serempak? Karena RunContinuationsAsynchronouslymengacu pada yang berikut ContinueWith, dan bukan milik kita. Agar dia berhubungan dengan kita, Anda perlu menulis yang berikut:



Contoh ini menunjukkan bagaimana parameter tidak intuitif, bagaimana mereka bersinggungan satu sama lain, bagaimana mereka memperkenalkan kompleksitas kognitif - sangat sulit untuk menulis.

Hirarki orangtua-anak


Task.Factory.StartNew(() => 
{
  //... some parent activity

   Task.Factory.StartNew(() => {
      //... some child activity
   })

})
.ContinueWith(...) // don’t wait for child

Ada opsi lain untuk menggunakan parameter. Misalnya, hierarki Orangtua-anak muncul ketika Anda meluncurkan satu tugas dan menjalankan yang lain di bawahnya. Dalam hal ini, jika Anda menulis ContinueWith, Anda ContinueWithtidak akan menunggu tugas diluncurkan di dalam.



Jika Anda menulis TaskCreationOptions.AttachedToParent, itu ContinueWithakan menunggu. Anda dapat menggunakan properti ini dalam produk Anda. Saya pikir semua orang dapat memberikan contoh di mana ada hierarki tugas, dengan tugas menunggu subtugas, dan subtugas untuk subtugasnya. Tidak perlu menulis di mana pun WaitForChildren, tunggu ini terjadi secara serempak. Artinya, tubuh tugas orang tua berakhir, dan setelah itu tugas orang tua tidak dianggap selesai, tidak memulai kelanjutannya sampai tugas anak berhasil.

Task.Factory.StartNew(() => 
{
  //... some parent activity
  Foo(); 

})
.ContinueWith(...) // still wait for child

void Foo() { 
   Task.Factory.StartNew(() => {
      //... parent task to attach is in ThreadStatic
   }, TaskCreationOptions.AttachedToParent); 
}

Mungkin ada masalah saat tugas ditransfer ke suatu tempat ThreadStatic, maka semua yang Anda mulai AttachedToParentakan ditambahkan ke tugas induk ini, yang merupakan bel alarm.

Task.Factory.StartNew(() => 
{
  //... some parent activity

  Foo();
}, TaskCreationOptions.DenyChildAttach)
.ContinueWith(...) // don’t wait for child

void Foo() { 
   Task.Factory.StartNew(() => {
      //... some child activity
   }, TaskCreationOptions.AttachedToParent); 
}

Di sisi lain, ada opsi yang membatalkan opsi sebelumnya DenyChildAttach. Aplikasi semacam itu sering terjadi.

Task.Run(() => 
{
  //... some parent activity

  Foo(); 

})
.ContinueWith(...) //don’t wait for child

void Foo() { 
   Task.Factory.StartNew(() => {
      //... some child activity
    }, TaskCreationOptions.AttachedToParent); 
}

Perlu diingat bahwa Task.Runini adalah cara standar untuk memulai, yang secara default menyiratkan DenyChildAttach.

Konteks implisit yang Anda masukkan ThreadStaticmenambah kompleksitas bagi Anda. Anda tidak mengerti bagaimana tugas itu bekerja, karena Anda perlu tahu konteksnya. Masalah lain yang mungkin timbul terkait dengan status siaga async / menunggu. Itu karena di async / menunggu Anda tidak memiliki tugas, tetapi tindakan. Kelanjutan bukanlah tugas yang jujur, tetapi tindakan. Saat Anda menulis kode async / AttachedToParentwait , Anda tidak perlu menggunakannya karena Anda secara eksplisit mengikat tugas-tugas untuk menunggu, dan ini adalah pendekatan yang tepat.



Anda memiliki enam opsi tentang cara memulai kelanjutan. Anda meluncurkan tugas, diluncurkanContinueWith. Pertanyaan: Apa status kelanjutan ini? Ada lima kemungkinan jawaban:

  • kelanjutan umum akan selesai dengan sukses, RunToCompletion akan terjadi;
  • tugas akan salah;
  • pembatalan akan terjadi;
  • tugas tidak akan mencapai penyelesaian sama sekali, itu akan menjadi semacam limbo;
  • opsi - "tergantung".



Dalam hal ini, tugas akan berada dalam status "dibatalkan", meskipun tidak ada kata "dibatalkan" di mana pun. Di sini kita mengadakan resepsi dan tidak melakukan apa pun. Masalahnya adalah ketika Anda membaca kode orang lain dengan banyak opsi - bahkan jika Anda tahu tentang opsi ini 10 menit yang lalu - Anda masih lupa apa yang terjadi di sini. Jadi jangan menulis.

Pembatalan



Task.Factory.StartNew(() => 
{
    throw new OperationCanceledException(); 
});

                                                      Failed

Parameter ketiga di awal tugas adalah kancellation. Anda menulis OperationCanceledException, yaitu, tindakan khusus yang menempatkan tugas dalam status "Dibatalkan". Dalam hal ini, tugas akan berada dalam status "Gagal", karena tidak semua OperationCanceledExceptionsama.

Task.Factory.StartNew(() => 
{
    throw new OperationCanceledException(cancellationToken); 
}, cancellationToken);

                                                      Canceled

Agar tugas dapat dilakukan Canceled, Anda harus membuangnya OperationCanceledExceptionbersama dengan PembatalanTokennya. Pada kenyataannya, Anda tidak pernah secara eksplisit melakukan ini, tetapi lakukan seperti ini:

Task.Factory.StartNew(() => 
{
    cancellationToken.ThrowIfCancellationRequested(); 
}, cancellationToken);
                                                       Canceled

Apakah perlu untuk membedakan cancellationToken? Di suatu tempat di dalam tugas, Anda memeriksa bahwa seseorang menghapus Anda: membuang pembatalan lemparan, lalu tugas tersebut masuk ke status Canceled. Atau seseorang mengklik "Batalkan" pada waktu berjalan dan membatalkan tugas. Latihan kami di JetBrains menunjukkan bahwa Anda tidak perlu membedakan antara token ini. Jika Anda mendapatkan OperationCanceledException - jenis khusus yang terjadi ketika beberapa pembatalan telah terjadi, Anda dapat membedakannya. Dalam hal ini, Anda hanya perlu menyelesaikan tugas secara normal, jangan masuk, dan ketika Anda menerima eksekusi - masuk.

Tumpukan yang dalam


Task.Factory.StartNew(() => 
{
    Foo();
}, cancellationToken);

  void Foo() { 
     Bar() {
       ...
          Baz() {
             //how to get cancellation token?
          } 
    }
}

Katakanlah Anda memiliki tumpukan yang dalam. Ini CancellationTokenadalah satu-satunya parameter eksplisit yang kami diskusikan. Ini harus ditransmisikan ke mana-mana melalui semua hierarki. Apa yang harus saya lakukan jika, di hadapan hierarki yang mendalam, Anda perlu membatalkan tugas Anda di suatu tempat, pada tingkat yang paling rendah, untuk membuang resepsi? Ada trik khusus yang kami gunakan. Dia dipanggil AsyncLocal.

static AsyncLocal<Cancelation> asyncLocalCancellation;

Task.Factory.StartNew(() => 
{
     asyncLocalCancellation.Set(cancellationToken) 
    Foo();
}, cancellationToken); // use AsyncLocal to put cancellation int

  void Foo() { 
     async Bar() {
      ...
         Baz() {
             asyncLocalCancellation.Value.CheckForInterrupt(); 
         }
   } 
}

Ini sama dengan, ThreadStatichanya yang spesial ThreadLocalyang bertahan dari async / menunggu perjalanan kode. Karena kode Anda asinkron, dan Anda memiliki kancellation ini, Anda memasukkannya AsyncLocal, dan di suatu tempat pada level yang lebih dalam Anda dapat mengatakan " CheckForInterrupt Throw If Cancellation Requested". Sekali lagi, ini adalah satu-satunya parameter CancellationTokenyang perlu sepenuhnya mengolesi seluruh kode, tetapi, menurut pendapat saya, untuk sebagian besar tugas Anda hanya perlu tahu apa yang terjadi OperationCanceledException, dan dari sini menarik kesimpulan yang menyatakan: Dibatalkan atau Gagal.

Kompleksitas kognitif


Task.Factory.StartNew(Action, 
    TaskCreationOptions, 
    TaskScheduler, 
    CancellationToken
)
                                                   JetBrains.Lifetimes

lifetime.Start(TaskScheduler, Action) //puts lifetime in AsyncLocal

lifetime.StartMainRead(Action) 
lifetime.StartMainWrite(TaskScheduler, Action) 
lifetime.StartBackgroundRead(TaskScheduler, Action)

Semakin sulit kode dibaca saat memulai tugas, semakin tinggi risiko kesalahan. Melihat kode setelah satu tahun, Anda akan lupa apa yang dilakukannya, karena ada sejumlah besar parameter. Tetapi kami memiliki pustaka JetBrains.Lifetimes , yang menawarkan masa hidup modern, CancellToken yang dioptimalkan dengan baik, yang dengannya metode Start ditulis ulang dan masalah dengan pengulangan potongan kode dipecahkan, seperti dengan Task.Factory.StartNewdan TaskCreationOptions.

Ada sejumlah kecil penjadwal yang memungkinkan Anda untuk menjadwalkan tugas pada utas utama dengan kunci baca. Artinya, baca kunci bukanlah sesuatu yang Anda pilih secara eksplisit, itu adalah penjadwal khusus yang menjadwalkan kode Anda pada utas utama dengan kunci baca, serta utas utama dengan kunci tulis, utas latar belakang - dan sekarang metode menjadi sangat sederhana untuk memulai shuffle. Pada saat yang sama, masa hidup secara otomatis membatalkan AsyncLocal, menyederhanakan kode secara signifikan.



Mari kita lihat bagaimana cara menyinkronkan masalah ini, dan masalah apa yang mereka perkenalkan.

Dalam contoh ini, bagian dari kode dieksekusi secara sinkron, kemudian menunggu dan kode asinkron. Pertama, itu baik bahwa ada potongan kode berulang yang jauh lebih sedikit ( boiler-plate ). Kedua, bagus bahwa kode asinkron sangat mirip dengan kode sinkron, inilah gunanya async / menunggu . Anda dapat menulis secara tidak sinkron dengan cara yang sama seperti Anda menulis secara sinkron, tanpa mengambil utas.

Dalam hal apa kompiler akan digunakan? Kode sinkron akan dieksekusi secara sinkron, setelah itu tugas InnerAsyncakan dijalankan secara sinkron , dari mana objek GetAwaiter khusus berasal. Dalam hal ini, kami tertarik TaskAwaiter. Anda dapat menulis penunggu untuk objek apa pun. Akibatnya, kami menunggu tugas untuk menyelesaikan InnerAsyncdan secara sinkron melaksanakannya continuationCode. Jika tugas tidak selesai, maka kode lanjutan dijadwalkan pada penjadwal konteks . Mungkin saja, meskipun Anda menulis menunggu , semuanya pasti akan dipanggil secara serempak.

async Task MyFuncAsync() { 
  synchronousCode();
   await InnerAsync();
   await Task.Yield(); //guaranteed !IsCompleted 
   continuationCode();
}

Ada satu trik Task.Yield- ini adalah tugas khusus yang memastikan bahwa penantunya tidak akan selalu kembali kepada Anda IsCompleted. Dengan demikian, continuationitu tidak akan dipanggil secara serempak di tempat ini. Untuk utas UI, ini bisa menjadi penting karena Anda tidak menggunakan utas ini untuk waktu yang lama.



Bagaimana memilih utas untuk kelanjutan? Filosofi async / await adalah ini: Anda menulis kode asinkron sama dengan sinkron. Jika Anda memiliki kumpulan utas , tidak ada bedanya bagi Anda - continuationCode akan dieksekusi di utas lainnya. Terlepas dari apakah itu InnerAsyncselesai ketika Anda mengatakan menunggu atau tidak, Anda perlu segalanya untuk dijalankan di utas UI.

Mekanisme untuk tugas menunggu adalah sebagai berikut: itu diambil static, itu disebutSynchronizationContextdan darinya diciptakan TaskScheduler. SynchronizationContext adalah sesuatu dengan metode Post, yang sangat mirip dengan metode ini Queue. Bahkan TaskScheduler, yang sebelumnya, hanya mengambil SynchronizationContextdan melalui Post melakukan tugasnya di atasnya.

async Task MyFuncAsync() { 
  synchronousCode();

    await InnerAsync().ConfigureAwait(false);
    continuationCode(); 
}

Ada cara untuk mengubah perilaku ini menggunakan parameter ContinueOnCapturedContext. API paling menjijikkan yang ada di .NET disebut ConfigureAwait. Dalam hal ini, API membuat penunggu khusus yang berbeda dari TaskAwaiteryang menggeser kelanjutan, itu berjalan di utas yang sama, dalam konteks yang sama di mana metode berakhir InnerAsync dan di mana tugas berakhir.

async Task MyFuncAsync() { 
  synchronousCode();

    await InnerAsync().ConfigureAwait(continueOnCapturedContext: false); 
    continuationCode(); //code must be absolutely context-agnostic
}

Ada sejumlah saran gila di Internet: jika Anda menemui jalan buntu , mohon apakan semua kode ConfigureAwait Anda dan semuanya akan baik-baik saja. Ini cara yang salah. ConfigureAwaitdapat digunakan dalam kasus di mana Anda ingin sedikit meningkatkan kinerja, atau di akhir metode, di beberapa metode pustaka.

Jalan buntu


async Task MyFuncAsync() { //UI thread 
  synchronousCode();

    await Task.Delay(10).ConfigureAwait(continueOnCapturedContext: false); 
    continuationCode();
}
myFuncAsync().Wait() //on UI thread

Ini adalah kebuntuan klasik . Di utas UI, mereka menunggu sepuluh detik dan melakukannya Wait. Karena apa yang telah Anda lakukan Wait, itu continuationCodetidak akan pernah diluncurkan, Waitoleh karena itu , tidak akan pernah kembali. Semua itu terjadi di awal.

async Task OnBluttionClick() { //UI thread 
  int v = Button.Text.ParseInt();

    await Task.Delay(10).ConfigureAwait(continueOnCapturedContext: false); 
  Button.Text.Set((v+1).ToString());
}
myFuncAsync().Wait() //on UI thread

Bayangkan ini adalah aktivitas nyata. Kami mengklik tombol, mengambilnya Button.ParseInt, menunggu , menulis, ConfigureAwaitKami berkata: "Tolong jangan tutup aliran UI kami, lakukan kelanjutan." Masalahnya adalah bahwa kita ingin bagian kedua setelah ConfigureAwaitjuga dieksekusi di utas UI, karena ini adalah filosofi menunggu . Artinya, kode asinkron Anda terlihat sama dengan kode sinkron, dan berjalan dalam konteks yang sama. Dalam hal ini, tentu saja, akan ada kesalahan. Dan selain Button.Text.Setitu bisa ada sejumlah panggilan metode yang juga mengasumsikan konteksnya. Apa yang harus dilakukan dalam situasi ini? Kamu bisa melakukan ini:

async Task MyFuncAsync() { //UI thread 
  synchronousCode();

    await Task.Delay(10).ConfigureAwait(continueOnCapturedContext: false); 
    continuationCode(); //The same UI context
}
PumpUntil(() => task.IsCompleted);
//VS synchronization contexts always pump on any Wait

Dengan utas UI, Anda harus melarang melakukannya Waitpada utas yang memiliki antrian pesan umum. Alih-alih melakukan Waitatau menulis ConfigureAwait, Anda dapat memompa antrian pesan ini, dan pada saat yang sama, kontinum juga akan dipompa. Jika Anda tidak dapat mencampur kode sinkron dan asinkron, maka Anda tidak boleh mencampurnya. Namun terkadang ini tidak bisa dihindari.

Misalnya, Anda memiliki kode lama, dan Anda harus mencampurnya, lalu Anda memompa aliran UI. Visual Studio memompa utas UI sesuai harapan, bahkan SynchronizationContextsedikit berubah. Jika Anda masuk ke WaitHandle di mana saja Wait, maka ketika Anda hang, aliran UI Anda dipompa. Dengan demikian, mereka memilih antara kebuntuan dan ras demi ras.

Pumpuntil- Ini adalah API yang tidak ideal, yaitu, ketika Anda melakukan kontinuitas acak di tempat yang sewenang-wenang, mungkin ada nuansa. Sayangnya, tidak ada cara lain. Campur kode sinkron dan asinkron. Jika ada, seluruh Penunggang begitu diatur di tempat-tempat lama, jadi kadang-kadang ada nuansa juga.

Ubah konteks


async Task MyFuncAsync() { 
  synchronousCode(); // on initial context

    await myTaskScheduler;
    continuationCode(); //on scheduler context 
}

Ada cara lain yang menarik untuk menggunakan async / menunggu . Anda dapat menulis Awaitertentang schedulerdan melompat pada benang. Saya membaca posting di Visual Studio, mereka menulis untuk waktu yang sangat lama bahwa tidak baik untuk bolak-balik di tengah metode, tetapi sekarang mereka melakukannya sendiri. Visual Studio memiliki API yang melompat pada utas melalui penjadwal. Untuk penggunaan normal, melakukan ini tidak baik.

Konkurensi terstruktur


async Task MyFuncAsync() { 
  synchronousCode(); // on initial context

    await Task.Factory.StartNew(() => {...}, myTaskScheduler);
    continuationCode(); //on initial context 
}

Untuk perendaman yang nyaman dalam konteks baru dan kembali ke yang lama, beberapa kompetisi struktural, atau paralelisme struktural, harus dibangun. Misalnya, pada tahun enam puluhan, operator GoTo dianggap berbahaya karena melanggar strukturalitas. Jadi di sini. Melompat dengan benang melanggar struktur. Anehnya, menggunakan mesin negara async sepertinya jalan keluar yang bagus. Yaitu, di mana struktur biasa Anda dilanggar, Anda melompat di GoTo, Anda dapat melanggar struktur utas: jangan menunggu , campur dengan tag. Ini adalah situasi yang sangat aneh dan langka ketika Anda perlu melakukan ini. Namun, lebih baik ketika menunggu kembali ke konteks yang sama. Dengan demikian, kumpulan utas tidak akan memiliki utas yang sama, tetapi konteks yang sama seperti aslinya.

Perilaku berurutan


Mengapa menunggu tidak sama dengan eksekusi paralel? Menunggu eksekusi adalah eksekusi berurutan. Dalam hal ini, kami memulai tugas pertama, tunggu, mulai tugas kedua - kami menunggu. Kami tidak memiliki paralelisme. Untuk sebagian besar kegunaan, paralelisme tidak diperlukan. Paralelisme itu sendiri lebih kompleks daripada urutan. Kode serial lebih sederhana daripada paralel, ini adalah aksioma. Tetapi kadang-kadang Anda perlu menjalankan sesuatu dalam kode paralel, dan Anda melakukannya seperti ini:

async Task MyAsync() {

  var task1 = StartTask1Async();
  await task1;

  var task2 = StartTask2Async();
  await task2; 
}

Perilaku serentak


async Task MyAsync() {
  var task1 = StartTask1Async();
  var task2 = StartTask2Async();

  await task1;
  await task2; 
}

Di sini tugas dimulai secara paralel. Jelas bahwa metode dapat mengembalikan tugas segera dalam keadaan berjalan, maka tidak akan ada paralelisme. Misalkan saja kedua tasky melakukan eksekusi. Dan Anda menunggu tugas pertama, lalu pada menunggu pertama berangkat. Artinya, segera setelah Anda menulis await task1, Anda lepas landas dan tidak memproses exception task2. Menariknya, ini adalah kode yang benar-benar valid. Dan kode inilah yang menyebabkan .NET pada fakta bahwa dalam versi 4.5 perilaku bekerja dengan eksekusi telah berubah.

Penanganan pengecualian


async Task MyAsync() {
  var task1 = StartTask1Async();
  var task2 = StartTask2Async(); 

  await task1;
  await task2;

  // if task1 throws exception and task2 throws exception we only throw and
  // handle task1’s exception

  //4.0 -> 4.5 framework: unhandled exceptions now don’t crush process
  //still visible in UnobservedExceptionHandler
}

Sebelumnya, eksekusi tidak tertangani hanya membuang proses, dan jika Anda tidak menangkap beberapa eksekusi UnobservedExceptionHandler(ini juga beberapa staticyang dapat Anda lampirkan pada penjadwal), maka proses ini tidak dieksekusi. Sekarang ini adalah kode yang benar-benar valid. Meskipun .NET mengubah perilakunya, tetap mempertahankan pengaturan untuk mengembalikan perilaku ke arah yang berlawanan.

async  Task  MyAsync(CancellationToken cancellationToken)  {  

  await  SomeTask1  Async(cancellationToken); 
 
  await  Some Task2Async( cancellation  Token); 
  //you should always pass use async API with cancelationToken  if possible 
} 
  
try { 
    await  MyAsync( cancellation  Token); 
} catch (OperationException e) { // do nothing: OCE happened
} catch (Exception e) { 
    log.Error(e);
}

Lihat bagaimana pemrosesan eksekusi berjalan. PembatalanToken-s harus ditransmisikan, perlu untuk "smear" PembatalanToken-s semua kode. Perilaku normal async adalah Anda tidak memeriksa di mana pun Task.Status ancellationToken, Anda bekerja dengan kode asinkron dengan cara yang sama seperti dengan sinkron. Yaitu, dalam kasus pembatalan, Anda mendapatkan eksekusi, dan dalam kasus ini, Anda tidak melakukan apa pun ketika Anda menerimanya OperationCanceledException.

Perbedaan antara status Dibatalkan dan Kesalahan adalah bahwa Anda tidak menerima OperationCanceledException, tetapi eksekusi biasa. Dan dalam hal ini, kita bisa menjanjikannya, Anda hanya perlu mendapatkan eksekusi dan menarik kesimpulan berdasarkan ini. Jika Anda memulai tugas secara eksplisit, melalui Tugas, Anda akan diterbangkan AggregateException. Dan di async, dalam kasus ini mereka AggregateExceptionselalu membuang eksekusi pertama yang ada di dalamnya (dalam hal ini - OperationCanceled).

Dalam praktek


Metode sinkron


DataTable<File, ProcessedFile> sharedMemory;

// in any thread
void SynchronousWorker(...) {
  File f = blockingQueue.Dequeue(); 
  ProcessedFile p = ProcessInParallel(f);

  lock (_lock) { 
    sharedMemory.add(f, p);
  } 
}

Misalnya, setan berfungsi di ReSharper - editor yang memberi warna pada file untuk Anda. Jika file dibuka di editor, maka ada beberapa aktivitas yang menempatkannya dalam antrian pemblokiran. Proses kami workermembaca dari sana, setelah itu melakukan banyak tugas yang berbeda dengan file ini, mewarnai, mem-parsing, membangun, setelah itu file-file ini ditambahkan sharedMemory. Dengan sharedMemorykunci, mekanisme lain sudah bekerja dengannya.

Metode asinkron


Saat menulis ulang kode menjadi asinkron, pertama-tama kita akan menggantinya voiddengan async Task. Pastikan untuk menulis kata "Async" di akhir. Semua metode asinkron harus diakhiri dengan Async - ini adalah konvensi.

DataTable<File, ProcessedFile> sharedMemory;
// in any thread
async Task WorkerAsync(...) {

  File f = blockingQueue.Dequeue(); 

  ProcessedFile p = ProcessInParallel(f);

  lock (_lock) { 
    sharedMemory.add(f, p);
  } 
}

Setelah itu, Anda perlu melakukan sesuatu dengan kami blockingQueue. Jelas, jika ada beberapa primitif sinkron, maka harus ada beberapa primitif asinkron.



Primitif ini disebut saluran: saluran yang hidup dalam paket System.Threading.Channels. Anda dapat membuat saluran dan antrian, terbatas dan tidak terbatas, yang dapat Anda tunggu secara tidak sinkron. Selain itu, Anda dapat membuat saluran dengan nilai "nol", artinya tidak memiliki buffer sama sekali. Saluran semacam itu disebut saluran pertemuan dan secara aktif dipromosikan di Go dan Kotlin. Dan pada prinsipnya, jika dimungkinkan untuk menggunakan saluran dalam kode asinkron, ini adalah pola yang sangat bagus. Artinya, kami mengubah antrian ke saluran tempat ada metode ReadAsyncdan WriteAsync.

ProcessInParallel adalah sekelompok kode paralel yang melakukan pemrosesan file dan mengubahnya menjadiProcessedFile. Bisakah async membantu kita menulis bukan asinkron, tetapi kode paralel lebih kompak?

Sederhanakan Kode Paralel


Kode dapat ditulis ulang dengan cara ini:

DataTable<File, ProcessedFile> sharedMemory;

// in any thread
async Task WorkerAsync(...) {

  File f = await channel.ReadAsync();

  ProcessedFile p = await ProcessInParallelAsync(f);

  lock (_lock) { 
    sharedMemory.add(f, p);
  } 
}



Mereka seperti apa ProcessInParallel? Sebagai contoh, kami memiliki file. Pertama, kita memecahnya menjadi leksem, dan kita dapat memiliki dua tugas secara paralel: membangun cache pencarian dan membangun pohon sintaksis. Setelah itu muncul tugas "mencari kesalahan semantik." Penting di sini bahwa semua tugas ini membentuk grafik asiklik terarah. Artinya, Anda dapat menjalankan beberapa bagian dalam utas paralel, sebagian tidak bisa, dan jelas ada ketergantungan tugas mana yang harus menunggu untuk tugas lain. Anda mendapatkan grafik dari tugas-tugas tersebut, Anda ingin entah bagaimana menyebarkannya di sepanjang utas. Apakah mungkin untuk menulisnya dengan indah, tanpa kesalahan? Dalam kode kami, masalah ini diselesaikan beberapa kali, setiap kali dengan cara yang berbeda. Jarang terjadi ketika kode ini ditulis tanpa kesalahan.



Kami mendefinisikan grafik tugas ini sebagai berikut: misalkan setiap tugas memiliki tugas lain yang bergantung padanya, kemudian menggunakan kamus ExecuteBefore kami menulis kerangka metode kami.

Solusi kerangka


Dictionary<Action<ProcessedFile>, Action<ProcessedFile>[]> ExecuteBefore; async Task<ProcessedFile> ProcessInParallelAsync() {
  var res = new ProcessedFile();


  // lots of work with toposort, locks, etc.

  return res; 
}

Jika Anda menyelesaikan masalah ini secara langsung, maka Anda perlu melakukan pengurutan topologi dari grafik ini. Kemudian ambil tugas yang tidak memiliki tugas yang tergantung, jalankan, analisis struktur di bawah kunci, lihat tugas mana yang tidak memiliki yang tergantung. Jalankan, sebarkan mereka entah bagaimana Task Runner. Kami menulisnya sedikit lebih kompak: pengurutan topologi dari grafik + pelaksanaan tugas-tugas tersebut pada utas yang berbeda.

Async malas


Dictionary<Action<ProcessedFile>, Action<ProcessedFile>[]> ExecuteBefore;
async Task<ProcessedFile> ProcessInParallelAsync() {
  var res = new ProcessedFile();
  var lazy = new Dictionary<Action<ProcessedFile>, Lazy<Task>>(); 
  foreach ((action, beforeList) in ExecuteBefore)
    lazy[action] = new Lazy<Task>(async () => 
    {
      await Task.WhenAll(beforeList.Select(b => lazy[b].Value)) 
      await Task.Yield();
      action(res);
}
  await Task.WhenAll(lazy.Values.Select(l => l.Value)) 
  return res;
}

Ada pola yang disebut Async Lazy. Kami membuat milik kami ProcessedFiledi mana berbagai tindakan harus dijalankan. Mari kita membuat kamus: kita akan memformat setiap tahap kita (Action ProcessedFile) menjadi beberapa Tugas, atau lebih tepatnya, ke Malas dari Tugas dan berjalan di sepanjang grafik asli. Variabel actionakan memiliki tindakan itu sendiri , dan di beforeList - tindakan-tindakan yang harus dilakukan sebelum kita. Lalu buat Lazydari action. Kami menulis di Tugas await. Dengan demikian, kami menunggu semua tugas yang harus diselesaikan sebelum itu. Di beforeList, pilih Lazyyang ada di kamus ini.

Harap perhatikan bahwa di sini tidak ada yang akan dijalankan secara sinkron, sehingga kode ini tidak akan jatuh ItemNotFoundException in Dictionary. Kami melakukan semua tugas yang ada di hadapan kami, melakukan pencarian dengan tindakanLazy Task. Lalu kami melakukan tindakan kami. Pada akhirnya, Anda hanya perlu meminta setiap tugas untuk memulai, jika tidak, Anda tidak akan pernah tahu jika sesuatu tidak dimulai. Dalam hal ini, tidak ada yang dimulai. Ini solusinya. Metode ini ditulis dalam 10 menit, sangat jelas.

Dengan demikian, kode asinkron membuat keputusan kami, awalnya ia menempati beberapa layar dengan kode kompetitif yang kompleks. Di sini dia sangat konsisten. Saya bahkan tidak menggunakannya ConcurrentDictionary, saya menggunakan yang biasa Dictionary, karena kami tidak menulis apa pun untuk itu secara kompetitif. Ada kode yang konsisten dan konsisten. Kami memecahkan masalah penulisan kode paralel menggunakan async-s dengan indah, yang berarti - tanpa bug.

Singkirkan kunci


DataTable<File, ProcessedFile> sharedMemory;

// in any thread
async Task WorkerAsync(...) {

  File f = await channel.ReadAsync();

  ProcessedFile p = await ProcessInParallelAsync(f);

    lock (_lock) {
      sharedMemory.add(f, p);
   }
 }

Apakah perlu menarik async dan kunci-kunci ini? Sekarang ada semua jenis kunci async, async semaphores, yaitu upaya untuk menggunakan primitif yang ada dalam kode sinkron dan asinkron. Konsep ini tampaknya salah, karena dengan kunci Anda melindungi sesuatu dari eksekusi paralel. Tugas kami adalah menerjemahkan eksekusi paralel menjadi berurutan, karena lebih mudah. Dan jika lebih sederhana - lebih sedikit kesalahan.

Channel<Pair<File, ProcessedFile>> output;
// in any thread
async Task WorkerAsync(...) {

  File f = await channel.ReadAsync();

  ProcessedFile p = await ProcessInParallelAsync(f);
  
  await output.WriteAsync(); 
}

Kita dapat membuat beberapa saluran dan meletakkan di sana beberapa File dan ProcessedFile, dan ReadAsyncbeberapa prosedur lain akan memproses saluran ini , dan itu akan melakukannya secara berurutan. Mengunci sendiri, selain melindungi struktur, pada dasarnya membuat akses linier, tempat semua utas dari yang berurutan menjadi paralel. Dan kami mengganti ini secara eksplisit dengan saluran.



Arsitekturnya adalah sebagai berikut: pekerja menerima file dari inputdan mengirimnya ke suatu tempat ke prosesor, yang juga memproses semuanya secara berurutan, tidak ada paralelisme. Kode terlihat lebih sederhana. Saya mengerti bahwa tidak semuanya bisa dilakukan dengan cara ini. Arsitektur seperti itu, ketika Anda bisa membangun pipa data, tidak selalu berhasil.



Mungkin saja Anda memiliki saluran kedua yang masuk ke prosesor Anda dan bukan grafik berarah asiklik yang terbentuk dari saluran, tetapi grafik dengan siklus. Ini adalah contoh yang dikatakan Roman Elizarov kepada KotlinConf pada tahun 2018. Dia menulis contoh di Kotlin dengan saluran-saluran ini, dan ada siklus di sana, dan contoh ini ditutup. Masalahnya adalah jika Anda memiliki siklus seperti itu dalam grafik, maka semuanya menjadi lebih rumit di dunia asinkron. Kebuntuan asinkron buruk karena jauh lebih sulit dipecahkan daripada sinkron ketika Anda memiliki setumpuk utas, dan jelas apa yang terjadi. Oleh karena itu, ini adalah alat yang harus digunakan dengan benar.

Ringkasan


  • Hindari sinkronisasi dalam kode asinkron.
  • Kode serial lebih sederhana daripada paralel.
  • Kode asinkron bisa sederhana dan menggunakan parameter minimum dan konteks implisit yang mengubah perilakunya.

Jika Anda telah mengembangkan kebiasaan menulis kode sinkron, dan meskipun kode asinkron sangat mirip dengan kode sinkron, Anda tidak perlu menyeret primitif di sana, yang biasa Anda gunakan dalam kode sinkron async mutex. Gunakan feed, jika mungkin, dan primitif pesan yang lewat lainnya .

Kode serial lebih sederhana daripada paralel. Jika Anda dapat menulis arsitektur Anda sehingga terlihat berurutan, tanpa menjalankan kode paralel dan mengunci, maka tulis arsitektur secara berurutan.

Dan hal terakhir yang kami lihat dari sejumlah besar contoh dengan tugas. Saat Anda mendesain sistem Anda, cobalah untuk tidak terlalu bergantung pada konteks implisit. Konteks implisit mengarah pada kesalahpahaman tentang apa yang terjadi dalam kode, dan Anda bisa melupakan masalah tersirat dalam setahun. Dan jika orang lain mengerjakan kode ini dan mengulang sesuatu di dalamnya, ini dapat menyebabkan kesulitan yang pernah Anda ketahui, dan programmer baru tidak tahu karena konteks yang tersirat. Akibatnya, desain yang buruk dicirikan oleh sejumlah besar parameter, kombinasi dan konteks implisit.

Apa yang harus dibaca



-10 . DotNext .

All Articles