aidoc.uz

1-qism · Dart asoslari·03-bob

Asinxron dasturlash — Future, Stream va Isolate

Event loop, Future va async/await, Stream turlari va Isolate — Flutter'da UI qotib qolmasligi uchun asosiy bo'lgan mavzular chuqur tahlili.

Flutter — bitta UI thread'da ishlaydigan freymvork. Agar shu thread biror og'ir ishni (masalan, katta faylni parslash yoki og'ir hisob-kitobni) bloklab qo'ysa, butun ilova "muzlab" qoladi — animatsiyalar to'xtaydi, tugmalar bosilmay qoladi. Shuning uchun asinxron dasturlashni chuqur tushunish — bu Flutter'da shunchaki "qulay xususiyat" emas, balki ilovani ishlaydigan qilishning o'zagi. Senior intervyusida sizdan async/await sintaksisini bilishdan tashqari, nega va qanday ishlashini — event loop darajasida — tushuntirish kutiladi.

1. Dart'ning bitta-threadli modeli va event loop#

Dart dasturi (shu jumladan Flutter ilovasi) bitta asosiy isolate'da, bitta thread'da ishlaydi. Parallellik illyuziyasi event loop orqali yaratiladi: kod hech qachon bir vaqtning o'zida ikki joyda ishlamaydi, lekin uzoq operatsiyalar (tarmoq so'rovi, fayl o'qish, timer) "navbatga qo'yiladi" va natija tayyor bo'lganda navbat orqali qayta ishga tushiriladi.

Dart'da ikkita navbat mavjud, va ularning ustuvorligi bir xil emas:

Microtask navbati
Yuqori ustuvorlik — Future.then/async natijalari shu yerga tushadi, event navbatidan OLDIN to'liq bo'shatiladi
Event navbati
Timer, I/O, foydalanuvchi kirishi (gesture), tarmoq javoblari shu yerda kutadi
void main() {
  print('1: sinxron');
 
  Future(() => print('4: event navbatidan'));       // event queue'ga tushadi
 
  Future.microtask(() => print('3: microtask'));     // microtask queue'ga tushadi
 
  print('2: sinxron');
}
// Natija: 1, 2, 3, 4

Bu tartib tasodifiy emas: Dart har doim microtask navbatini to'liq bo'shatib bo'lgandan keyingina event navbatidagi keyingi elementga o'tadi. Agar microtask'lar ichida yangi microtask'lar yaratilaversa (masalan, zanjirlangan .then() chaqiruvlari), bu event navbatini "och qoldirishi" (starvation) mumkin — bu real loyihalarda uchraydigan, lekin kamdan-kam tushuntiriladigan performance muammosi.

2. Future — bitta, kelajakdagi qiymat#

Future<String> malumotYukla() async {
  await Future.delayed(Duration(seconds: 2));   // tarmoq so'rovini simulyatsiya qiladi
  return "Yuklangan ma'lumot";
}
 
void main() async {
  print("Boshlandi");
  final natija = await malumotYukla();   // shu yerda kod "to'xtaydi", lekin thread band emas
  print(natija);
  print("Tugadi");
}
// Natija: "Boshlandi" darhol, 2 soniyadan keyin "Yuklangan ma'lumot" va "Tugadi"

Muhim tushuncha: await thread'ni bloklamaydi. awaitga yetganda, Dart joriy funksiyani "to'xtatadi" (uning holatini saqlab qo'yadi) va event loop'ni boshqa ishlarni (masalan, boshqa widget'larni qayta chizishni) bajarishga qo'yib beradi. Future bajarilib bo'lgach, funksiya to'xtagan joyidan davom etadi. Bu — Future ning aynan JavaScript'dagi Promise'dan farqli, lekin konseptual jihatdan juda o'xshash ishlash tamoyili.

Future.wait — parallel bajarish#

Future<void> parallelYuklash() async {
  final natijalar = await Future.wait([
    malumotYukla(),
    boshqaMalumotYukla(),
    uchinchiMalumotYukla(),
  ]);
  // Barcha uchtasi TAXMINAN bir vaqtda (interleaved) boshlanadi,
  // hammasi tugagach davom etadi. Ketma-ket await qilishdan tezroq.
}
 
// Ketma-ket (sekin, agar ular bir-biriga bog'liq bo'lmasa) — umumiy vaqt = yig'indi
Future<void> ketmaKetYuklash() async {
  final a = await malumotYukla();          // 2 soniya kutadi
  final b = await boshqaMalumotYukla();    // yana 2 soniya kutadi
  // Jami: ~4 soniya
}

Future.wait bilan parallel yuklash — real ilovalarda tez-tez uchraydigan optimallashtirish: agar bir nechta so'rov bir-biriga bog'liq bo'lmasa (masalan, foydalanuvchi profili va uning buyurtmalari tarixi), ularni ketma-ket emas, parallel yuborish umumiy kutish vaqtini sezilarli qisqartiradi.

Xatolarni boshqarish#

Future<String> xavfliOperatsiya() async {
  try {
    final natija = await tarmoqSorovi();
    return natija;
  } on TimeoutException {
    return "Vaqt tugadi";
  } on SocketException catch (e) {
    return "Tarmoq xatosi: ${e.message}";
  } catch (e, stackTrace) {
    // umumiy fallback — lekin production kodda hech qachon jim yutib yubormang
    debugPrint('Kutilmagan xato: $e\n$stackTrace');
    rethrow;
  } finally {
    yuklashniToxtat();   // har doim bajariladi — muvaffaqiyat yoki xato bo'lishidan qat'i nazar
  }
}

.catchError() bilan try/catch orasidagi farq muhim: .catchError() faqat Future zanjiri ichida ishlaydi va noto'g'ri ishlatilganda xatoni "yutib yuborishi" (silently swallow) oson, try/catch esa async/await sintaksisi bilan sinxron kodga o'xshab, aniqroq va o'qilishi osonroq xato boshqaruvini beradi — shuning uchun zamonaviy Dart kodida try/catch afzal ko'riladi.

3. Stream — vaqt bo'ylab ko'p qiymat#

Future — bitta qiymat (yoki xato) bilan bir marta yakunlanadi. Stream esa vaqt davomida nechta xohlasa, shuncha qiymat yetkazib berishi mumkin.

Stream<int> sanoq() async* {
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;   // Stream'ga yangi qiymat yuboradi
  }
}
 
