28-Bob: I/O-Bound Asinxron Operatsiyalar

Windows I/O mexanizmi, C# async/await, kompilyator transformatsiyasi, state machine, SynchronizationContext va I/O operatsiyalarni boshqarish

Ushbu bobda men sizga Windows operatsion tizimi I/O operatsiyalarni qanday asinxron bajarishini tushuntiraman. Bu sizga hech qanday oqimni bloklash yoki behuda sarflamasdan kengayuvchan (scalable) va sezgir (responsive) ilovalar yaratish imkonini beradi. Bu, afsuski, ko'p dasturchilar to'g'ri tushunmaydigan mavzu, chunki Microsoft yillar davomida asinxron dasturlash modellarini bir necha marta o'zgartirgan. Men avval asosiy Windows mexanizmlarini tushuntirib, keyin C# ning async va await kalit so'zlari orqali zamonaviy asinxron dasturlashni ko'rsataman.

Ushbu bobda:

  • Windows sinxron va asinxron I/O operatsiyalarni qanday bajara olishi
  • C# ning asinxron funksiyalari (async / await)
  • Kompilyator asinxron funksiyalarni state machine ga qanday aylantirishi
  • Asinxron funksiya kengaytiruvchanligi va TaskLogger
  • Async funksiyalar va event handlerlar
  • Framework Class Library (FCL) dagi asinxron funksiyalar
  • Asinxron funksiyalar va xatolar boshqaruvi (exception handling)
  • Boshqa asinxron funksiya xususiyatlari
  • Ilova modellari va ularning oqim modellari
  • Serverni asinxron amalga oshirish
  • I/O operatsiyalarni bekor qilish
  • Sinxron bajarilishi zarur bo'lgan I/O operatsiyalari
  • FileStream ga xos muammolar
  • I/O so'rov ustuvorliklari (I/O Request Priorities)

Windows I/O Operatsiyalarni Qanday Bajaradi

Keling, avval dasturlar Windows orqali I/O operatsiyalarni (fayl, tarmoq, ma'lumotlar bazasi va boshqalar) qanday bajarishini ko'rib chiqaylik. Quyidagi muhokamada men sizga fayldagi ma'lumotlarni asinxron o'qish misolida Windows ning ichki ishlashini soddalashtirilgan tarzda tushuntiraman.

Sinxron I/O: Muammo

Birinchi rasmda (28-1 rasm kitobda) sinxron I/O operatsiya qanday bajarilishi ko'rsatilgan. Sizning dasturingiz FileStream ning Read metodini chaqiradi. Bu ichki tarzda Windows ning ReadFile funksiyasini chaqiradi. ReadFile bitta kichik ma'lumotlar strukturasini ajratadi — bu I/O So'rov Paketi (I/O Request Packet — IRP) deb ataladi. IRP so'rov parametrlari bilan to'ldiriladi: fayl handle, fayldagi offset, Byte[] massiv ko'rsatkichi va o'qiladigan baytlar soni.

ReadFile keyin IRP ni Windows yadrosiga (kernel) uzatadi, u esa qurilma drayverining IRP navbatiga qo'shadi. Sizning oqimingiz endi hech qanday foydali ish bajarmaydi — u shunchaki bloklangan holda kutib turadi, protsessor vaqtini sarflamaydi lekin qimmatbaho tizim resurslarini (oqimning steki, yadro obyektlari va boshqalar) egallab turadi.

Nihoyat, qurilma (masalan, qattiq disk) I/O operatsiyani bajarib bo'lgach, Windows sizning oqimingizni uyg'otadi, CPU ni rejalashtiradi va boshqaruv yadroda foydalanuvchi rejimiga qaytadi. FileStream ning Read metodi endi Int32 qiymat qaytaradi — o'qilgan baytlar sonini bildiradi.

Sinxron I/O ning muammosi

Tasavvur qiling, veb-ilovangiz har bir mijoz so'rovida ma'lumotlar bazasiga murojaat qiladi. Sinxron so'rov yuborsangiz, oqim javob kutib bloklangan holda turadi. Shu vaqtda yangi mijoz so'rovi kelsa, oqim havzasi (thread pool) yangi oqim yaratishi kerak. So'rovlar ko'paygan sari tobora ko'proq oqimlar yaratiladi — barchasi bloklangan holda kutib turadi. Natijada veb-serveringiz juda ko'p tizim resurslari (oqimlar va ularning xotirasi) ajratadi, lekin ulardan deyarli foydalanmaydi!

Bundan ham yomoni, ma'lumotlar bazasi javob qaytarganda, barcha oqimlar blokdan chiqib birdaniga ishlashni boshlaydi. Ko'p oqimlar ishlayotgan va nisbatan kam protsessor yadrolari mavjud bo'lganligi sababli, Windows tez-tez kontekst almashtirishlar (context switches) bajarishi kerak, bu esa unumdorlikni yanada pasaytiradi. Bu kengayuvchan ilovani amalga oshirishning yo'li emas.

Asinxron I/O: Yechim

Endi Windows asinxron I/O operatsiyalarni qanday bajarishini ko'rib chiqamiz. 28-2 rasmda men barcha qurilmalarni olib tashladim, faqat qattiq diskni qoldirdim, va CLR ning oqim havzasini (thread pool) kiritdim. Kodni biroz o'zgartirdim: endi men FileStream ni FileOptions.Asynchronous bayrog'i bilan ochaman. Bu bayroq Windows ga faylga nisbatan o'qish va yozish operatsiyalarimni asinxron bajarishni xohlashimni bildiradi.

Fayldan ma'lumot o'qish uchun endi Read o'rniga ReadAsync ni chaqiraman. ReadAsync ichki tarzda Task<Int32> obyektni ajratadi — o'qish operatsiyasining kutilayotgan tugallanishini ifodalash uchun. Keyin ReadAsync Win32 ning ReadFile funksiyasini chaqiradi. ReadFile IRP ni ajratadi, uni sinxron stsenaridagi kabi to'ldiradi va Windows yadrosiga uzatadi. Windows IRP ni qattiq disk drayverining IRP navbatiga qo'shadi.

Lekin endi, sizning oqimingizni bloklash o'rniga, u darhol qaytib keladiReadAsync chaqiruvidan (#5, #6, va #7). Albatta, IRP hali qayta ishlanmagan bo'lishi mumkin, shuning uchun ReadAsync dan qaytgandan keyin Byte[] dagi baytlarga murojaat qiladigan kod yozib bo'lmaydi.

FileStream fs = new FileStream(..., FileOptions.Asynchronous);
Task<Int32> task = fs.ReadAsync(...);

// Oqimingiz bu yerda bloklashmasdan davom etadi!
// IRP tugagach, Task<Int32> natija yoki istisno bilan yakunlanadi

Qurilma I/O operatsiyani qayta ishlashni tugallgach (a), tugallangan IRP ni CLR ning oqim havzasiga yuboradi (b). Kelajakda oqim havzasi oqimi tugallangan IRP ni oladi va vazifani bajaruvchi kodni ishga tushiradi — istisno o'rnatadi (agar xato bo'lsa) yoki natijani qaytaradi (bu holda o'qilgan baytlar sonini ifodalovchi Int32) (c). Endi Task obyekti operatsiya tugallanganini biladi va bu, o'z navbatida, sizning kodingizni davom ettirishga ruxsat beradi.

Endi barchasini katta rasmda ko'rib chiqaylik. Aytaylik, mijoz so'rovi keldi va serverimiz asinxron ma'lumotlar bazasi so'rovini amalga oshiradi. Natijada oqimimiz bloklashmas va oqim havzasiga qaytib, yangi mijoz so'rovlarini qabul qilishi mumkin. Ma'lumotlar bazasi javob qaytarganda, uning javobi oqim havzasiga navbatga qo'yiladi va oqim havzasi oqimi javobni qayta ishlab, natijani mijozga yuboradi.

Natija

Endi biz bitta oqim bilan barcha mijoz so'rovlari va ma'lumotlar bazasi javoblarini qayta ishlaymiz. Serverimiz juda kam tizim resurslaridan foydalanadi va tez ishlaydi, ayniqsa kontekst almashtirishlari yo'qligi sababli!

Agar oqim havzasiga bittadan ko'proq ish kelib qolsa, oqim havzasi qo'shimcha oqimlar yaratishi mumkin. To'rt yadroli mashinada to'rtta mijoz so'rovi/ma'lumotlar bazasi javoblari (har qanday kombinatsiyada) to'rtta oqimda kontekst almashtirishsiz parallel ishlaydi.

Asinxron I/O ning Qo'shimcha Afzalliklari

Kam resurs ishlatish va kamaytirilgan kontekst almashtirishlardan tashqari, I/O operatsiyalarni asinxron bajarishda yana ko'p afzalliklar mavjud.

Chiqindi yig'ish (Garbage Collection) tezlashadi. Chiqindi yig'ish boshlanayotganda, CLR jarayondagi barcha oqimlarni to'xtatishi kerak. Shuning uchun, qancha kam oqim bo'lsa, chiqindi yig'uvchi shuncha tez ishlaydi. Bundan tashqari, chiqindi yig'ish yuz berganda, CLR barcha oqimlarning steklarini ildiz (root) izlash uchun yurishi kerak. Qancha kam oqim bo'lsa — shuncha kam stek va shuncha tez ishlaydi. Agar oqimlarimiz ish bajarish paytida bloklashmas, ular stek tepasida (top of stack) bo'ladi va har bir oqim stekini ildiz uchun yurish juda kam vaqt oladi.

Debug qilish osonlashadi. Ilovangizni debug qilayotganda, Windows breakpoint da barcha oqimlarni to'xtatadi. Keyin bajarishni davom ettirganingizda, barcha oqimlarni qayta boshlashi kerak. Ko'p oqimli ilovada har bir oqim bo'ylab qadam bosish (single-stepping) juda sekin bo'lishi mumkin. Asinxron I/O sizga kamroq oqimlar bilan ishlash imkonini beradi va bu debug qilish samaradorligini oshiradi.

Unumdorlik (performance) oshadi. Aytaylik, ilovangiz 10 ta rasmni turli veb-saytlardan yuklamoqchi va har birini yuklash 5 soniya oladi. Sinxron yuklaydigan bo'lsangiz (birin-ketin), jami 50 soniya kerak bo'ladi. Lekin asinxron yuklab, barchasini bir vaqtda boshlasangiz, barcha 10 tasini 5 soniyada olasiz! Sinxron holda jami vaqt barcha alohida vaqtlarning yig'indisi, asinxron holda esa eng sekin operatsiyaning vaqtiga teng.

GUI ilovalar uchun asinxron operatsiyalar yana bir afzallik taklif etadi: foydalanuvchi interfeysi hang qilmaydi va sezgir bo'lib qoladi. Aslida, Microsoft Silverlight yoki Windows Store ilovalari yaratsangiz, barcha I/O operatsiyalarni asinxron bajarishingiz shart, chunki sizga faqat asinxron metodlar taqdim etilgan — ekvivalent sinxron metodlar kutubxonada mavjud emas. Bu qasddan shunday qilingan — dasturchilarni sezgir ilovalar yaratishga majburlash uchun.

C# ning Asinxron Funksiyalari

Asinxron operatsiyalarni bajarish kengayuvchan va sezgir ilovalar qurish kalitidir. Oqim havzasi bilan birgalikda asinxron operatsiyalar mashinadagi barcha protsessor yadrolilaridan foydalanish imkonini beradi. Microsoft bu ulkan imkoniyatni anglagan holda, dasturlash modelini loyihalagan va uni C# tili xususiyati sifatida amalga oshirgan. Bu model 27-bobda muhokama qilingan Task lardan va asinxron funksiyalar (async functions, qisqacha async funksiyalar) deb ataladigan C# xususiyatidan foydalanadi.

Mana ikkita asinxron I/O operatsiyani bajaradigan async funksiyani ishlatadigan kod misoli:

private static async Task<String> IssueClientRequestAsync(String serverName, String message) {
   using (var pipe = new NamedPipeClientStream(serverName, "PipeName", PipeDirection.InOut,
      PipeOptions.Asynchronous | PipeOptions.WriteThrough)) {

      pipe.Connect(); // ReadMode o'rnatishdan oldin Connect bo'lishi kerak
      pipe.ReadMode = PipeTransmissionMode.Message;

      // Serverga asinxron ma'lumot yuborish
      Byte[] request = Encoding.UTF8.GetBytes(message);
      await pipe.WriteAsync(request, 0, request.Length);

      // Serverning javobini asinxron o'qish
      Byte[] response = new Byte[1000];
      Int32 bytesRead = await pipe.ReadAsync(response, 0, response.Length);
      return Encoding.UTF8.GetString(response, 0, bytesRead);
   }  // Pipe ni yopish
}

Yuqoridagi kodda IssueClientRequestAsync async funksiya ekanligini birinchi satrda static dan keyin async belgilangani bilan aniqlash mumkin. Metodga async belgisini qo'yganingizda, kompilyator sizning metod kodingizni state machine ni amalga oshiradigan turga aylantiradi (batafsil tafsilotlar keyingi bo'limda muhokama qilinadi). Bu oqimga kodning bir qismini ishga tushirish va keyin to'liq tugallanmasdan qaytib kelish imkonini beradi.

