.NET: Alat untuk bekerja dengan multithreading dan asynchrony. Bahagian 1

Saya menerbitkan artikel asal mengenai Habr, yang terjemahannya disiarkan dalam korporat post blog.

Keperluan untuk melakukan sesuatu secara tidak segerak, tanpa menunggu hasilnya di sini dan sekarang, atau untuk membahagikan kerja besar antara beberapa unit yang melaksanakannya, wujud sebelum kemunculan komputer. Dengan kedatangan mereka, keperluan ini menjadi sangat ketara. Sekarang, pada tahun 2019, saya menaip artikel ini pada komputer riba dengan pemproses Intel Core 8-teras, di mana lebih daripada seratus proses berjalan selari, dan lebih banyak lagi benang. Berdekatan, terdapat telefon yang sedikit lusuh, dibeli beberapa tahun lalu, ia mempunyai pemproses 8 teras di atasnya. Sumber tematik penuh dengan artikel dan video di mana pengarangnya mengagumi telefon pintar utama tahun ini yang menampilkan pemproses 16 teras. MS Azure menyediakan mesin maya dengan pemproses teras 20 dan RAM 128 TB dengan harga kurang daripada $2/jam. Malangnya, adalah mustahil untuk mengekstrak maksimum dan memanfaatkan kuasa ini tanpa dapat mengurus interaksi benang.

Terminologi

Proses - Objek OS, ruang alamat terpencil, mengandungi benang.
Benang - objek OS, unit terkecil pelaksanaan, sebahagian daripada proses, benang berkongsi memori dan sumber lain di antara mereka dalam proses.
Pelbagai tugas - Harta OS, keupayaan untuk menjalankan beberapa proses secara serentak
Berbilang teras - sifat pemproses, keupayaan untuk menggunakan beberapa teras untuk pemprosesan data
Pemprosesan berbilang - harta komputer, keupayaan untuk bekerja secara serentak dengan beberapa pemproses secara fizikal
Multithreading — sifat sesuatu proses, keupayaan untuk mengedarkan pemprosesan data antara beberapa utas.
Paralelisme - melakukan beberapa tindakan secara fizikal serentak setiap unit masa
Asynchrony — pelaksanaan operasi tanpa menunggu selesai pemprosesan ini; hasil pelaksanaan boleh diproses kemudian.

Metafora

Tidak semua definisi bagus dan ada yang memerlukan penjelasan tambahan, jadi saya akan menambah metafora tentang memasak sarapan pagi kepada istilah yang diperkenalkan secara rasmi. Memasak sarapan dalam metafora ini adalah satu proses.