void main() async {
  await for (final son in sanoq()) {
    print(son);   // 1, 2, 3, 4, 5 — har biri 1 soniya oralig'ida
  }
}

Single-subscription vs broadcast Stream#

// Single-subscription — faqat BITTA listener bo'lishi mumkin (masalan, fayl o'qish)
final faylOqish = File('data.txt').openRead();
 
// Broadcast — bir nechta listener bo'lishi mumkin (masalan, foydalanuvchi harakatlari)
final controller = StreamController<String>.broadcast();
controller.stream.listen((data) => print('Listener 1: $data'));
controller.stream.listen((data) => print('Listener 2: $data'));
controller.add('yangilik');   // ikkala listener ham oladi

Nega bu farq muhim? Single-subscription Stream ma'lumotni faqat bitta listen() chaqiruvi bo'lgandagina "oqiza" boshlaydi va uni bufer'da saqlaydi agar listener tayyor bo'lmasa (backpressure). Ikkinchi marta listen() chaqirilsa — StateError. Broadcast Stream esa istalgan sondagi listener'ga ruxsat beradi, lekin listener obuna bo'lishidan oldingi hodisalarni "eslamaydi" — kech qo'shilgan listener avvalgi qiymatlarni yo'qotadi. Flutter'da StreamBuilder odatda broadcast Stream bilan ishlatiladi, chunki widget qayta qurilganda qayta obuna bo'lish kerak bo'lishi mumkin.

StreamController — Stream'ni qo'lda boshqarish#

class HisoblagichServis {
  final _controller = StreamController<int>.broadcast();
  int _qiymat = 0;
 
  Stream<int> get qiymatOqimi => _controller.stream;
 
  void oshirish() {
    _qiymat++;
    _controller.add(_qiymat);   // yangi qiymatni barcha listener'larga yuboradi
  }
 
  void dispose() {
    _controller.close();   // XOTIRA OQIB KETISHINI oldini olish uchun MAJBURIY
  }
}

4. Isolate — chinakam parallellik#

async/await va Stream — bularning barchasi bitta thread ichida concurrency (navbat almashtirish) yaratadi, lekin chinakam parallellik emas. Agar sizga haqiqatan CPU'ni band qiladigan og'ir hisob-kitob kerak bo'lsa (masalan, katta JSON'ni parslash, rasmni qayta ishlash, shifrlash), await bu yerda yordam bermaydi — chunki hisob-kitob o'zi sinxron va uni "to'xtatib" boshqa ishga o'tib bo'lmaydi.