Oqim IssueClientRequestAsync ni chaqirganda, NamedPipeClientStream ni yaratadi, Connect ni chaqiradi, ReadMode xususiyatini o'rnatadi, xabarni Byte[] ga aylantiradi va keyin WriteAsync ni chaqiradi. WriteAsync ichki tarzda Task obyektni ajratadi va uni IssueClientRequestAsync ga qaytaradi. Bu nuqtada, C# ning await operatori samarali ravishda Task obyektida ContinueWith ni chaqiradi va state machine ni davom ettiradigan callback metodini uzatadi. Keyin oqim IssueClientRequestAsync dan qaytadi.

Kelajakda tarmoq qurilma drayveri pipe ga ma'lumot yozishni tugallaydi va oqim havzasi oqimi Task obyektini xabardor qiladi, u esa ContinueWith callback ni faollashtiradi va state machine ni davom ettiradi. Aniqroq aytganda, oqim IssueClientRequestAsync metodiga qayta kiradi, lekin await operatorining nuqtasida. Metodimiz endi kompilyator yaratgan kodni bajaradi — Task holatini tekshiradi. Operatsiya muvaffaqiyatsiz bo'lsa, istisnoni tashlaydi. Muvaffaqiyatli bo'lsa, await operatori natijani qaytaradi. Bu holda WriteAsync Task<TResult> o'rniga Task qaytaradi, shuning uchun qaytish qiymati yo'q.

Endi metodimiz Byte[] ajratadi va NamedPipeClientStream ning ReadAsync asinxron metodini chaqiradi. ReadAsync Task<Int32> obyektini yaratadi va qaytaradi. Yana await operatori Task<Int32> da ContinueWith ni chaqiradi va oqim IssueClientRequestAsync dan qaytadi.

Kelajakda server javob yuboradi, tarmoq qurilma drayveri Task<Int32> obyektini xabardor qiladi va state machine ni davom ettiradi. await operatori Task obyektining Result xususiyatini so'raydi (bu Int32) va natijani bytesRead lokal o'zgaruvchiga o'rnatadi. Keyin IssueClientRequestAsync ning qolgan kodi bajariladi — natija satrini va pipe ni yopishni qaytaradi.

IssueClientRequestAsync metodining qaytarish turi Task<String> ekanligiga e'tibor bering. Aslida u Task<String> obyektni qaytaradi — kompilyator yaratgan kod chaqiruvchiga ushbu Task<String> ni qaytaradi. IssueClientRequestAsync tanasining oxirida return "Done"; kabi satr qaytarilsa, kompilyator yaratgan kod Task<String> ning Result xususiyatini qaytarilgan satrga o'rnatadi.

Async Funksiyalar Bilan Bog'liq Cheklovlar

Async funksiyalar bilan bog'liq quyidagi cheklovlardan xabardor bo'lishingiz kerak:

  • Ilovangizning Main metodini async funksiyaga aylantira olmaysiz. Shuningdek, konstruktorlar, property accessor metodlari va event accessor metodlarini async funksiyaga aylantira olmaysiz.
  • Async funksiyada out yoki ref parametrlar bo'lishi mumkin emas.
  • await operatorini catch, finally yoki unsafe blok ichida ishlata olmaysiz.
  • await operatoridan oldin oqim egalik (ownership) yoki rekursiyani qo'llab-quvvatlaydigan lock ni ola olmaysiz va await operatoridan keyin uni bo'shata olmaysiz. Sababi shuki, await dan oldin bitta oqim kodni bajarishi mumkin, await dan keyin esa boshqa oqim. C# ning lock ifodasida await ishlatsangiz, kompilyator xato beradi. Agar aniq Monitor.Enter va Exit metodlarini ishlatsangiz, kod kompilyatsiya bo'ladi, lekin Monitor.Exit ish vaqtida SynchronizationLockException tashlaydi.
  • So'rov ifodasi (query expression) ichida await operatori faqat birinchi to'plam (from bandi) yoki join bandining to'plam ifodasi ichida ishlatilishi mumkin.

Bu cheklovlar nisbatan kichik. Agar ulardan birini buzsangiz, kompilyator sizga xabar beradi va siz kichik kod o'zgartirishlari bilan muammoni hal qilishingiz mumkin.

Kompilyator Async Funksiyani State Machine ga Qanday Aylantiradi

Async funksiyalar bilan ishlayotganda, agar kompilyatorning siz uchun bajarayotgan kod transformatsiyasini tushunsangiz, ular bilan yanada samaraliroq ishlaysiz. Va buni o'rganishning eng yaxshi yo'li — misol orqali o'tish. Keling, oddiy tip ta'riflari va metodlardan boshlaylik:

internal sealed class Type1 { }
internal sealed class Type2 { }
private static async Task<Type1> Method1Async() {
   /* Type1 obyektini qaytaradigan asinxron amal */
}
private static async Task<Type2> Method2Async() {
   /* Type2 obyektini qaytaradigan asinxron amal */
}

Endi bu oddiy turlar va metodlarni ishlatadigan async funksiyani ko'rib chiqaylik:

private static async Task<String> MyMethodAsync(Int32 argument) {
   Int32 local = argument;
   try {
      Type1 result1 = await Method1Async();
      for (Int32 x = 0; x < 3; x++) {
         Type2 result2 = await Method2Async();
      }
   }
   catch (Exception) {
      Console.WriteLine("Catch");
   }
   finally {
      Console.WriteLine("Finally");
   }
   return "Done";
}

MyMethodAsync sun'iy ko'rinsa-da, bir nechta muhim narsalarni ko'rsatadi. Birinchidan, u Task<String> qaytaradigan async funksiya, lekin tanasi aslida String qaytaradi. Ikkinchidan, u asinxron operatsiyalarni bajaradigan boshqa funksiyalarni chaqiradi — biri mustaqil, boshqasi for sikl ichidan. Nihoyat, u istisno boshqaruvi (exception handling) kodini ham o'z ichiga oladi.

MyMethodAsync ni kompilyatsiya qilganda, kompilyator ushbu metod kodini to'xtatib va davom ettirib bo'ladigan state machine strukturasiga aylantiradi.

State Machine: Kompilyator Yaratgan Kod

Men yuqoridagi kodni kompilyatsiya qilib, IL kodni C# ga qayta tiklash (reverse engineer) qildim. Keyin kodni soddalashtirib, async funksiyalar qanday ishlashini tushunishingiz uchun ko'p izohlar qo'shdim. Quyida transformatsiya qilingan MyMethodAsync metodi va u bog'liq bo'lgan state machine strukturasi ko'rsatilgan:

