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:
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, 4Bu 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 oladiNega 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#
| Savol | Xato (junior) javob | To'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.
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.