// YOMON: katta hisob-kitob asosiy isolate'ni bloklaydi
Future<int> ogirHisoblash(List<int> royxat) async {
  int yigindi = 0;
  for (final son in royxat) {
    yigindi += juda_murakkab_funksiya(son);   // UI shu vaqt qotib qoladi
  }
  return yigindi;
}
 
// YAXSHI: compute() alohida Isolate'da ishga tushiradi
Future<int> ogirHisoblashIsolateBilan(List<int> royxat) async {
  return compute(_ogirHisoblashSinxron, royxat);
}
 
int _ogirHisoblashSinxron(List<int> royxat) {
  // Bu funksiya BOSHQA isolate'da, mustaqil xotira bilan ishlaydi
  int yigindi = 0;
  for (final son in royxat) {
    yigindi += juda_murakkab_funksiya(son);
  }
  return yigindi;
}

compute() — Flutter'ning Isolate.run() ustidagi qulay qobig'i. U yangi Isolate yaratadi (bu — alohida xotira maydoni bilan, asosiy isolate bilan hech qanday xotirani baham ko'rmaydigan haqiqiy parallel ijro birligi), berilgan funksiyani shu yerda ishga tushiradi, natijani message-passing orqali (ya'ni serializatsiya qilib) asosiy isolate'ga qaytaradi va Isolate'ni yopadi.