// AsyncStateMachine atributi asinxron metodni ko'rsatadi (refleksiya vositalari uchun yaxshi);
// tur state machine ni amalga oshiradigan strukturani ko'rsatadi
[DebuggerStepThrough, AsyncStateMachine(typeof(StateMachine))]
private static Task<String> MyMethodAsync(Int32 argument) {
   // State machine instansiyasini yaratish va initsializatsiya qilish
   StateMachine stateMachine = new StateMachine() {
      // Ushbu stub metodidan Task<String> qaytaradigan builder yaratish
      // State machine Task yakunlanishi/istisno uchun builderga murojaat qiladi
      m_builder = AsyncTaskMethodBuilder<String>.Create(),

      m_state = -1,               // State machine holatini initsializatsiya qilish
      m_argument = argument       // Argumentlarni state machine maydonlariga ko'chirish
   };

   // State machine ni ishga tushirish
   stateMachine.m_builder.Start(ref stateMachine);
   return stateMachine.m_builder.Task; // State machine ning Task ini qaytarish
}
// Bu state machine strukturasi
[CompilerGenerated, StructLayout(LayoutKind.Auto)]
private struct StateMachine : IAsyncStateMachine {
   // State machine ning builder (Task) va uning joylashuvi uchun maydonlar
   public AsyncTaskMethodBuilder<String> m_builder;
   public Int32 m_state;

   // Argument va lokal o'zgaruvchilar endi maydonlar:
   public Int32 m_argument, m_local, m_x;
   public Type1 m_resultType1;
   public Type2 m_resultType2;

   // Har bir awaiter turi uchun 1 ta maydon bor.
   // Har qanday vaqtda faqat 1 tasi muhim. Bu maydon hozirda
   // asinxron tugatilayotgan eng so'nggi bajarilgan await ga ishora qiladi:
   private TaskAwaiter<Type1> m_awaiterType1;
   private TaskAwaiter<Type2> m_awaiterType2;

   // Bu state machine metodining o'zi
   void IAsyncStateMachine.MoveNext() {
      String result = null;    // Task ning natija qiymati

      // Kompilyator kiritgan try bloki state machine task ning tugallanishini ta'minlaydi
      try {
         Boolean executeFinally = true;  // Mantiqan 'try' blokdan chiqayapmiz deb faraz qilamiz
         if (m_state == -1) {            // Agar state machine da birinchi marta
            m_local = m_argument;        // asl metod boshlanishini bajaramiz
         }

         // Asl kodimizda bo'lgan Try bloki
         try {
            TaskAwaiter<Type1> awaiterType1;
            TaskAwaiter<Type2> awaiterType2;

            switch (m_state) {
               case -1: // 'try' dagi kodni bajarishni boshlash
                  // Method1Async ni chaqirish va uning awaiter ini olish
                  awaiterType1 = Method1Async().GetAwaiter();
                  if (!awaiterType1.IsCompleted) {
                     m_state = 0;                       // 'Method1Async' asinxron
                                                        // tugatilmoqda
                     m_awaiterType1 = awaiterType1; // Qaytib kelganimizda uchun saqlash

                     // Operatsiya tugagach MoveNext ni chaqirishni awaiter ga aytish
                     m_builder.AwaitUnsafeOnCompleted(ref awaiterType1, ref this);
                     // Yuqoridagi satr awaiterType1 ning OnCompleted ni chaqiradi
                     // Bu taxminan kutilayotgan Task da
                     // ContinueWith(t => MoveNext()) ni chaqiradi.
                     // Task tugagach, ContinueWith task MoveNext ni chaqiradi

                     executeFinally = false;   // Mantiqan 'try' blokdan chiqmayapmiz
                     return;                   // Oqim chaqiruvchiga qaytadi
                  }
                  // 'Method1Async' sinxron tugallandi
                  break;

               case 0:   // 'Method1Async' asinxron tugallandi
                  awaiterType1 = m_awaiterType1;  // Eng so'nggi awaiter ni tiklash
                  break;

               case 1:   // 'Method2Async' asinxron tugallandi
                  awaiterType2 = m_awaiterType2;  // Eng so'nggi awaiter ni tiklash
                  goto ForLoopEpilog;
            }

            // Birinchi await dan keyin natijani olamiz va 'for' siklni boshlaymiz
            m_resultType1 = awaiterType1.GetResult(); // awaiter natijasini olish

         ForLoopPrologue:
            m_x = 0;           // 'for' sikl initsializatsiyasi
            goto ForLoopBody;  // 'for' sikl tanasiga o'tish

         ForLoopEpilog:
            m_resultType2 = awaiterType2.GetResult();
            m_x++;             // Har bir sikl iteratsiyasidan keyin x ni oshirish
            // 'for' sikl tanasiga tushish

         ForLoopBody:
            if (m_x < 3) {   // 'for' sikl testi
               // Method2Async ni chaqirish va uning awaiter ini olish
               awaiterType2 = Method2Async().GetAwaiter();
               if (!awaiterType2.IsCompleted) {
                  m_state = 1;                       // 'Method2Async' asinxron tugatilmoqda
                  m_awaiterType2 = awaiterType2; // Qaytib kelganimizda uchun saqlash

                  // Operatsiya tugagach MoveNext ni chaqirishni awaiter ga aytish
                  m_builder.AwaitUnsafeOnCompleted(ref awaiterType2, ref this);
                  executeFinally = false;   // Mantiqan 'try' blokdan chiqmayapmiz
                  return;                   // Oqim chaqiruvchiga qaytadi
               }
               // 'Method2Async' sinxron tugallandi
               goto ForLoopEpilog; // Sinxron tugallandi, siklni davom ettirish
            }
         }
         catch (Exception) {
            Console.WriteLine("Catch");
         }
         finally {
            // Oqim 'try' dan fizik chiqsa, 'finally' har doim bajariladi
            // Faqat oqim mantiqan 'try' dan chiqsa, shu kodni bajarishni xohlaymiz
            if (executeFinally) {
               Console.WriteLine("Finally");
            }
         }
         result = "Done"; // Async funksiyadan qaytarmoqchi bo'lgan narsa
      }
      catch (Exception exception) {
         // Boshqarilmagan istisno: state machine Task ini istisno bilan yakunlash
         m_builder.SetException(exception);
         return;
      }
      // Istisno yo'q: state machine Task ini natija bilan yakunlash
      m_builder.SetResult(result);
   }
}
Muhim: State Machine Mexanizmi

Yuqoridagi kodni sinchiklab o'qib, barcha izohlarni o'qishga vaqt ajratsangiz, kompilyator siz uchun qanday ishni bajarayotganini to'liq tushunishingiz mumkin. Lekin yana bir "yopishtiruvchi" qism borki, uni tushunish foydali: await operatorini kodda ishlatsangiz, kompilyator ko'rsatilgan operandni olib, unda GetAwaiter metodini chaqirishga urinadi. Bu metod instansiya metodi yoki kengaytma metodi bo'lishi mumkin. GetAwaiter chaqirilgandan qaytarilgan obyekt awaiter deb ataladi.

State Machine Qanday Ishlaydi

State machine awaiter ni olgach, uning IsCompleted xususiyatini so'raydi. Agar operatsiya sinxron tugallangan bo'lsa va true qaytarsa, state machine kodni to'g'ridan-to'g'ri davom ettiradi (optimallashtirish sifatida). Bu nuqtada awaiter ning GetResult metodi chaqiriladi — operatsiya muvaffaqiyatsiz bo'lsa istisno tashlaydi, muvaffaqiyatli bo'lsa natijani qaytaradi. State machine natijani qayta ishlash uchun shu joydan davom etadi.

Agar operatsiya asinxron tugallanayotgan bo'lsa, IsCompleted false qaytaradi. Bu holda state machine awaiter ning OnCompleted metodini chaqirib, state machine ning MoveNext metodiga delegatni uzatadi. Endi state machine oqimga qaytib kelgan joyiga qaytish imkonini beradi — boshqa kodlarni bajarishi uchun. Kelajakda awaiter, ichki Task ni o'rab turgan, Task tugallanganini biladi va MoveNext ni chaqiruvchi delegatni faollashtiradi. State machine ichidagi maydonlar kodda to'g'ri nuqtaga etib borishni ta'minlaydi — metod to'xtatilgan joydan davom etayotgandek illuziya yaratadi. Bu nuqtada kod awaiter ning GetResult metodini chaqiradi va natijani qayta ishlash uchun shu yerdan davom etadi.

Mana async funksiyalar shunday ishlaydi va butun maqsad — bloklashmasdan (non-blocking) kod yozish uchun odatda kerak bo'ladigan kodlash mehnatini soddalashtirish.

Async Funksiya Kengaytiruvchanligi

Kengaytiruvchanlik nuqtai nazaridan, agar kelajakda tugallaydigan operatsiya atrofida Task obyektni o'rasangiz, siz await operatorini ishlatib o'sha operatsiyani kutishingiz mumkin. Barcha turdagi asinxron operatsiyalarni ifodalash uchun bitta tip (Task) dan foydalanish juda foydali, chunki bu sizga kombinatorlarni (masalan, Task ning WhenAll va WhenAny metodlari) va boshqa foydali operatsiyalarni amalga oshirish imkonini beradi. Ushbu bobda keyinroq CancellationToken ni Task bilan o'rash orqali asinxron operatsiyani kutish va bekor qilish imkonini ham ko'rsataman.

Bundan tashqari, yana bir kengaytiruvchanlik nuqtasi bor: kompilyator await bilan ishlatiladigan har qanday operandda GetAwaiter ni chaqiradi. Shuning uchun operand Task obyekt bo'lishi shart emas — GetAwaiter metodi mavjud har qanday tip bo'lishi mumkin. Mana mening EventAwaiter klassim — u async metod state machine va event o'rtasidagi yopishtiruvchi qism bo'lgan o'z awaiter imni yaratish misoli:

public sealed class EventAwaiter<TEventArgs> : INotifyCompletion {
   private ConcurrentQueue<TEventArgs> m_events = new ConcurrentQueue<TEventArgs>();
   private Action m_continuation;

   #region State machine tomonidan chaqiriladigan a'zolar
   // State machine avval awaiter olish uchun buni chaqiradi; biz o'zimizni qaytaramiz
   public EventAwaiter<TEventArgs> GetAwaiter() { return this; }

   // State machine ga biror event yuz berganligini aytish
   public Boolean IsCompleted { get { return m_events.Count > 0; } }

   // State machine bizga keyinroq qaysi metodni chaqirishni aytadi; biz saqlaymiz
   public void OnCompleted(Action continuation) {
      Volatile.Write(ref m_continuation, continuation);
   }

   // State machine natijani so'raydi; bu await operatorning natijasi
   public TEventArgs GetResult() {
      TEventArgs e;
      m_events.TryDequeue(out e);
      return e;
   }
   #endregion