Semasa menyediakan sarapan pagi saya (CPU) Saya datang ke dapur (komputer). Saya mempunyai 2 tangan (Cores). Terdapat beberapa peranti di dapur (IO): ketuhar, cerek, pembakar roti, peti sejuk. Saya menghidupkan gas, meletakkan kuali di atasnya dan menuang minyak ke dalamnya tanpa menunggu ia panas (secara tidak segerak, Tidak Menyekat-IO-Tunggu), saya keluarkan telur dari peti ais dan pecahkan ke dalam pinggan, kemudian pukul dengan sebelah tangan (Benang #1), dan kedua (Benang #2) memegang pinggan (Sumber Dikongsi). Sekarang saya ingin menghidupkan cerek, tetapi saya tidak mempunyai cukup tangan (Kelaparan Benang) Pada masa ini, kuali menjadi panas (Memproses hasilnya) di mana saya tuangkan apa yang telah saya sebat. Saya mencapai cerek dan menghidupkannya dan bodoh melihat air mendidih di dalamnya (Menyekat-IO-Tunggu), walaupun pada masa ini dia boleh mencuci pinggan tempat dia menyebat telur dadar.

Saya memasak telur dadar hanya menggunakan 2 tangan, dan saya tidak mempunyai lebih banyak, tetapi pada masa yang sama, pada masa sebat telur dadar, 3 operasi berlaku serentak: sebat telur dadar, memegang pinggan, memanaskan kuali. CPU adalah bahagian terpantas komputer, IO adalah yang paling kerap semuanya menjadi perlahan, jadi selalunya penyelesaian yang berkesan adalah untuk mengisi CPU dengan sesuatu semasa menerima data daripada IO.

Meneruskan metafora:

  • Jika dalam proses menyediakan telur dadar, saya juga akan cuba menukar pakaian, ini akan menjadi contoh multitasking. Nuansa penting: komputer jauh lebih baik dalam hal ini daripada manusia.
  • Dapur dengan beberapa tukang masak, contohnya di restoran - komputer berbilang teras.
  • Banyak restoran di medan selera di pusat membeli-belah - pusat data

.Alat BERSIH

.NET pandai bekerja dengan benang, seperti dengan banyak perkara lain. Dengan setiap versi baharu, ia memperkenalkan lebih banyak alat baharu untuk bekerja dengannya, lapisan abstraksi baharu ke atas utas OS. Apabila bekerja dengan pembinaan abstraksi, pembangun rangka kerja menggunakan pendekatan yang meninggalkan peluang, apabila menggunakan abstraksi peringkat tinggi, untuk turun satu atau lebih tahap di bawah. Selalunya ini tidak perlu, sebenarnya ia membuka pintu untuk menembak diri anda dengan senapang patah, tetapi kadang-kadang, dalam kes yang jarang berlaku, ia mungkin satu-satunya cara untuk menyelesaikan masalah yang tidak diselesaikan pada tahap abstraksi semasa .

Dengan alatan, saya maksudkan kedua-dua antara muka pengaturcaraan aplikasi (API) yang disediakan oleh rangka kerja dan pakej pihak ketiga, serta keseluruhan penyelesaian perisian yang memudahkan pencarian sebarang masalah yang berkaitan dengan kod berbilang benang.

Memulakan benang

Kelas Thread ialah kelas paling asas dalam .NET untuk bekerja dengan benang. Pembina menerima salah satu daripada dua perwakilan:

  • ThreadStart — Tiada parameter
  • ParametrizedThreadStart - dengan satu parameter objek jenis.

Perwakilan akan dilaksanakan dalam utas yang baru dibuat selepas memanggil kaedah Mula. Jika perwakilan jenis ParametrizedThreadStart diserahkan kepada pembina, maka objek mesti dihantar ke kaedah Mula. Mekanisme ini diperlukan untuk memindahkan sebarang maklumat tempatan ke strim. Perlu diingat bahawa mencipta benang adalah operasi yang mahal, dan benang itu sendiri adalah objek berat, sekurang-kurangnya kerana ia memperuntukkan 1MB memori pada tindanan dan memerlukan interaksi dengan API OS.

new Thread(...).Start(...);

Kelas ThreadPool mewakili konsep kolam. Dalam .NET, kumpulan benang adalah sebahagian daripada kejuruteraan, dan pembangun di Microsoft telah meletakkan banyak usaha untuk memastikan ia berfungsi secara optimum dalam pelbagai jenis senario.

Konsep umum:

Dari saat aplikasi dimulakan, ia mencipta beberapa utas dalam simpanan di latar belakang dan menyediakan keupayaan untuk mengambilnya untuk digunakan. Jika benang digunakan dengan kerap dan dalam jumlah yang banyak, kolam akan mengembang untuk memenuhi keperluan pemanggil. Apabila tiada utas percuma dalam kumpulan pada masa yang sesuai, ia sama ada akan menunggu untuk salah satu utas itu kembali atau mencipta benang baharu. Ini berikutan bahawa kumpulan benang bagus untuk beberapa tindakan jangka pendek dan kurang sesuai untuk operasi yang dijalankan sebagai perkhidmatan sepanjang keseluruhan operasi aplikasi.

Untuk menggunakan benang daripada kumpulan, terdapat kaedah QueueUserWorkItem yang menerima perwakilan jenis WaitCallback, yang mempunyai tandatangan yang sama seperti ParametrizedThreadStart, dan parameter yang dihantar kepadanya menjalankan fungsi yang sama.

ThreadPool.QueueUserWorkItem(...);

Kaedah kumpulan benang yang kurang dikenali RegisterWaitForSingleObject digunakan untuk mengatur operasi IO yang tidak menyekat. Wakil yang diserahkan kepada kaedah ini akan dipanggil apabila WaitHandle yang dihantar kepada kaedah itu adalah "Dikeluarkan".

ThreadPool.RegisterWaitForSingleObject(...)

.NET mempunyai pemasa benang dan ia berbeza daripada pemasa WinForms/WPF kerana pengendalinya akan dipanggil pada benang yang diambil dari kolam.

System.Threading.Timer

Terdapat juga cara yang agak eksotik untuk menghantar perwakilan untuk pelaksanaan ke benang dari kolam - kaedah BeginInvoke.

DelegateInstance.BeginInvoke

Saya ingin membincangkan secara ringkas tentang fungsi yang mana banyak kaedah di atas boleh dipanggil - CreateThread dari Kernel32.dll Win32 API. Terdapat cara, terima kasih kepada mekanisme kaedah luaran, untuk memanggil fungsi ini. Saya telah melihat panggilan sedemikian hanya sekali dalam contoh kod warisan yang mengerikan, dan motivasi pengarang yang melakukan perkara ini dengan tepat masih kekal sebagai misteri kepada saya.

Kernel32.dll CreateThread

Melihat dan Menyahpepijat Benang

Thread yang dibuat oleh anda, semua komponen pihak ketiga dan kumpulan .NET boleh dilihat dalam tetingkap Threads Visual Studio. Tetingkap ini hanya akan memaparkan maklumat benang apabila aplikasi berada di bawah nyahpepijat dan dalam mod Pecah. Di sini anda boleh melihat nama tindanan dan keutamaan setiap utas dengan mudah dan menukar penyahpepijatan kepada utas tertentu. Menggunakan sifat Keutamaan kelas Thread, anda boleh menetapkan keutamaan utas, yang OC dan CLR akan anggap sebagai cadangan apabila membahagikan masa pemproses antara utas.

.NET: Alat untuk bekerja dengan multithreading dan asynchrony. Bahagian 1

Perpustakaan Selari Tugas

Perpustakaan Selari Tugasan (TPL) telah diperkenalkan dalam .NET 4.0. Kini ia adalah standard dan alat utama untuk bekerja dengan tak segerak. Sebarang kod yang menggunakan pendekatan lama dianggap warisan. Unit asas TPL ialah kelas Tugas daripada ruang nama System.Threading.Tasks. Tugas ialah abstraksi ke atas benang. Dengan versi baharu bahasa C#, kami mendapat cara yang elegan untuk bekerja dengan Tasks - pengendali async/waiit. Konsep-konsep ini memungkinkan untuk menulis kod tak segerak seolah-olah ia mudah dan segerak, ini membolehkan walaupun orang yang kurang memahami cara kerja dalaman benang untuk menulis aplikasi yang menggunakannya, aplikasi yang tidak membeku apabila melakukan operasi yang lama. Menggunakan async/waiit ialah topik untuk satu atau beberapa artikel, tetapi saya akan cuba mendapatkan intipatinya dalam beberapa ayat:

  • async ialah pengubah suai kaedah mengembalikan Tugas atau batal
  • dan await ialah pengendali menunggu Tugas yang tidak menyekat.

Sekali lagi: pengendali menunggu, dalam kes umum (terdapat pengecualian), akan melepaskan utas pelaksanaan semasa dengan lebih lanjut, dan apabila Tugas menyelesaikan pelaksanaannya, dan utas (sebenarnya, lebih tepat untuk menyebut konteksnya , tetapi lebih lanjut mengenainya kemudian) akan terus melaksanakan kaedah selanjutnya. Di dalam .NET, mekanisme ini dilaksanakan dengan cara yang sama seperti pulangan hasil, apabila kaedah bertulis bertukar menjadi keseluruhan kelas, yang merupakan mesin keadaan dan boleh dilaksanakan dalam bahagian berasingan bergantung pada keadaan ini. Sesiapa yang berminat boleh menulis sebarang kod mudah menggunakan asynс/waiit, menyusun dan melihat pemasangan menggunakan JetBrains dotPeek dengan Kod Dihasilkan Pengkompil didayakan.

Mari lihat pilihan untuk melancarkan dan menggunakan Tugas. Dalam contoh kod di bawah, kami mencipta tugas baharu yang tidak berguna (Thread.Sleep(10000)), tetapi dalam kehidupan sebenar ini sepatutnya menjadi kerja intensif CPU yang kompleks.

using TCO = System.Threading.Tasks.TaskCreationOptions;

public static async void VoidAsyncMethod() {
    var cancellationSource = new CancellationTokenSource();

    await Task.Factory.StartNew(
        // Code of action will be executed on other context
        () => Thread.Sleep(10000),
        cancellationSource.Token,
        TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness,
        scheduler
    );

    //  Code after await will be executed on captured context
}

Tugas dibuat dengan beberapa pilihan:

  • LongRunning ialah pembayang bahawa tugas itu tidak akan diselesaikan dengan cepat, yang bermaksud mungkin berbaloi untuk tidak mengambil utas daripada kumpulan, tetapi mencipta satu yang berasingan untuk Tugasan ini agar tidak membahayakan orang lain.
  • AttachedToParent - Tugasan boleh disusun dalam hierarki. Jika pilihan ini digunakan, maka Tugas itu mungkin berada dalam keadaan ia sendiri telah selesai dan sedang menunggu pelaksanaan anak-anaknya.
  • PreferFairness - bermakna adalah lebih baik untuk melaksanakan Tugasan yang dihantar untuk pelaksanaan lebih awal sebelum yang dihantar kemudian. Tetapi ini hanyalah cadangan dan keputusan tidak dijamin.

Parameter kedua yang diluluskan kepada kaedah ialah CancellationToken. Untuk mengendalikan pembatalan operasi dengan betul selepas ia dimulakan, kod yang sedang dilaksanakan mesti diisi dengan semakan untuk keadaan PembatalanToken. Jika tiada semakan, kaedah Batal yang dipanggil pada objek CancellationTokenSource akan dapat menghentikan pelaksanaan Tugas hanya sebelum ia bermula.

Parameter terakhir ialah objek penjadual jenis TaskScheduler. Kelas ini dan keturunannya direka bentuk untuk mengawal strategi untuk mengedarkan Tugasan merentas urutan; secara lalai, Tugasan akan dilaksanakan pada urutan rawak daripada kumpulan.

Pengendali menunggu digunakan pada Tugas yang dibuat, yang bermaksud kod yang ditulis selepasnya, jika ada, akan dilaksanakan dalam konteks yang sama (selalunya ini bermakna pada urutan yang sama) seperti kod sebelum menunggu.

Kaedah ini ditandakan sebagai async void, yang bermaksud ia boleh menggunakan operator await, tetapi kod panggilan tidak akan dapat menunggu untuk pelaksanaan. Jika ciri sedemikian diperlukan, maka kaedah mesti mengembalikan Tugas. Kaedah yang ditanda async void agak biasa: sebagai peraturan, ini adalah pengendali acara atau kaedah lain yang berfungsi pada prinsip kebakaran dan lupa. Jika anda bukan sahaja perlu memberi peluang untuk menunggu sehingga akhir pelaksanaan, tetapi juga mengembalikan hasilnya, maka anda perlu menggunakan Tugas.

Pada Tugas yang kaedah StartNew dikembalikan, dan juga pada mana-mana yang lain, anda boleh memanggil kaedah ConfigureAwait dengan parameter palsu, kemudian pelaksanaan selepas menunggu akan diteruskan bukan pada konteks yang ditangkap, tetapi pada satu sewenang-wenangnya. Ini harus sentiasa dilakukan apabila konteks pelaksanaan tidak penting untuk kod selepas menunggu. Ini juga merupakan cadangan daripada MS semasa menulis kod yang akan dihantar dalam pakej dalam perpustakaan.

Mari kita fikirkan lebih lanjut tentang bagaimana anda boleh menunggu untuk menyelesaikan Tugasan. Di bawah ialah contoh kod, dengan ulasan tentang apabila jangkaan dilakukan secara bersyarat dengan baik dan apabila ia dilakukan secara bersyarat dengan buruk.

public static async void AnotherMethod() {

    int result = await AsyncMethod(); // good

    result = AsyncMethod().Result; // bad

    AsyncMethod().Wait(); // bad

    IEnumerable<Task> tasks = new Task[] {
        AsyncMethod(), OtherAsyncMethod()
    };

    await Task.WhenAll(tasks); // good
    await Task.WhenAny(tasks); // good

    Task.WaitAll(tasks.ToArray()); // bad
}

Dalam contoh pertama, kami menunggu Tugas selesai tanpa menyekat utas panggilan; kami akan kembali memproses hasil hanya apabila ia sudah ada; sehingga itu, utas panggilan dibiarkan pada perantinya sendiri.

Dalam pilihan kedua, kami menyekat benang panggilan sehingga hasil kaedah dikira. Ini buruk bukan sahaja kerana kami telah menduduki satu utas, sumber program yang begitu berharga, dengan kemalasan yang mudah, tetapi juga kerana jika kod kaedah yang kami panggil mengandungi menunggu, dan konteks penyegerakan memerlukan kembali ke utas panggilan selepas tunggu, maka kita akan mendapat kebuntuan : Benang panggilan menunggu hasil kaedah tak segerak dikira, kaedah tak segerak cuba sia-sia untuk meneruskan pelaksanaannya dalam utas panggilan.

Satu lagi kelemahan pendekatan ini ialah pengendalian ralat yang rumit. Hakikatnya ialah ralat dalam kod asynchronous apabila menggunakan async/wait adalah sangat mudah untuk dikendalikan - mereka berkelakuan sama seolah-olah kod itu segerak. Walaupun jika kita menggunakan eksorsisme tunggu segerak pada Tugas, pengecualian asal bertukar menjadi AggregateException, i.e. Untuk mengendalikan pengecualian, anda perlu memeriksa jenis InnerException dan menulis rantai if sendiri di dalam satu blok tangkapan atau gunakan tangkapan semasa membina, bukannya rantaian blok tangkapan yang lebih biasa dalam dunia C#.

Contoh ketiga dan terakhir juga ditanda buruk untuk sebab yang sama dan mengandungi semua masalah yang sama.

Kaedah WhenAny dan WhenAll sangat mudah untuk menunggu kumpulan Tugasan; mereka membungkus kumpulan Tugasan menjadi satu, yang akan menyala sama ada apabila Tugasan daripada kumpulan itu mula-mula dicetuskan, atau apabila kesemuanya telah menyelesaikan pelaksanaannya.

Menghentikan benang

Atas pelbagai sebab, mungkin perlu menghentikan aliran selepas ia bermula. Terdapat beberapa cara untuk melakukan ini. Kelas Thread mempunyai dua kaedah yang sesuai dinamakan: Batalkan и Mengganggu. Yang pertama sangat tidak disyorkan untuk digunakan, kerana selepas memanggilnya pada bila-bila masa rawak, semasa pemprosesan sebarang arahan, pengecualian akan dilemparkan ThreadAbortedException. Anda tidak menjangkakan pengecualian sedemikian akan dilemparkan apabila menambah sebarang pembolehubah integer, bukan? Dan apabila menggunakan kaedah ini, ini adalah situasi yang sangat nyata. Jika anda perlu menghalang CLR daripada menjana pengecualian sedemikian dalam bahagian kod tertentu, anda boleh membungkusnya dalam panggilan Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Sebarang kod yang ditulis dalam blok akhirnya dibungkus dalam panggilan sedemikian. Atas sebab ini, dalam kedalaman kod rangka kerja anda boleh mencari blok dengan percubaan kosong, tetapi bukan kosong akhirnya. Microsoft tidak menggalakkan kaedah ini sehingga mereka tidak memasukkannya ke dalam teras .net.

Kaedah Interrupt berfungsi lebih boleh diramalkan. Ia boleh mengganggu benang dengan pengecualian ThreadInterruptedException hanya pada saat-saat benang dalam keadaan menunggu. Ia memasuki keadaan ini semasa tergantung sementara menunggu WaitHandle, kunci, atau selepas memanggil Thread.Sleep.

Kedua-dua pilihan yang diterangkan di atas adalah buruk kerana tidak dapat diramalkan. Penyelesaiannya adalah dengan menggunakan struktur PembatalanToken dan kelas CancellationTokenSource. Intinya ialah ini: contoh kelas CancellationTokenSource dicipta dan hanya orang yang memilikinya boleh menghentikan operasi dengan memanggil kaedah Batal. Hanya CancellationToken dihantar kepada operasi itu sendiri. Pemilik CancellationToken tidak boleh membatalkan operasi itu sendiri, tetapi hanya boleh menyemak sama ada operasi telah dibatalkan. Terdapat sifat Boolean untuk ini IsCancellationRequested dan kaedah ThrowIfCancelRequested. Yang terakhir akan membuang pengecualian TaskCancelledException jika kaedah Batal dipanggil pada contoh PembatalanToken yang sedang dipamerkan. Dan ini adalah kaedah yang saya cadangkan untuk digunakan. Ini adalah penambahbaikan berbanding pilihan sebelumnya dengan memperoleh kawalan penuh ke atas pada masa mana operasi pengecualian boleh dihentikan.

Pilihan yang paling kejam untuk menghentikan benang adalah dengan memanggil fungsi Win32 API TerminateThread. Kelakuan CLR selepas memanggil fungsi ini mungkin tidak dapat diramalkan. Pada MSDN perkara berikut ditulis tentang fungsi ini: “TerminateThread ialah fungsi berbahaya yang hanya boleh digunakan dalam kes yang paling ekstrem. “

Menukar API warisan kepada Berasaskan Tugas menggunakan kaedah FromAsync

Jika anda cukup bernasib baik untuk mengerjakan projek yang dimulakan selepas Tasks diperkenalkan dan tidak lagi menimbulkan kengerian senyap bagi kebanyakan pembangun, maka anda tidak perlu berurusan dengan banyak API lama, kedua-dua API pihak ketiga dan pasukan anda telah menyeksa pada masa lalu. Nasib baik, pasukan .NET Framework menjaga kami, walaupun mungkin matlamatnya adalah untuk menjaga diri kami sendiri. Walau apa pun, .NET mempunyai beberapa alat untuk menukar kod tanpa rasa sakit yang ditulis dalam pendekatan pengaturcaraan tak segerak lama kepada yang baharu. Salah satunya ialah kaedah FromAsync TaskFactory. Dalam contoh kod di bawah, saya membungkus kaedah async lama kelas WebRequest dalam Tugas menggunakan kaedah ini.

object state = null;
WebRequest wr = WebRequest.CreateHttp("http://github.com");
await Task.Factory.FromAsync(
    wr.BeginGetResponse,
    we.EndGetResponse
);

Ini hanyalah satu contoh dan anda mungkin tidak perlu melakukan ini dengan jenis terbina dalam, tetapi mana-mana projek lama hanya dipenuhi dengan kaedah BeginDoSomething yang mengembalikan kaedah IAsyncResult dan EndDoSomething yang menerimanya.

Tukar API legasi kepada Task Based menggunakan kelas TaskCompletionSource

Satu lagi alat penting untuk dipertimbangkan ialah kelas Sumber Penyelesaian Tugas. Dari segi fungsi, tujuan dan prinsip operasi, ia mungkin agak mengingatkan kaedah RegisterWaitForSingleObject kelas ThreadPool, yang saya tulis di atas. Menggunakan kelas ini, anda boleh membungkus API tak segerak lama dengan mudah dan mudah dalam Tasks.

Anda akan mengatakan bahawa saya telah bercakap tentang kaedah FromAsync kelas TaskFactory yang dimaksudkan untuk tujuan ini. Di sini kita perlu mengingati keseluruhan sejarah pembangunan model tak segerak dalam .net yang telah ditawarkan oleh Microsoft sejak 15 tahun lalu: sebelum Corak Tak Segerak Berasaskan Tugas (TAP), terdapat Corak Pengaturcaraan Asynchronous (APP), yang adalah mengenai kaedah MemulakanDoSomething kembali IAsyncResult dan kaedah akhirDoSomething yang menerimanya dan untuk warisan tahun-tahun ini kaedah FromAsync adalah sempurna, tetapi dari masa ke masa, ia telah digantikan oleh Corak Asynchronous Based Event (EAP), yang mengandaikan bahawa peristiwa akan dibangkitkan apabila operasi tak segerak selesai.

TaskCompletionSource sesuai untuk membungkus Tugasan dan API warisan yang dibina di sekeliling model acara. Intipati kerjanya adalah seperti berikut: objek kelas ini mempunyai harta awam jenis Tugas, keadaannya boleh dikawal melalui kaedah SetResult, SetException, dsb. kelas TaskCompletionSource. Di tempat di mana pengendali menunggu digunakan pada Tugasan ini, ia akan dilaksanakan atau gagal dengan pengecualian bergantung pada kaedah yang digunakan pada TaskCompletionSource. Jika masih tidak jelas, mari kita lihat contoh kod ini, di mana beberapa API EAP lama dibalut dengan Tugas menggunakan TaskCompletionSource: apabila acara dijalankan, Tugasan akan dipindahkan ke keadaan Selesai dan kaedah yang menggunakan operator menunggu kepada Tugasan ini akan menyambung semula pelaksanaannya setelah menerima objek mengakibatkan.

public static Task<Result> DoAsync(this SomeApiInstance someApiObj) {

    var completionSource = new TaskCompletionSource<Result>();
    someApiObj.Done += 
        result => completionSource.SetResult(result);
    someApiObj.Do();

    result completionSource.Task;
}

Petua & Trik Sumber Tugasan

Membungkus API lama bukan semua yang boleh dilakukan menggunakan TaskCompletionSource. Menggunakan kelas ini membuka kemungkinan menarik untuk mereka bentuk pelbagai API pada Tugasan yang tidak menduduki urutan. Dan aliran, seperti yang kita ingat, adalah sumber yang mahal dan bilangannya adalah terhad (terutamanya dengan jumlah RAM). Had ini boleh dicapai dengan mudah dengan membangunkan, sebagai contoh, aplikasi web yang dimuatkan dengan logik perniagaan yang kompleks. Mari kita pertimbangkan kemungkinan yang saya bincangkan semasa melaksanakan helah seperti Long-Polling.

Ringkasnya, intipati helah adalah ini: anda perlu menerima maklumat daripada API tentang beberapa peristiwa yang berlaku di sisinya, manakala API, atas sebab tertentu, tidak boleh melaporkan acara itu, tetapi hanya boleh mengembalikan keadaan. Contoh ini adalah semua API yang dibina di atas HTTP sebelum zaman WebSocket atau apabila mustahil atas sebab tertentu untuk menggunakan teknologi ini. Pelanggan boleh bertanya kepada pelayan HTTP. Pelayan HTTP tidak boleh sendiri memulakan komunikasi dengan klien. Penyelesaian mudah adalah dengan meninjau pelayan menggunakan pemasa, tetapi ini mewujudkan beban tambahan pada pelayan dan kelewatan tambahan secara purata TimerInterval / 2. Untuk mengatasinya, helah yang dipanggil Long Polling telah dicipta, yang melibatkan penangguhan respons daripada pelayan sehingga Tamat Masa tamat atau peristiwa akan berlaku. Sekiranya peristiwa itu telah berlaku, maka ia diproses, jika tidak, maka permintaan itu dihantar semula.

while(!eventOccures && !timeoutExceeded)  {

  CheckTimout();
  CheckEvent();
  Thread.Sleep(1);
}

Tetapi penyelesaian sedemikian akan terbukti sangat buruk sebaik sahaja bilangan pelanggan yang menunggu acara meningkat, kerana... Setiap pelanggan sedemikian menduduki keseluruhan rangkaian menunggu acara. Ya, dan kami mendapat kelewatan 1ms tambahan apabila peristiwa dicetuskan, selalunya ini tidak penting, tetapi mengapa menjadikan perisian lebih teruk daripada yang boleh? Jika kita mengalih keluar Thread.Sleep(1), maka sia-sia kita akan memuatkan satu teras pemproses 100% melahu, berputar dalam kitaran yang tidak berguna. Menggunakan TaskCompletionSource anda boleh membuat semula kod ini dengan mudah dan menyelesaikan semua masalah yang dikenal pasti di atas:

class LongPollingApi {

    private Dictionary<int, TaskCompletionSource<Msg>> tasks;

    public async Task<Msg> AcceptMessageAsync(int userId, int duration) {

        var cs = new TaskCompletionSource<Msg>();
        tasks[userId] = cs;
        await Task.WhenAny(Task.Delay(duration), cs.Task);
        return cs.Task.IsCompleted ? cs.Task.Result : null;
    }

    public void SendMessage(int userId, Msg m) {

        if (tasks.TryGetValue(userId, out var completionSource))
            completionSource.SetResult(m);
    }
}

Kod ini tidak sedia pengeluaran, tetapi hanya demo. Untuk menggunakannya dalam kes sebenar, anda juga perlu, sekurang-kurangnya, untuk mengendalikan situasi apabila mesej tiba pada masa yang tiada siapa menjangkakannya: dalam kes ini, kaedah AsseptMessageAsync harus mengembalikan Tugas yang telah siap. Jika ini adalah kes yang paling biasa, maka anda boleh berfikir tentang menggunakan ValueTask.

Apabila kami menerima permintaan untuk mesej, kami mencipta dan meletakkan TaskCompletionSource dalam kamus, dan kemudian tunggu apa yang berlaku dahulu: selang masa yang ditentukan tamat tempoh atau mesej diterima.

ValueTask: kenapa dan bagaimana

Pengendali async/menunggu, seperti pengendali pulangan hasil, menjana mesin keadaan daripada kaedah, dan ini ialah penciptaan objek baharu, yang hampir selalu tidak penting, tetapi dalam kes yang jarang berlaku, ia boleh menimbulkan masalah. Kes ini mungkin kaedah yang sangat kerap dipanggil, kita bercakap tentang puluhan dan ratusan ribu panggilan sesaat. Jika kaedah sedemikian ditulis sedemikian rupa sehingga dalam kebanyakan kes ia mengembalikan hasil yang memintas semua kaedah menunggu, maka .NET menyediakan alat untuk mengoptimumkan ini - struktur ValueTask. Untuk menjelaskannya, mari lihat contoh penggunaannya: terdapat cache yang sering kita pergi. Terdapat beberapa nilai di dalamnya dan kemudian kami hanya mengembalikannya; jika tidak, maka kami pergi ke beberapa IO perlahan untuk mendapatkannya. Saya mahu melakukan yang terakhir secara tak segerak, yang bermaksud keseluruhan kaedah ternyata tidak segerak. Oleh itu, cara yang jelas untuk menulis kaedah adalah seperti berikut:

public async Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return val;
    return await RequestById(id);
}