Nega Isolate'lar xotirani baham ko'rmaydi? Bu — Dart'ning ataylab qilingan dizayn qarori: umumiy xotira bo'lmagani uchun race condition (poyga holati) va lock kerak bo'lgan murakkab sinxronizatsiya muammolari tug'ilmaydi — bu ko'p thread'li dasturlashning eng qiyin qismlaridan biri. Narxi: Isolate'lar orasida ma'lumot almashish faqat message-passing orqali (va bu ma'lumot serializatsiya qilinishi kerak) — shuning uchun Isolate og'ir, ko'p hisoblash talab qiladigan ishlar uchun to'g'ri, lekin tez-tez, kichik ma'lumot almashish uchun emas (serializatsiya narxi foydadan oshib ketishi mumkin).

5. Tartib va deadlock xavfi bilan bog'liq nozik holatlar#

// Diqqat: bu kod "deadlock"ga o'xshab qoladi, lekin aslida chexvsizlik
Future<void> misol() async {
  final completer = Completer<int>();
 
  // Agar completer hech qachon complete qilinmasa...
  final natija = await completer.future;   // MANGU kutadi — dastur "osilib qoladi"
  print(natija);
}

Completer — Futureni qo'lda yaratish uchun ishlatiladi (masalan, callback-asosidagi eski API'ni Futurega o'rash uchun). Agar completer.complete() yoki completer.completeError() hech qachon chaqirilmasa, uni kutayotgan await abadiy to'xtab qoladi — bu klassik "osilib qolgan Future" xatosi, va uni debug qilish qiyin, chunki hech qanday xato xabari chiqmaydi, dastur shunchaki javob bermay qoladi.

// Xavfsizroq: timeout bilan himoyalash
final natija = await completer.future.timeout(
  Duration(seconds: 10),
  onTimeout: () => -1,   // yoki TimeoutException tashlash
);

6. Ko'p uchraydigan xato javoblar#

SavolXato (junior) javobTo'g'ri (senior) javob
"await nima qiladi?""Threadni to'xtatib turadi"Faqat joriy funksiyani "pauza" qiladi — thread'ning o'zi band emas, event loop boshqa ishlarni bajaraveradi
"Isolate va thread bir xilmi?""Ha, bir xil narsa"Isolate — xotirani baham ko'rmaydigan alohida ijro birligi; klassik thread'lardan farqli, race condition muammosi yo'q
"Future.wait nima uchun ishlatiladi?""Ko'p Future'larni kutish uchun"Bir-biriga bog'liq bo'lmagan operatsiyalarni PARALLEL boshlash orqali umumiy kutish vaqtini qisqartirish uchun
"Nega compute() ishlatiladi?""async funksiya uchun"Faqat CPU'ni band qiladigan, uzoq sinxron hisob-kitoblar uchun — oddiy tarmoq so'rovlariga kerak emas, chunki ular allaqachon asosiy isolate'ni bloklamaydi

Tez-tez so'raladigan intervyu savollari#

Savol: await chaqirilganda aniq nima sodir bo'ladi — thread darajasida tushuntiring.

Javob: await ostidagi Future hali bajarilmagan bo'lsa, Dart joriy async funksiyaning bajarilishini "to'xtatadi" — funksiyaning joriy holati (local o'zgaruvchilar, qaysi qatorda to'xtagani) xotirada saqlanadi, va boshqaruv darhol chaqiruvchiga (yoki event loop'ga) qaytariladi. UI thread bo'sh qoladi va boshqa vazifalarni (masalan, animatsiya freym'larini chizish, gesture'larni qayta ishlash) bajaraveradi. Future bajarilib bo'lgach, uning davomi (continuation) microtask navbatiga qo'yiladi va funksiya xuddi to'xtagan joyidan davom etayotgandek ishlaydi. Muhimi: bu hech qanday yangi thread yaratmaydi — hammasi bitta thread'da, navbat almashtirish orqali sodir bo'ladi.

Savol: Microtask navbati va event navbati orasidagi farqni va ularning ustuvorligini tushuntiring.

Javob: Microtask navbati — yuqori ustuvorlikka ega: Future.then(), async funksiyalarning davomlari shu yerga tushadi. Event navbati esa timer'lar, I/O hodisalar, foydalanuvchi gesture'lari kabi "tashqi" hodisalarni ushlaydi. Dart har safar event navbatidan bitta element olishdan oldin, microtask navbatini to'liq bo'shatadi. Bu shuni anglatadiki, agar kod doimiy ravishda yangi microtask'lar yaratsa (masalan, cheksiz .then() zanjiri), event navbati "och qolishi" (starvation) — hatto UI qayta chizish hodisalari ham kechikishi mumkin. Bu — real loyihalarda kam uchraydigan, lekin topilganda tushuntirish qiyin bo'lgan performance nosozliklarining sababi bo'lishi mumkin.

Savol: Isolate bilan oddiy async/await orasidagi farqni qachon qaysi birini tanlashni tushuntiring.

Javob: async/await — I/O-bog'liq kutish (tarmoq, fayl, timer) uchun ideal, chunki bu vaqtda CPU bo'sh — shunchaki tashqi natijani kutamiz, thread'ni band qilmasdan boshqa ishlarga bo'shatib beramiz. Isolate esa CPU'ni chindan band qiladigan sinxron hisob-kitoblar uchun kerak — bu yerda kutish emas, balki haqiqiy protsessor vaqti sarflanadi, va await bunga yordam bermaydi, chunki hisoblashning o'zini "bo'lib" bo'lmaydi. Amaliy qoida: agar operatsiya "kutmoqda" bo'lsa — Future/await; agar operatsiya "hisoblamoqda" (va bu bir necha millisekunddan ortiq davom etsa, UI freym byudjeti — taxminan 16ms — buzilishi mumkin bo'lsa) — Isolate/compute().

Savol: Single-subscription va broadcast Stream orasidagi farqni qachon qaysi birini tanlashni tushuntiring.

Javob: Single-subscription Stream faqat bitta listen() chaqiruvini qabul qiladi va odatda "chekli" ma'lumot oqimi uchun ishlatiladi (masalan, HTTP javob tanasi, fayl o'qish) — u ma'lumotni listener tayyor bo'lguncha buferlaydi (backpressure). Broadcast Stream esa bir nechta listener'ga ruxsat beradi, lekin obuna bo'lishdan oldingi hodisalarni saqlamaydi. Flutter'da StreamBuilder odatda broadcast Stream bilan ishlaydi, chunki widget qayta qurilganda (parent rebuild) qayta obuna bo'lishi mumkin — agar Stream single-subscription bo'lsa, ikkinchi listen() StateError tashlaydi.

Savol: StreamControllerni yopmaslik (close() chaqirmaslik) nega xotira oqib ketishiga olib keladi?

Javob: StreamController ichkarida barcha faol obunachilar (subscribers) ro'yxatini va ularga bog'liq resurslarni saqlaydi. Agar widget yo'q qilinganda (dispose()da) controller yopilmasa, u va uning ichki holati (shu jumladan unga bog'liq listener'lar orqali ushlab turilgan boshqa obyektlar) garbage collector tomonidan tozalanmaydi — chunki controller hali "faol" hisoblanadi. Ko'p sonli bunday "yopilmagan" controller'lar vaqt o'tishi bilan ilovaning xotira iste'molini asta-sekin oshirib boradi, bu esa uzoq muddat ishlaydigan ilovalarda (masalan, doimo ochiq turadigan dashboard) sezilarli performance muammosiga aylanadi.

Savol: try/catch bilan .catchError() orasida qaysi birini ishlatasiz va nega?

Javob: async/await sintaksisi bilan yozilgan kodda try/catch afzal, chunki u sinxron kodga o'xshab o'qiladi va finally bloki bilan resurslarni tozalashni ham tabiiy qo'llab-quvvatlaydi. .catchError() Future zanjiri (.then()) uslubida yozilgan kodda ishlatiladi, lekin uni noto'g'ri joylashtirish oson — agar .catchError() zanjirning faqat bir qismini qamrab olsa, undan oldingi bosqichlardagi xatolar "tutilmasdan" qoladi. Zamonaviy Dart kodida deyarli har doim async/await + try/catch afzal ko'riladi, aniqroq va debug qilish osonroq bo'lgani uchun.

Savol: Future.wait([a(), b(), c()]) chaqirilganda, agar ulardan biri xato tashlasa, nima bo'ladi?

Javob: Sukut bo'yicha, Future.wait birinchi xato chiqqan Futurening xatosini darhol tashlaydi (boshqa Futurelarning natijasini kutmasdan) — bu esa boshqa Futurelar hali orqa fonda davom etayotganini anglatadi, garchi ularning natijasi endi ishlatilmasa ham. Agar barcha Futurelarning yakunlanishini (xato bo'lsa ham) kutish kerak bo'lsa, eagerError: false parametri (sukut bo'yicha false — demak aslida sukut bo'yicha ham hammasini kutadi, faqat birinchi xatoni saqlab qoladi) yoki har birini alohida try/catch bilan o'rash orqali nazorat qilinadi. Bu — production kodda ko'pincha e'tibordan chetda qoladigan, lekin muhim nozik holat.