   // Event yuz berganda bir nechta oqim tomonidan chaqirilishi mumkin
   public void EventRaised(Object sender, TEventArgs eventArgs) {
      m_events.Enqueue(eventArgs);   // EventArgs ni GetResult/await uchun saqlash

      // Agar kutilayotgan davomiyat (continuation) bo'lsa, uni olamiz
      Action continuation = Interlocked.Exchange(ref m_continuation, null);
      if (continuation != null) continuation();   // State machine ni davom ettirish
   }
}

Mana EventAwaiter klassidan foydalanadigan va event yuz bergan sari await operatoridan qaytadigan metod. Bu holda state machine AppDomain da istalgan oqimda istisno yuz bergan har safar davom etadi:

private static async void ShowExceptions() {
   var eventAwaiter = new EventAwaiter<FirstChanceExceptionEventArgs>();
   AppDomain.CurrentDomain.FirstChanceException += eventAwaiter.EventRaised;

   while (true) {
      Console.WriteLine("AppDomain exception: {0}",
         (await eventAwaiter).Exception.GetType());
   }
}

public static void Go() {
   ShowExceptions();

   for (Int32 x = 0; x < 3; x++) {
      try {
         switch (x) {
            case 0: throw new InvalidOperationException();
            case 1: throw new ObjectDisposedException("");
            case 2: throw new ArgumentOutOfRangeException();
         }
      }
      catch { }
   }
}

TaskLogger: Kutilayotgan Operatsiyalarni Kuzatish

Yana bir misolni baham ko'rmoqchiman. Maning TaskLogger klassim sizga hali tugallanmagan asinxron operatsiyalarni ko'rsatish imkonini beradi. Bu ayniqsa ilovangiz javob bermayotgandek ko'ringanda — noto'g'ri so'rov yoki javob qaytarmaydigan server tufayli — debug qilishda juda foydali.

public static class TaskLogger {
   public enum TaskLogLevel { None, Pending }
   public static TaskLogLevel LogLevel { get; set; }

   public sealed class TaskLogEntry {
      public Task Task { get; internal set; }
      public String Tag { get; internal set; }
      public DateTime LogTime { get; internal set; }
      public String CallerMemberName { get; internal set; }
      public String CallerFilePath { get; internal set; }
      public Int32 CallerLineNumber { get; internal set; }
      public override string ToString() {
         return String.Format("LogTime={0}, Tag={1}, Member={2}, File={3}({4})",
            LogTime, Tag ?? "(none)", CallerMemberName, CallerFilePath, CallerLineNumber);
      }
   }

   private static readonly ConcurrentDictionary<Task, TaskLogEntry> s_log =
      new ConcurrentDictionary<Task, TaskLogEntry>();
   public static IEnumerable<TaskLogEntry> GetLogEntries() { return s_log.Values; }

   public static Task<TResult> Log<TResult>(this Task<TResult> task, String tag = null,
      [CallerMemberName] String callerMemberName = null,
      [CallerFilePath] String callerFilePath = null,
      [CallerLineNumber] Int32 callerLineNumber = -1) {
      return (Task<TResult>)
         Log((Task)task, tag, callerMemberName, callerFilePath, callerLineNumber);
   }

   public static Task Log(this Task task, String tag = null,
      [CallerMemberName] String callerMemberName = null,
      [CallerFilePath] String callerFilePath = null,
      [CallerLineNumber] Int32 callerLineNumber = -1) {
      if (LogLevel == TaskLogLevel.None) return task;
      var logEntry = new TaskLogEntry {
         Task = task,
         LogTime = DateTime.Now,
         Tag = tag,
         CallerMemberName = callerMemberName,
         CallerFilePath = callerFilePath,
         CallerLineNumber = callerLineNumber
      };
      s_log[task] = logEntry;
      task.ContinueWith(t => { TaskLogEntry entry; s_log.TryRemove(t, out entry); },
         TaskContinuationOptions.ExecuteSynchronously);
      return task;
   }
}

Va bu klassni ishlatishni ko'rsatadigan kod:

public static async Task Go() {
#if DEBUG
   // TaskLogger xotira va unumdorlikka ta'sir qiladi; faqat debug buildlarda yoqing
   TaskLogger.LogLevel = TaskLogger.TaskLogLevel.Pending;
#endif

   // 3 ta taskni boshlash; davomiyliklarini aniq boshqaramiz
   var tasks = new List<Task> {
      Task.Delay(2000).Log("2s op"),
      Task.Delay(5000).Log("5s op"),
      Task.Delay(6000).Log("6s op"),
   };

   try {
      // Barcha tasklar uchun kutish lekin 3 soniyadan keyin bekor qilish; faqat 1 task tugashi kerak
      // Eslatma: WithCancellation ushbu bobda keyinroq tasvirlanadigan kengaytma metodim
      await Task.WhenAll(tasks).
         WithCancellation(new CancellationTokenSource(3000).Token);
   }
   catch (OperationCanceledException) { }

   // Logger dan qaysi tasklar hali tugallanmaganini so'rash va
   // ularni eng uzoq kutilganidan tartiblash
   foreach (var op in TaskLogger.GetLogEntries().OrderBy(tle => tle.LogTime))
      Console.WriteLine(op);
}

Bu kodni kompilyatsiya qilib ishga tushirsam, quyidagi natijani olaman:

LogTime=7/16/2012 6:44:31 AM, Tag=6s op, Member=Go, File=C:\CLR via C#\Code\Ch28-1-IOOps.cs(332)
LogTime=7/16/2012 6:44:31 AM, Tag=5s op, Member=Go, File=C:\CLR via C#\Code\Ch28-1-IOOps.cs(331)

Maxsus Awaiter Yaratish

Task bilan foydalanishning barcha moslashuvchanligiga qo'shimcha ravishda, async funksiyalarda yana bir kengaytiruvchanlik nuqtasi bor: kompilyator await bilan ishlatiladigan har qanday operandda GetAwaiter ni chaqiradi. Shuning uchun operand Task obyekt bo'lishi shart emas; GetAwaiter metodi mavjud har qanday tip bo'lishi mumkin. Awaiter uchun quyidagi talablar mavjud:

  • IsCompleted xususiyati (Boolean) — operatsiya tugallanganini bildiradi
  • OnCompleted(Action) metodi — davomiyat (continuation) delegatini ro'yxatdan o'tkazadi
  • GetResult() metodi — natijani qaytaradi yoki istisnoni tashlaydi

Async Funksiyalar va Event Handlerlar

Async funksiyalarning qaytarish turi odatda Task yoki Task<TResult> bo'ladi — funksiyaning state machine tugallanishini ifodalash uchun. Lekin, void qaytarish turiga ega async funksiya aniqlash ham mumkin. Bu C# kompilyatori juda keng tarqalgan stsenariyni soddalashtirish uchun ruxsat bergan maxsus holat — asinxron event handler ni amalga oshirish.

Deyarli barcha event handler metodlari quyidagi imzoga mos keladi:

void EventHandlerCallback(Object sender, EventArgs e);

Lekin event handler ichida I/O operatsiyalarni bajarish odatiy holat. Masalan, foydalanuvchi UI elementini bosib fayl ochib, undan o'qiganda. UI sezgir bo'lib qolishi uchun bu I/O asinxron bo'lishi kerak. Event handler metod void qaytarish turiga ega va buni o'zgartirish mumkin emas. Shu sababli C# kompilyatori async funksiyalarga void qaytarish turiga ega bo'lishga ruxsat beradi — await operatorini bloklashmasdan I/O operatsiyalar bajarish uchun ishlata olasiz.

void qaytarish turli async funksiyada, kompilyator hali ham state machine yaratadi, lekin Task obyektni yaratmaydi — chunki uni ishlatish mumkin emas. Shu sababli void qaytaruvchi async funksiya state machine qachon tugallanganini bilishning imkoni yo'q.

Diqqat

void qaytaruvchi async funksiya boshqarilmagan istisno tashlasa, kompilyator yaratgan kod istisnoni chaqiruvchining sinxronizatsiya konteksti (synchronization context) yordamida qayta tashlaydi. GUI oqimida bajarilsa — GUI oqimi istisnoni qayta tashlaydi. GUI bo'lmagan oqimda — oqim havzasi oqimi istisnoni qayta tashlaydi. Odatda bunday istisnolarni qayta tashlash butun jarayonni tugatadi.

Framework Class Library (FCL) dagi Async Funksiyalar

Men shaxsan async funksiyalarni yaxshi ko'raman, chunki ularni o'rganish nisbatan oson, ishlatish sodda va ular FCL dagi ko'plab turlar tomonidan qo'llab-quvvatlanadi. Async funksiyalarni aniqlash oson, chunki qoidaga ko'ra, metodning nomiga Async qo'shimchasi qo'shiladi. FCL da I/O operatsiyalarini taklif qiladigan turlarning ko'pchiligi XxxAsync metodlarini taqdim etadi. Mana ba'zi misollar:

  • System.IO.Stream dan hosila bo'lgan barcha klasslar ReadAsync, WriteAsync, FlushAsync va CopyToAsync metodlarini taklif qiladi.
  • System.IO.TextReader dan hosila bo'lgan barcha klasslar ReadAsync, ReadLineAsync, ReadToEndAsync va ReadBlockAsync metodlarini taklif qiladi. System.IO.TextWriter dan hosila bo'lgan klasslar esa WriteAsync, WriteLineAsync va FlushAsync metodlarini taklif qiladi.
  • System.Net.Http.HttpClient klassi GetAsync, GetStreamAsync, GetByteArrayAsync, PostAsync, PutAsync, DeleteAsync va boshqa ko'plab metodlarni taklif qiladi.
  • System.Net.WebRequest dan hosila bo'lgan barcha klasslar (FileWebRequest, FtpWebRequest, HttpWebRequest) GetRequestStreamAsync va GetResponseAsync metodlarini taklif qiladi.
  • System.Data.SqlClient.SqlCommand klassi ExecuteDbDataReaderAsync, ExecuteNonQueryAsync, ExecuteReaderAsync, ExecuteScalarAsync va ExecuteXmlReaderAsync metodlarini taklif qiladi.
  • Veb-xizmat proksi turlarini yaratuvchi vositalar (masalan, SvcUtil.exe) ham XxxAsync metodlarini yaratadi.