Kerana keinginan untuk mengoptimumkan sedikit, dan sedikit ketakutan tentang apa yang Roslyn akan hasilkan apabila menyusun kod ini, anda boleh menulis semula contoh ini seperti berikut:

public Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return Task.FromResult(val);
    return RequestById(id);
}

Sesungguhnya, penyelesaian optimum dalam kes ini adalah untuk mengoptimumkan laluan panas, iaitu, mendapatkan nilai daripada kamus tanpa sebarang peruntukan dan beban yang tidak perlu pada GC, manakala dalam kes yang jarang berlaku apabila kita masih perlu pergi ke IO untuk data , semuanya akan kekal sebagai tambah /tolak dengan cara lama:

public ValueTask<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return new ValueTask<string>(val);
    return new ValueTask<string>(RequestById(id));
}

Mari kita lihat lebih dekat pada sekeping kod ini: jika terdapat nilai dalam cache, kami mencipta struktur, jika tidak, tugas sebenar akan dibalut dengan yang bermakna. Kod panggilan tidak peduli laluan mana kod ini dilaksanakan: ValueTask, dari sudut pandangan sintaks C#, akan berkelakuan sama seperti Tugas biasa dalam kes ini.

Penjadual Tugas: menguruskan strategi pelancaran tugas

API seterusnya yang saya ingin pertimbangkan ialah kelas tugas Scheduler dan derivatifnya. Saya telah menyatakan di atas bahawa TPL mempunyai keupayaan untuk mengurus strategi untuk mengedarkan Tugasan merentas urutan. Strategi sedemikian ditakrifkan dalam keturunan kelas TaskScheduler. Hampir semua strategi yang anda perlukan boleh didapati di perpustakaan. ParallelExtensionsExtras, dibangunkan oleh Microsoft, tetapi bukan sebahagian daripada .NET, tetapi dibekalkan sebagai pakej Nuget. Mari kita lihat secara ringkas beberapa daripada mereka:

  • CurrentThreadTaskScheduler — melaksanakan Tugas pada urutan semasa
  • LimitedConcurrencyLevelTaskScheduler — mengehadkan bilangan Tugasan yang dilaksanakan secara serentak oleh parameter N, yang diterima dalam pembina
  • OrderedTaskScheduler — ditakrifkan sebagai LimitedConcurrencyLevelTaskScheduler(1), jadi tugasan akan dilaksanakan secara berurutan.
  • WorkStealingTaskScheduler - melaksanakan curi kerja pendekatan pengagihan tugas. Pada asasnya ia adalah ThreadPool yang berasingan. Menyelesaikan masalah bahawa dalam .NET ThreadPool ialah kelas statik, satu untuk semua aplikasi, yang bermaksud bahawa beban lampau atau penggunaan yang salah dalam satu bahagian program boleh membawa kepada kesan sampingan yang lain. Lebih-lebih lagi, sangat sukar untuk memahami punca kecacatan tersebut. Itu. Mungkin terdapat keperluan untuk menggunakan WorkStealingTaskSchedulers berasingan dalam bahagian program yang penggunaan ThreadPool mungkin agresif dan tidak dapat diramalkan.
  • QeuedTaskScheduler — membolehkan anda melaksanakan tugas mengikut peraturan baris gilir keutamaan
  • ThreadPerTaskScheduler — mencipta benang berasingan untuk setiap Tugasan yang dilaksanakan padanya. Boleh berguna untuk tugasan yang mengambil masa yang tidak dapat diramalkan untuk diselesaikan.