Savol: async* va yield nima qiladi, async/returndan qanday farq qiladi?

Javob: Oddiy async funksiya bitta Futureni qaytaradi va return bilan bitta qiymat beradi. async* funksiya esa Stream qaytaradi va yield orqali vaqt davomida bir nechta qiymat chiqarishi mumkin — funksiya tugamaydi, u har bir yielddan keyin davom etadi (agar tsikl ichida bo'lsa). Bu real vaqt hodisalarini (masalan, sensor ma'lumotlari, progress yangilanishlari, WebSocket xabarlari) modellashtirish uchun ishlatiladi — Future bitta yakuniy natija uchun, Stream esa davomiy oqim uchun mos.

Savol: Flutter'da qachon compute() ishlatish shart emas, garchi operatsiya "og'ir" tuyulsa ham?

Javob: Agar operatsiya asosan I/O kutish bo'lsa (tarmoq so'rovi, fayl o'qish/yozish, ma'lumotlar bazasi so'rovi) — bular allaqachon asosiy isolate'ni bloklamaydi, chunki ular Future/await orqali amalga oshiriladi va past darajada operatsion tizim ularni asinxron bajaradi. compute() faqat CPU-bog'liq, sinxron Dart kodi uchun foyda beradi. compute()ni I/O operatsiyalarga qo'llash foyda bermaydi va Isolate yaratish/yopish hamda ma'lumot serializatsiyasining qo'shimcha narxini behuda sarflaydi.

Amaliy topshiriq

Bir nechta manbadan parallel ma'lumot yig'ish

Uchta funksiya yozing: foydalanuvchiniYukla() (1 soniya), buyurtmalarniYukla() (2 soniya), sozlamalarniYukla() (0.5 soniya) — har biri Future.delayed bilan simulyatsiya qilingan. Ularni avval ketma-ket await bilan, so'ngra Future.wait bilan chaqirib, ikkala holatda ham umumiy bajarilish vaqtini Stopwatch bilan o'lchang va solishtiring. Keyin buyurtmalarniYukla()ni xato tashlaydigan qilib o'zgartirib, Future.wait xatti-harakatini kuzating.

Maslahatni ko'rsatish

uchta simulyatsiya qilingan tarmoq chaqiruvini (har biri turli Duration bilan Future.delayed) Future.wait bilan parallel chaqiring va umumiy vaqtni ketma-ket chaqiruv bilan solishtiring; keyin ulardan birini ataylab xato tashlaydigan qiling va natijani kuzating.