.NET Framework ning eski versiyalarida ishlaganlar uchun yana ikkita eski asinxron dasturlash modeli bilan tanish bo'lishingiz mumkin: BeginXxx/EndXxx metodlarini IAsyncResult interfeysi bilan ishlatadigan APM (Asynchronous Programming Model) va event-based model (XxxAsync metodlari Task qaytarmaydigan). Bu ikkala eski model endi eskirgan deb hisoblanadi va Task obyektlaridan foydalanadigan yangi model afzaldir.

FCL ni ko'rib chiqsangiz, ba'zi klasslarda XxxAsync metodlari mavjud emasligini va faqat eski BeginXxx/EndXxx metodlarini taklif qilishini sezishingiz mumkin. Bu asosan Microsoft bu klasslarni yangi metodlar bilan yangilash uchun vaqt topmag anligi sababli. Kelajakda Microsoft bu klasslarni kengaytirishi kerak. Shu paytgacha eski BeginXxx/EndXxx modelini yangi Task-ga asoslangan modelga moslashtiruvchi yordamchi metod mavjud.

Avvalroq men nomlangan pipe orqali so'rov yuboradigan mijoz ilovasi kodini ko'rsatgan edim. Keling endi server tomonini ko'rib chiqaylik:

private static async void StartServer() {
   while (true) {
      var pipe = new NamedPipeServerStream(c_pipeName, PipeDirection.InOut, -1,
         PipeTransmissionMode.Message, PipeOptions.Asynchronous | PipeOptions.WriteThrough);

      // Asinxron ravishda mijoz ulanishini qabul qilish
      // ESLATMA: NamedPipServerStream eski APM modelini ishlatadi
      // Men TaskFactory ning FromAsync metodi orqali eski modelni moslashtiryapman
      await Task.Factory.FromAsync(pipe.BeginWaitForConnection, pipe.EndWaitForConnection,
         null);

      // Mijozga xizmat ko'rsatishni boshlash — bu darhol qaytadi chunki asinxron
      ServiceClientRequestAsync(pipe);
   }
}

NamedPipeServerStream klassida BeginWaitForConnection va EndWaitForConnection metodlari aniqlangan, lekin WaitForConnectionAsync metodi hali yo'q. Shuning uchun yuqoridagi kodda TaskFactory ning FromAsync metodini chaqirib, BeginXxx va EndXxx metod nomlarini uzatyapman. FromAsync ichki tarzda bu metodlarni o'rab turgan Task obyektini yaratadi. Endi Task obyektini await operatori bilan ishlata olaman.

Eski event-ga asoslangan dasturlash modeli uchun FCL hech qanday yordamchi metodlarni o'z ichiga olmaydi — siz buni qo'lda qilishingiz kerak. Mana WebClient ni (event-ga asoslangan modelni ishlatadi) TaskCompletionSource bilan o'rab async funksiyada await qilib bo'ladigan qilish misoli:

private static async Task<String> AwaitWebClient(Uri uri) {
   // System.Net.WebClient klassi Event-based Asynchronous Pattern ni qo'llab-quvvatlaydi
   var wc = new System.Net.WebClient();

   // TaskCompletionSource va uning ichki Task obyektini yaratish
   var tcs = new TaskCompletionSource<String>();

   // Satr yuklanib bo'lgach, WebClient event ni ko'taradi va TaskCompletionSource ni yakunlaydi
   wc.DownloadStringCompleted += (s, e) => {
      if (e.Cancelled) tcs.SetCanceled();
      else if (e.Error != null) tcs.SetException(e.Error);
      else tcs.SetResult(e.Result);
   };

   // Asinxron operatsiyani boshlash
   wc.DownloadStringAsync(uri);

   // Endi TaskCompletionSource ning Task ini olishimiz va natijani odatdagidek qayta ishlashimiz mumkin
   String result = await tcs.Task;
   // Natijani qayta ishlash (agar kerak bo'lsa)...

   return result;
}

Async Funksiyalar va Xatolar Boshqaruvi (Exception Handling)

Windows qurilma drayveri asinxron I/O so'rovini qayta ishlayotganda, biror narsa noto'g'ri ketishi mumkin. Masalan, baytlarni yuborish yoki tarmoqdan baytlar kutish paytida vaqt tugashi (timeout) mumkin. Agar ma'lumotlar o'z vaqtida kelmasa, qurilma drayveri sizga asinxron operatsiya xato bilan tugallanganini aytmoqchi bo'ladi. Buning uchun qurilma drayveri tugallangan IRP ni CLR ning oqim havzasiga joylaydi va oqim havzasi oqimi Task obyektini istisno bilan yakunlaydi. State machine metodi davom ettirilganda, await operatori operatsiya muvaffaqiyatsiz bo'lganini ko'radi va istisnoni tashlaydi.

27-bobda men Task obyektlari odatda AggregateException tashlagani va keyin siz bu istisnoning InnerExceptions xususiyatini so'rab asl istisno(lar)ni ko'rishingiz haqida gapirgan edim. Lekin await bilan Task ni ishlatganda, AggregateException o'rniga birinchi ichki istisno tashlanadi. Bu siz kutgan dasturlash tajribasini berish uchun qilingan. Aks holda, kodingiz davomida har doim AggregateException ni ushlashingiz, ichki istisnoni tekshirishingiz va uni qayta ishlashingiz yoki qayta tashlashingiz kerak bo'lar edi. Bu juda zerikarli bo'lardi.

Texnik tafsilot

Qiziquvchanlar uchun: AggregateException o'rniga birinchi ichki istisnoni tashlaydigan TaskAwaiter ning GetResult metodi.

Agar state machine metodi boshqarilmagan istisnoni boshdan kechirsa, async funksiyangizni ifodalovchi Task obyekti boshqarilmagan istisno bilan yakunlanadi. Bu Task ning tugallanishini kutayotgan har qanday kod istisnoni ko'radi. Lekin, void qaytarish turli async funksiyada bu maxsus holat: chaqiruvchi istisnoni aniqlash imkoni yo'q. void qaytaruvchi async funksiya boshqarilmagan istisno tashlasa, kompilyator yaratgan kod istisnoni chaqiruvchining sinxronizatsiya konteksti yordamida qayta tashlaydi. GUI oqimi bajarilsa — GUI oqimi istisnoni qayta tashlaydi. GUI bo'lmagan oqimda — oqim havzasi oqimi istisnoni qayta tashlaydi. Odatda bu jarayonni tugatadi.

Boshqa Async Funksiya Xususiyatlari

Men sizga async funksiyalarga bog'liq ba'zi qo'shimcha xususiyatlarni baham ko'rmoqchiman.

Visual Studio ning debug qo'llab-quvvatlashi juda yaxshi. await operatorida to'xtatilganda, qadam bosish (stepping over, F10) ishlatsa, debugger keyingi bayonotga o'tganda to'xtaydi. Bu kod boshqa oqimda, hatto await ni boshlagan oqimdan farqli oqimda bajarilishi mumkin. Bu debug qilishni juda soddalashtiradi.

Agar tasodifan async funksiya ichiga kirsangiz (F11 bilan step into), chiqish uchun Shift+F11 bosishingiz mumkin — lekin buni async funksiya ning ochilish qavsida turib bajarishingiz kerak. Qavsdan o'tib ketsangiz, step out (Shift+F11) async funksiya state machine oxirigacha ishlaydi. State machine tugallanmasdan oldin chaqiruvchi metodini debug qilish kerak bo'lsa, chaqiruvchi metodda breakpoint qo'ying va ishga tushiring (F5).

Ba'zi asinxron operatsiyalar juda tez tugallaydi. Bunday hollarda state machine ni to'xtatib va keyin boshqa oqimni davom ettirish samarasiz bo'ladi — state machine ni to'g'ridan-to'g'ri davom ettirish ancha samarali. Yaxshiyamki, await operatorining kompilyator yaratgan kodi buni tekshiradi. Agar asinxron operatsiya oqim qaytishidan oldin tugallansa, oqim qaytmaydi va shunchaki keyingi kod satrini bajaradi.

Async Lambda Ifodalari

Bu kod yana bir C# xususiyatini namoyish etadi: async lambda ifodalari. Ko'ryapsizki, await operatorini oddiy lambda ifoda tanasiga qo'yib bo'lmaydi, chunki kompilyator metodni state machine ga aylantirishni bilmaydi. Lekin lambda ifodadan oldin async qo'ysangiz, kompilyator lambda ifodani Task yoki Task<TResult> qaytaruvchi state machine metodiga aylantiradi. Natijani Func delegat o'zgaruvchisiga biriktirishingiz mumkin:

// Task.Run GUI oqimida chaqiriladi
Task.Run(async () => {
   // Bu kod oqim havzasi oqimida ishlaydi
   // TODO: Intensiv hisoblash ishlarini bajarish...

   await XxxAsync();   // Asinxron operatsiyani boshlash
   // Boshqa ishlarni davom ettirish...
});

Async funksiyani chaqirib, await operatorini qo'yishni unutish oson; quyidagi kod buni ko'rsatadi:

static async Task OuterAsyncFunction() {
   InnerAsyncFunction();     // Oops, bu satrda await operatorini qo'yish esdan chiqdi!

   // InnerAsyncFunction ham bajarilayotgan paytda bu yer davom etadi...
}

static async Task InnerAsyncFunction() { /* Bu yerdagi kod muhim emas */ }

Yaxshiyamki, buni qilganingizda, C# kompilyatori quyidagi ogohlantirishni beradi: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call. Bu yaxshi, lekin kamdan-kam hollarda siz haqiqatan ham InnerAsyncFunction tugallanishini kutishni xohlamasligingiz mumkin va kompilyator ogohlantirishini ko'rmaslikni istashingiz mumkin.

Kompilyator ogohlantirishini yo'qotish uchun shunchaki InnerAsyncFunction dan qaytarilgan Task ni o'zgaruvchiga o'rnating va keyin e'tiborsiz qoldiring:

static async Task OuterAsyncFunction() {
   var noWarning = InnerAsyncFunction(); // Bu satrda await qo'ymaslikni qasddan xohlayman.

   // InnerAsyncFunction ham bajarilayotgan paytda bu yer davom etadi...
}