Terdapat terperinci yang baik artikel tentang TaskSchedulers pada blog microsoft.

Untuk nyahpepijat mudah semua yang berkaitan dengan Tugas, Visual Studio mempunyai tetingkap Tugas. Dalam tetingkap ini anda boleh melihat keadaan semasa tugas dan melompat ke baris kod yang sedang dilaksanakan.

.NET: Alat untuk bekerja dengan multithreading dan asynchrony. Bahagian 1

PLinq dan kelas Selari

Selain Tugasan dan semua yang dikatakan tentangnya, terdapat dua lagi alatan yang menarik dalam .NET: PLinq (Linq2Parallel) dan kelas Selari. Yang pertama menjanjikan pelaksanaan selari semua operasi Linq pada berbilang benang. Bilangan utas boleh dikonfigurasikan menggunakan kaedah sambungan WithDegreeOfParallelism. Malangnya, selalunya PLinq dalam mod lalainya tidak mempunyai maklumat yang mencukupi tentang dalaman sumber data anda untuk memberikan peningkatan kelajuan yang ketara, sebaliknya, kos percubaan adalah sangat rendah: anda hanya perlu menghubungi kaedah AsParallel sebelum rantaian kaedah Linq dan menjalankan ujian prestasi. Selain itu, adalah mungkin untuk menghantar maklumat tambahan kepada PLinq tentang sifat sumber data anda menggunakan mekanisme Partition. Anda boleh membaca lebih lanjut di sini и di sini.