Yoki men afzal ko'radigan kengaytma metodi:

[MethodImpl(MethodImplOptions.AggressiveInlining)]  // Kompilyatorga chaqiruvni optimallashtirish
public static void NoWarning(this Task task) { /* Kod yo'q */ }

Va uni quyidagicha ishlating:

static async Task OuterAsyncFunction() {
   InnerAsyncFunction().NoWarning(); // Bu satrda await qo'ymaslikni qasddan xohlayman.

   // InnerAsyncFunction ham bajarilayotgan paytda bu yer davom etadi...
}

Parallel I/O Operatsiyalar

Asinxron I/O operatsiyalarning eng ajoyib xususiyatlaridan biri — ko'plab operatsiyalarni bir vaqtda boshlashingiz va ularning barchasini parallel bajarishga qo'yishingiz mumkin. Bu ilovangizga ulkan unumdorlik oshishini beradi.

Men avvalroq nomlangan pipe serverni boshlagan va keyin ko'plab mijoz so'rovlarini yuborgan kodni ko'rsatmagan edim. Mana shu kod:

public static async Task Go() {
   // Serverni boshlash — u darhol qaytadi chunki
   // asinxron ravishda mijoz so'rovlarini kutadi
   StartServer(); // Bu void qaytaradi, shuning uchun kompilyator ogohlantirishiga e'tibor bermang

   // Ko'plab asinxron mijoz so'rovlarini yaratish; har bir mijozning Task<String> ini saqlash
   List<Task<String>> requests = new List<Task<String>>(10000);
   for (Int32 n = 0; n < requests.Capacity; n++)
      requests.Add(IssueClientRequestAsync("localhost", "Request #" + n));

   // Barcha mijoz so'rovlari tugalguncha asinxron kutish
   // ESLATMA: Agar 1+ task istisno tashlasa, WhenAll oxirgi istisnoni qayta tashlaydi
   String[] responses = await Task.WhenAll(requests);

   // Barcha javoblarni qayta ishlash
   for (Int32 n = 0; n < responses.Length; n++)
      Console.WriteLine(responses[n]);
}

Bu kod nomlangan pipe serverini ishga tushiradi va keyin for siklida 10,000 mijoz so'rovini iloji boricha tez boshlaydi. Har safar IssueClientRequestAsync chaqirilganda, u Task<String> obyektni qaytaradi va uni to'plamga qo'shaman. Nomlangan pipe server bu so'rovlarni iloji boricha tez qayta ishlaydi va oqim havzasi oqimlaridan foydalanib barcha protsessor yadrolarini band qilib turadi.

Men barcha mijoz so'rovlari tugalguncha natijalarni qayta ishlashdan oldin kutishni xohlayman. Buning uchun Task ning statik WhenAll metodini chaqiraman. Bu metod ichki tarzda List dagi barcha Task obyektlari tugagandan keyin tugallanadigan Task<String[]> obyektini yaratadi. Keyin men Task<String[]> ni await qilaman. Barcha tasklar tugagandan keyin, barcha javoblarni siklda ko'rsataman (Console.WriteLine).

Balki har bir javobni barcha javoblarni kutmasdan, tayyor bo'lgani sari qayta ishlashni afzal ko'rsangiz. Bunga Task ning statik WhenAny metodi yordamida erishish deyarli oson. Qayta yozilgan kod quyidagicha ko'rinadi:

public static async Task Go() {
   // Serverni boshlash — u darhol qaytadi chunki
   // asinxron ravishda mijoz so'rovlarini kutadi
   StartServer();

   // Ko'plab asinxron mijoz so'rovlarini yaratish; har bir mijozning Task<String> ini saqlash
   List<Task<String>> requests = new List<Task<String>>(10000);
   for (Int32 n = 0; n < requests.Capacity; n++)
      requests.Add(IssueClientRequestAsync("localhost", "Request #" + n));

   // HAR BIR task tugashi bilan qayta ishlash
   while (requests.Count > 0) {
      // Har bir tugallangan javobni ketma-ket qayta ishlash
      Task<String> response = await Task.WhenAny(requests);
      requests.Remove(response);   // Tugallangan taskni to'plamdan olib tashlash

      // Bitta mijoz javobini qayta ishlash
      Console.WriteLine(response.Result);
   }
}

Bu yerda while sikli har bir mijoz so'rovi uchun bir marta iteratsiya qiladi. Sikl ichida Task ning WhenAny metodini await qilaman — u bitta Task<String> ni qaytaradi va server javob bergan mijoz so'rovini bildiradi. Bu Task<String> obyektini olgach, uni to'plamdan olib tashlayman va natijani qayta ishlash uchun Console.WriteLine ga uzataman.

Ilovalar va Ularning Threading Modellari

.NET Framework bir necha xil ilova modellarini qo'llab-quvvatlaydi va har bir ilova modeli o'zining oqim (threading) modelini joriy qilishi mumkin.

Konsol ilovalari va Windows xizmatlari (aslida konsol ilovalari) hech qanday oqim modelini joriy etmaydi; ya'ni, har qanday oqim xohlagan ishni xohlagan vaqtda qilishi mumkin.

GUI ilovalar (Windows Forms, Windows Presentation Foundation — WPF, Silverlight va Windows Store ilovalari) esa UI elementini yaratgan oqim — faqat o'sha oqim uni yangilashi mumkin degan oqim modelini joriy etadi. GUI oqimi asinxron operatsiyani boshlash, bloklashmasdan foydalanuvchi kiritishlariga (sichqoncha, klaviatura, pen, touch) javob berish uchun tez-tez ishlatiladi. Lekin asinxron operatsiya tugalganda, oqim havzasi oqimi Task obyektini yakunlaydi va state machine ni davom ettiradi.

Ba'zi ilova modellari uchun bu yaxshi va hatto kerakli, chunki u samarali. Lekin ba'zi boshqa ilova modellari, masalan GUI ilovalar, uchun bu muammo — chunki kod UI elementlarini oqim havzasi oqimi orqali yangilashga urinsa, istisno tashlanadi. Qanday bo'lmasin, oqim havzasi oqimi GUI oqimi orqali UI elementlarini yangilashga erishishi kerak.

ASP.NET ilovalar esa har qanday oqimga xohlagan ishini qilishga ruxsat beradi. Oqim havzasi oqimi mijoz so'rovini qayta ishlashni boshlaganda, u mijozning madaniyatini (System.Globalization.CultureInfo) va identifikatsiyasini (System.Security.Principal.IPrincipal) qabul qilishi mumkin. Oqim havzasi oqimi asinxron operatsiyani boshlasa, u boshqa oqim havzasi oqimi tomonidan yakunlanishi mumkin. Yangi oqim asl mijoz so'rovi nomidan ishlaganligi sababli, madaniyat va identifikatsiya ma'lumotlari yangi oqimga "oqishi" kerak.

SynchronizationContext

FCL System.Threading.SynchronizationContext bazaviy klassini aniqlagan. Bu klass oddiy qilib aytganda SynchronizationContext dan hosila bo'lgan obyektni ilova modelining oqim modeliga ulaydi. FCL SynchronizationContext dan hosila bo'lgan bir nechta klasslarni aniqlagan, lekin odatda siz bu klasslar bilan bevosita ishlamaysiz; ko'pchiligi ochiq emas va hujjatlashtirilmagan.

Ko'pincha, ilova dasturchilari SynchronizationContext klassi haqida hech narsa bilishi shart emas. Task ni await qilganingizda, chaqiruvchi oqimning SynchronizationContext obyekti olinadi. Oqim havzasi oqimi Task ni yakunlaganda, SynchronizationContext obyekti sizning ilova modelingiz uchun to'g'ri oqim modelini ta'minlash uchun ishlatiladi. Shuning uchun, GUI oqimi Task ni await qilganda, await operatoridan keyingi kod GUI oqimida bajarilishi kafolatlangan — UI elementlarini yangilash imkonini beradi. ASP.NET ilova uchun await operatoridan keyingi kod mijozning madaniyati va identifikatsiyasiga ega oqim havzasi oqimida bajarilishi kafolatlanadi.

Deadlock Muammosi

Ko'p hollarda, state machine ni ilova modelining oqim modeli yordamida davom ettirish juda foydali va qulay. Lekin ba'zan bu sizni muammoga duchor qilishi mumkin. Mana WPF ilovasida deadlockka olib keladigan misol:

private sealed class MyWpfWindow : Window {
   public MyWpfWindow() { Title = "WPF Window"; }

   protected override void OnActivated(EventArgs e) {
      // Result xususiyatini so'rash GUI oqimning qaytishiga to'sqinlik qiladi;
      // oqim natijani kutib bloklaydi
      String http = GetHttp().Result;   // Satrni sinxron olish!

      base.OnActivated(e);
   }

   private async Task<String> GetHttp() {
      // HTTP so'rovni yuborish va oqimni GetHttp dan qaytishga ruxsat berish
      HttpResponseMessage msg = await new HttpClient().GetAsync("http://Wintellect.com/");
      // Bu yerga hech qachon etib kelmaymiz: GUI oqimi bu metod tugashini kutmoqda
      // lekin bu metod tugay olmaydi chunki GUI oqimi tugashini kutmoqda --> DEADLOCK!

      return await msg.Content.ReadAsStringAsync();
   }
}
Diqqat: Deadlock!

Yuqoridagi kodda OnActivated metodi GetHttp().Result ni chaqiradi — bu GUI oqimini GetHttp tugagunga qadar bloklaydi. GetHttp ichida await operatori HttpClient ning asinxron operatsiyasini boshlaydi va GUI oqimini qaytaradi. Lekin operatsiya tugagach, state machine GUI oqimida davom ettirilishi kerak (chunki SynchronizationContext GUI oqimini talab qiladi). GUI oqimi esa Result kutib bloklangan! Natija: deadlock.

ConfigureAwait: Deadlockdan Qochish

Klass kutubxonalarini yaratayotgan dasturchilar SynchronizationContext obyektidan xabardor bo'lishlari kerak. Bundan tashqari, klass kutubxonasi dasturchilar deadlock vaziyatlarini oldini olish uchun hamma narsani qilishlari kerak. Ikkala muammoni hal qilish uchun Task va Task<TResult> klasslari ConfigureAwait deb nomlangan metodni taklif qiladi:

// Task bu metodni aniqlaydi:
public ConfiguredTaskAwaitable ConfigureAwait(Boolean continueOnCapturedContext);

// Task<TResult> bu metodni aniqlaydi:
public ConfiguredTaskAwaitable<TResult> ConfigureAwait(Boolean continueOnCapturedContext);

Bu metodga true uzatish metodni umuman chaqirmaslik bilan bir xil xatti-harakatni beradi. Lekin false uzatsangiz, await operatori chaqiruvchi oqimning SynchronizationContext obyektini so'ramaydi va oqim havzasi oqimi Task ni yakunlaganda, await operatoridan keyingi kod shunchaki oqim havzasi oqimida bajariladi.

Hatto GetHttp metodim klass kutubxonasi kodi bo'lmasa ham, ConfigureAwait ni qo'shsam, deadlock muammosi hal bo'ladi:

private async Task<String> GetHttp() {
   // HTTP so'rovni yuborish va oqimni GetHttp dan qaytishga ruxsat berish
   HttpResponseMessage msg = await new HttpClient().GetAsync("http://Wintellect.com/")
      .ConfigureAwait(false);
   // Endi bu yerga oqim havzasi oqimi orqali etib kelamiz,
   // GUI oqimini majburlamasdan
   // as opposed to forcing the GUI thread to execute it.

   return await msg.Content.ReadAsStringAsync().ConfigureAwait(false);
}

Yuqoridagi kodda ko'rsatilganidek, ConfigureAwait(false) ni har bir await qiladigan Task obyektiga qo'llash kerak. Sababi shundaki, asinxron operatsiyalar sinxron tugallashi mumkin va bunday holda chaqiruvchi oqim shunchaki davom etadi; siz hech qachon qaysi operatsiya sinxron tugashini bilmaysiz, shuning uchun hammasiga aytishingiz kerak. Bu shuni ham anglatadiki, sizning klass kutubxonasi kodingiz ilova modeliga bog'liq bo'lmasligi kerak.

Muqobil variant sifatida, GetHttp metodimni quyidagicha qayta yozishim mumkin — butun kod oqim havzasi oqimida bajariladi:

private Task<String> GetHttp() {
   return Task.Run(async () => {
      // Biz oqim havzasi oqimida ishlaymiz — SynchronizationContext yo'q
      HttpResponseMessage msg = await new HttpClient().GetAsync("http://Wintellect.com/");
      // Bu yerga etib kelamiz chunki oqim havzasi oqimi bu kodni bajara oladi

      return await msg.Content.ReadAsStringAsync();
   });
}

Bu versiyada e'tibor bering, GetHttp metodi endi async funksiya emas; men async kalit so'zini metod imzosidan olib tashladim, chunki metodda await operatori yo'q. Boshqa tomondan, Task.Run ga uzatgan lambda ifodasi async funksiya.

Serverni Asinxron Amalga Oshirish

Ko'p dasturchilar bilan gaplashib, men ularning ko'pchiligi .NET Framework ning asinxron serverlarni yaratish uchun ajoyib qo'llab-quvvatlashga ega ekanligidan xabarsiz ekanligini aniqladim. Men ushbu kitobda har bir server turi uchun buni qanday qilishni tushuntira olmayman, lekin MSDN hujjatlarida nimani qidirish kerakligini sanab o'taman:

  • Asinxron ASP.NET Web Forms: .aspx faylingizda sahifangizga "Async=true" direktivasini qo'shing va System.Web.UI.Page ning RegisterAsyncTask metodini ko'ring.
  • Asinxron ASP.NET MVC kontroller: kontroller klassingizni System.Web.Mvc.AsyncController dan hosila qilib, action metodingiz Task<ActionResult> qaytarsin.
  • Asinxron ASP.NET handler: klassingizni System.Web.HttpTaskAsyncHandler dan hosila qilib, ProcessRequestAsync abstrakt metodini override qiling.
  • Asinxron WCF xizmati: xizmatingizni async funksiya sifatida amalga oshiring va Task yoki Task<TResult> qaytarsin.

I/O Operatsiyalarni Bekor Qilish (Canceling I/O Operations)

Umuman olganda, Windows sizga davom etayotgan asinxron I/O operatsiyani bekor qilish yo'lini bermaydi. Bu ko'p dasturchilar xohlagan xususiyat, lekin amalda amalga oshirish juda qiyin. Axir, serverga so'rov yuborsangiz va keyin javobni xohlamasligingizga qaror qilsangiz, serverga asl so'rovingizni tark etishni aytish imkoni yo'q. Bu holatda baytlarni kelishiga ruxsat berib, keyin ularni tashlab yuborish yo'li. Bundan tashqari, poyga holati (race condition) bor — bekor qilish so'rovingiz server javob yuborayotgan paytda kelishi mumkin. Endi ilovangiz nima qilishi kerak?