Kelas statik Selari menyediakan kaedah untuk lelaran melalui koleksi Foreach secara selari, melaksanakan gelung For, dan melaksanakan berbilang perwakilan secara selari Invoke. Pelaksanaan benang semasa akan dihentikan sehingga pengiraan selesai. Bilangan utas boleh dikonfigurasikan dengan menghantar ParallelOptions sebagai hujah terakhir. Anda juga boleh menentukan TaskScheduler dan CancellationToken menggunakan pilihan.

Penemuan

Apabila saya mula menulis artikel ini berdasarkan bahan-bahan laporan saya dan maklumat yang saya kumpulkan semasa saya bekerja selepas itu, saya tidak menyangka akan ada begitu banyak. Sekarang, apabila penyunting teks tempat saya menaip artikel ini secara mencela memberitahu saya bahawa halaman 15 telah hilang, saya akan meringkaskan keputusan sementara. Helah, API, alat visual dan perangkap lain akan dibincangkan dalam artikel seterusnya.

Kesimpulan:

  • Anda perlu mengetahui alat untuk bekerja dengan benang, asynchrony dan paralelisme untuk menggunakan sumber PC moden.
  • .NET mempunyai banyak alat yang berbeza untuk tujuan ini
  • Tidak semua daripada mereka muncul serentak, jadi anda sering boleh mencari yang lama, namun, terdapat cara untuk menukar API lama tanpa banyak usaha.
  • Bekerja dengan benang dalam .NET diwakili oleh kelas Thread dan ThreadPool
  • Kaedah Thread.Abort, Thread.Interrupt dan Win32 API TerminateThread adalah berbahaya dan tidak disyorkan untuk digunakan. Sebaliknya, lebih baik menggunakan mekanisme PembatalanToken
  • Aliran adalah sumber yang berharga dan bekalannya terhad. Situasi di mana benang sibuk menunggu acara harus dielakkan. Untuk ini adalah mudah untuk menggunakan kelas TaskCompletionSource
  • Alat .NET yang paling berkuasa dan termaju untuk bekerja dengan selari dan tak segerak ialah Tugas.
  • Pengendali c# async/wait melaksanakan konsep menunggu tanpa menyekat
  • Anda boleh mengawal pengedaran Tugas merentas urutan menggunakan kelas terbitan TaskScheduler
  • Struktur ValueTask boleh berguna dalam mengoptimumkan laluan panas dan trafik memori
  • Tetingkap Tugas dan Benang Visual Studio menyediakan banyak maklumat yang berguna untuk menyahpepijat kod berbilang benang atau tak segerak
  • PLinq ialah alat yang hebat, tetapi ia mungkin tidak mempunyai maklumat yang mencukupi tentang sumber data anda, tetapi ini boleh diperbaiki menggunakan mekanisme pembahagian
  • Perlu diteruskan ...

Sumber: www.habr.com

Tambah komen