Bunga yordam berish uchun, Task<TResult> ni kengaytiradigan WithCancellation kengaytma metodi yaratishni tavsiya qilaman (siz Task ni kengaytiradigan o'xshash overload ham kerak):

private struct Void { } // Generik bo'lmagan TaskCompletionSource klassi yo'qligi sababli.

private static async Task<TResult> WithCancellation<TResult>(this Task<TResult> originalTask,
   CancellationToken ct) {

   // CancellationToken bekor qilinganda tugallaydigan Task yaratish
   var cancelTask = new TaskCompletionSource<Void>();

   // CancellationToken bekor qilinganda Task ni yakunlash
   using (ct.Register(
      t => ((TaskCompletionSource<Void>)t).TrySetResult(new Void()), cancelTask)) {

      // Asl task yoki CancellationToken Task idan birontasi tugashini kutadigan Task yaratish
      Task any = await Task.WhenAny(originalTask, cancelTask.Task);

      // Agar biron Task CancellationToken tufayli tugasa, OperationCanceledException tashlash
      if (any == cancelTask.Task) ct.ThrowIfCancellationRequested();
   }

   // Asl taskni await qilish (sinxron); muvaffaqiyatsiz bo'lsa, await uni
   // AggregateException o'rniga birinchi ichki istisnoni tashlaydi
   return await originalTask;
}

Bu kengaytma metodini quyidagicha chaqirish mumkin:

public static async Task Go() {
   // CancellationTokenSource yaratish — # millisekunddan keyin o'zini bekor qiladi
   var cts = new CancellationTokenSource(5000); // Tezroq bekor qilish uchun cts.Cancel() chaqiring
   var ct = cts.Token;

   try {
      // Men sinov uchun Task.Delay ishlatdim; buni Task qaytaradigan boshqa metod bilan almashtiring
      await Task.Delay(10000).WithCancellation(ct);
      Console.WriteLine("Task completed");
   }
   catch (OperationCanceledException) {
      Console.WriteLine("Task cancelled");
   }
}

WithCancellation Metodi: Batafsil Tushuntirish

WithCancellation metodi ikkita Task yaratadi: biri asl asinxron operatsiyani ifodalaydi, ikkinchisi CancellationToken bekor qilinganda tugallaydi. Keyin Task.WhenAny ni ishlatib, ikkisidan biri tugashini kutadi.

Agar CancellationToken task birinchi tugasa — demak bekor qilish so'rovi kelgan, shuning uchun OperationCanceledException tashlanadi. Agar asl task birinchi tugasa — hammasi yaxshi va natija qaytariladi.

Ba'zi I/O Operatsiyalar Sinxron Bajarilishi Kerak

Win32 API I/O operatsiyalarni bajaradigan ko'plab funksiyalarni taklif qiladi. Afsuski, bu metodlarning ba'zilari sizga I/O ni asinxron bajarishga ruxsat bermaydi. Masalan, Win32 ning CreateFile funksiyasi (FileStream konstruktori chaqiradi) doimo sinxron ishlaydi. Agar tarmoq serveridagi faylni ochmoqchi yoki yaratmoqchi bo'lsangiz, CreateFile qaytgunga qadar bir necha soniya kutish kerak bo'lishi mumkin — chaqiruvchi oqim bo'sh o'tiradi.

Optimal sezgirlik va kengayuvchanlik uchun mo'ljallangan ilova ideal holatda sizga faylni asinxron yaratish yoki ochish imkonini beradigan Win32 funksiyasini chaqirishi kerak. Afsuski, Win32 bunday CreateFile ga o'xshash funksiya taklif etmaydi va shuning uchun FCL ham faylni asinxron ochish yo'lini taklif eta olmaydi.

Windows shuningdek reyestrga asinxron kirish, event logga kirish, katalog fayllari/pastki kataloglarini olish, fayl/katalog atributlarini o'zgartirish funksiyalarini ham taklif etmaydi.

Buning amaldagi muammosiga misol: oddiy UI boshqaruvi yozishni tasavvur qiling — fayl yo'lini yozish va avtomatik to'ldirish (File Open dialogi ga o'xshash). Bu boshqaruv kataloglarni skanerlash uchun alohida oqimlar ishlatishi kerak, chunki Windows fayllarni asinxron ro'yxatlash funksiyasini taklif etmaydi. Foydalanuvchi yozishni davom ettirsa, yangi oqimlar ishlatib eski natijalarni e'tiborsiz qoldirish kerak.

Windows Vista dan boshlab Microsoft yangi Win32 funksiyasini taqdim etdi — CancelSynchronousIO. Bu funksiya bitta oqimga boshqa oqim bajarayotgan sinxron I/O operatsiyani bekor qilish imkonini beradi. Bu funksiya FCL tomonidan ochilmagan, lekin P/Invoke orqali chaqirish mumkin.

Eslatma

Muhim xulosa shuki, ko'p odamlar sinxron API lar bilan ishlash osonroq deb o'ylaydi va ko'p hollarda bu to'g'ri. Lekin ba'zi hollarda sinxron API lar ishlarni qiyinlashtiradi. Windows Runtime ni loyihalashda, Windows jamoasi barcha I/O operatsiyalarni asinxron bajaradigan metodlarni ochishga qaror qildi. Aslida, Windows Runtime sinxron I/O bajaradigan hech qanday API taklif etmaydi. C# ning async funksiya xususiyati bu API larni chaqirishda kodlashni soddalashtiradi.

FileStream ga Xos Muammolar

FileStream obyektini yaratganingizda, FileOptions.Asynchronous bayrog'i orqali sinxron yoki asinxron operatsiyalardan foydalanishni belgilashingiz mumkin (bu Win32 ning CreateFile funksiyasini FILE_FLAG_OVERLAPPED bayrog'i bilan chaqirish bilan teng).

Bayroq ko'rsatilmasa: Windows barcha operatsiyalarni fayl ustida sinxron bajaradi. Albatta, siz hali ham FileStream ning ReadAsync metodini chaqirishingiz mumkin va ilovangiz uchun u asinxron ko'rinadi, lekin ichki tarzda FileStream klassi asinxron xatti-harakatni emulyatsiya qilish uchun boshqa oqimdan foydalanadi; bu behuda va unumdorlikka zarar beradi.

Bayroq ko'rsatilsa: Windows barcha operatsiyalarni fayl ustida asinxron bajaradi. Lekin siz hali ham FileStream ning Read metodini chaqirshingiz mumkin — sinxron operatsiya bajarish uchun. Ichki tarzda FileStream klassi asinxron operatsiyani boshlash va keyin chaqiruvchi oqimni operatsiya tugagunga qadar uyquga qo'yish orqali sinxron xatti-harakatni emulyatsiya qiladi. Bu ham samarasiz, lekin FileOptions.Asynchronous bayrog'isiz yaratilgan FileStream da ReadAsync chaqirishdan ko'ra yaxshiroq.

Xulosa qilib aytganda, FileStream bilan ishlayotganda, sinxron yoki asinxron I/O ni faylga nisbatan bajarishingizni oldindan hal qilishingiz kerak:

  • FileOptions.Asynchronous bayrog'ini ko'rsatsangiz — doimo ReadAsync ni chaqiring.
  • Bayroq ko'rsatmasangiz — doimo Read ni chaqiring.
  • Ba'zi sinxron, ba'zi asinxron operatsiyalar bajarmoqchi bo'lsangiz — FileOptions.Asynchronous bayrog'i bilan yaratish samaraliroq.
  • Muqobil variant: bitta FileStream ni asinxron I/O uchun, ikkinchisini sinxron I/O uchun oching.

System.IO.File klassining yordamchi metodlari (Create, Open, OpenWrite) FileStream obyektlarini yaratib qaytaradi, lekin hech biri FileOptions.Asynchronous bayrog'ini ko'rsatmaydi. Sezgir yoki kengayuvchan ilovalar yaratmoqchi bo'lsangiz, bu metodlardan foydalanishdan saqlaning.

Muhim

NTFS fayl tizimi drayveri ba'zi operatsiyalarni qanday faylni ochganligingizdan qat'i nazar sinxron bajarishini bilishingiz kerak. Batafsil ma'lumot uchun Microsoft qo'llab-quvvatlash maqolasini ko'ring.

I/O So'rov Ustuvorliklari (I/O Request Priorities)

26-bobda, "Oqim Asoslari"da, men oqim ustuvorliklari (thread priorities) oqimlarning qanday rejalashtirilishiga ta'sir qilishini ko'rsatgan edim. Lekin oqimlar turli qurilmalardan ma'lumot o'qish va yozish uchun I/O so'rovlarini ham bajaradi. Past ustuvorlikli oqim protsessor vaqti olsa, u bir zumda yuzlab yoki minglab I/O so'rovlarini navbatga qo'yishi mumkin. I/O so'rovlarini qayta ishlash odatda vaqt talab qilganligi sababli, past ustuvorlikli oqim tizimning sezgirligiga jiddiy ta'sir qilishi mumkin — yuqori ustuvorlikli oqimlarni ularning ishlarini bajarishdan to'xtatib qo'yishi mumkin. Shu sababli, disk defragmenterlari, virus skanerlari, kontentni indekslovchilar kabi uzoq ishlovchi past ustuvorlikli xizmatlarni ishga tushirganingizda mashinaning sekinlashganini sezishingiz mumkin.

Windows I/O so'rovlari uchun ustuvorlik belgilash imkonini beradi. Batafsil ma'lumot uchun Microsoft ning texnik hujjatiga qarang. Afsuski, FCL bu funksiyani hali qo'llab-quvvatlamaydi, lekin P/Invoke orqali Win32 funksiyalarini chaqirish mumkin. Mana P/Invoke kodi:

internal static class ThreadIO {
   public static BackgroundProcessingDisposer BeginBackgroundProcessing(
      Boolean process = false) {

      ChangeBackgroundProcessing(process, true);
      return new BackgroundProcessingDisposer(process);
   }

   public static void EndBackgroundProcessing(Boolean process = false) {
      ChangeBackgroundProcessing(process, false);
   }

   private static void ChangeBackgroundProcessing(Boolean process, Boolean start) {
      Boolean ok = process
         ? SetPriorityClass(GetCurrentWin32ProcessHandle(),
              start ? ProcessBackgroundMode.Start : ProcessBackgroundMode.End)
         : SetThreadPriority(GetCurrentWin32ThreadHandle(),
              start ? ThreadBackgroundgMode.Start : ThreadBackgroundgMode.End);
      if (!ok) throw new Win32Exception();
   }

   // Bu struktura C# ning using ifodasida fon qayta ishlash rejimini yakunlaydi
   public struct BackgroundProcessingDisposer : IDisposable {
      private readonly Boolean m_process;
      public BackgroundProcessingDisposer(Boolean process) { m_process = process; }
      public void Dispose() { EndBackgroundProcessing(m_process); }
   }

   // Win32 ning THREAD_MODE_BACKGROUND_BEGIN va THREAD_MODE_BACKGROUND_END
   private enum ThreadBackgroundgMode { Start = 0x10000, End = 0x20000 }

   // Win32 ning PROCESS_MODE_BACKGROUND_BEGIN va PROCESS_MODE_BACKGROUND_END
   private enum ProcessBackgroundMode { Start = 0x100000, End = 0x200000 }

   [DllImport("Kernel32", EntryPoint = "GetCurrentProcess", ExactSpelling = true)]
   private static extern SafeWaitHandle GetCurrentWin32ProcessHandle();

   [DllImport("Kernel32", ExactSpelling = true, SetLastError = true)]
   [return: MarshalAs(UnmanagedType.Bool)]
   private static extern Boolean SetPriorityClass(
      SafeWaitHandle hprocess, ProcessBackgroundMode mode);

   [DllImport("Kernel32", EntryPoint = "GetCurrentThread", ExactSpelling = true)]
   private static extern SafeWaitHandle GetCurrentWin32ThreadHandle();

   [DllImport("Kernel32", ExactSpelling = true, SetLastError = true)]
   [return: MarshalAs(UnmanagedType.Bool)]
   private static extern Boolean SetThreadPriority(
      SafeWaitHandle hThread, ThreadBackgroundgMode mode);

   // http://msdn.microsoft.com/en-us/library/aa480216.aspx
   [DllImport("Kernel32", SetLastError = true, EntryPoint = "CancelSynchronousIo")]
   [return: MarshalAs(UnmanagedType.Bool)]
   private static extern Boolean CancelSynchronousIO(SafeWaitHandle hThread);
}

Uni quyidagicha ishlatish mumkin:

public static void Main () {
   using (ThreadIO.BeginBackgroundProcessing()) {
      // Bu yerda past ustuvorlikli I/O so'rovlarini yuboring (masalan, ReadAsync/WriteAsync chaqiruvlar)
   }
}

Windows ga oqimingiz past ustuvorlikli I/O so'rovlarini yuborishni xohlashingizni ThreadIO ning BeginBackgroundProcessing metodi orqali aytasiz. E'tibor bering, bu oqimning protsessor rejalashtirish ustuvorligini ham pasaytiradi. Oqimni oddiy ustuvorlikli I/O so'rovlariga (va oddiy protsessor rejalashtirish ustuvorligiga) qaytarish uchun EndBackgroundProcessing ni chaqiring yoki BeginBackgroundProcessing qaytargan qiymatda Dispose ni chaqiring (yuqoridagi kodda ko'rsatilganidek C# ning using ifodasi orqali). Oqim faqat o'zining fon qayta ishlash rejimiga ta'sir qilishi mumkin; Windows bitta oqimga boshqa oqimning fon qayta ishlash rejimini o'zgartirishga ruxsat bermaydi.

Agar jarayondagi barcha oqimlarning past ustuvorlikli I/O so'rovlarini yuborishi va past protsessor rejalashtirish ustuvorligiga ega bo'lishini xohlasangiz, BeginBackgroundProcessing ga process parametri uchun true uzating. Jarayon faqat o'zining fon qayta ishlash rejimiga ta'sir qilishi mumkin; Windows bitta jarayonga boshqa jarayonning fon qayta ishlash rejimini o'zgartirishga ruxsat bermaydi.

Muhim: Ustuvorlik Inversiyasi

Dasturchi sifatida ushbu yangi fon ustuvorliklarini ustuvorlik inversiyasi (priority inversion) ni hisobga olgan holda ishlatish sizning mas'uliyatingiz. Intensiv oddiy ustuvorlikli I/O operatsiyalar mavjud bo'lganda, fon ustuvorlikda ishlaydigan oqim I/O so'rovlari natijasini olish uchun soniyalab kutishi mumkin. Agar past ustuvorlikli oqim oddiy ustuvorlikli oqim kutayotgan oqim sinxronizatsiya qulfini egallab turgan bo'lsa, oddiy ustuvorlikli oqimlar past ustuvorlikli I/O so'rovlari tugaguncha kutishga majbur bo'lishi mumkin. Fon ustuvorlikli oqimingiz muammo bo'lishi uchun I/O yuborishining ham zaruriyati yo'q. Shuning uchun oddiy ustuvorlikli va fon ustuvorlikli oqimlar o'rtasidagi umumiy sinxronizatsiya obyektlaridan foydalanishni minimallashtirish (yoki iloji boricha yo'qotish) kerak — oddiy ustuvorlikli oqimlar fon ustuvorlikli oqimlar egallab turgan qufllar tufayli bloklangan holda qolishdan saqlash uchun.