Mundarija (35)
- Bu darsda
- 1. Nega bu kerak?
- 2. Timeout: kutishga chegara
- 2.1 Promise.race bilan
- 2.2 AbortSignal.timeout: ishni ham to'xtatish
- 3. Retry: qayta urinish
- 3.1 Eng oddiy shakl
- 3.2 Eksponensial kutish
- 3.3 Jitter: hamma bir vaqtda qaytmasin
- 3.4 To'liq qaytaUrin
- 3.5 Har xatoda qayta urinish mumkin emas
- 4. Parallellik cheklovi
- 4.1 Hovuz (pool)
- 4.2 Qadamma-qadam
- 5. Navbat: birin-ketin
- 6. Race condition: "oxirgi so'rov yutadi"
- 6.1 Muammo
- 6.2 1-yechim: so'rov raqami
- 6.3 2-yechim: oldingi so'rovni bekor qilish
- 6.4 Ikki marta bosish: bitta jarayon
- 6.5 Sardorning beshta muammosi — beshta javob
- 7. Og'ir ishni bo'laklash
- 8. Ko'p uchraydigan xatolar
- 8.1 Promise'lar massivini hovuzga berish
- 8.2 Kutishsiz qayta urinish
- 8.3 Hamma xatoda qayta urinish
- 8.4 Promise.race timeout'i ishni to'xtatadi deb o'ylash
- 8.5 Javob tartibiga ishonish
- 9. Mashqlar
- 1-mashq (oson): Jadvalni to'ldiring
- 2-mashq (o'rta): Zaxira bilan kutish
- 3-mashq (qiyin): Aqlli qaytaUrin
- 10. Real ishda
- Xulosa
- Manbalar
JavaScript asinxron naqshlar: retry, timeout, parallellik cheklovi va race condition
Qisqacha: Tarmoq ishonchsiz: so'rov sekinlashadi, server band bo'ladi, javoblar aralash tartibda keladi. Bunga beshta kundalik naqsh javob beradi. Timeout — "shuncha kutaman, keyin voz kechaman". Retry — vaqtinchalik xatoda qayta urinish, har safar ko'proq kutib (eksponensial kutish) va tasodifiy farq bilan (jitter). Parallellik cheklovi — bir vaqtda N tadan ortiq ish yo'q. Navbat — ishlar birin-ketin. "Oxirgi so'rov yutadi" — eskirgan javob yangisini bosib ketmasin.
Bu darsda
- Promise'ga vaqt chegarasini qo'ya olasiz va
Promise.racehamdaAbortSignal.timeoutfarqini tushuntira olasiz. - Eksponensial kutish va jitter bilan qayta urinadigan funksiya yoza olasiz va qaysi xatoda qayta urinish mumkin emasligini bilasiz.
- Bir vaqtdagi ishlar sonini cheklaydigan "hovuz" (pool) yasay olasiz.
- Ishlarni navbat bilan, birin-ketin bajara olasiz.
- Race condition'ni taniy olasiz va uni so'rov raqami yoki
AbortControllerbilan tuzata olasiz.
Oldin bilishingiz kerak: Ketma-ket va parallel bajarish, Promise kombinatorlari, Bekor qilish: AbortController, Ushlanmagan xatolar.
1. Nega bu kerak?
Tushlik payti, soat 13:00. «Bahor» saytiga bir vaqtda yuzlab odam kiradi. Server og'irlashadi va ba'zi so'rovlarga "band" deb javob beradi. Mehmon menyuni ochmoqchi edi — ekranda xato. U sahifani yangilaydi va... menyu ochiladi. Bir soniya oldin server band edi, endi bo'sh.
Sardor kuzatib, beshta muammoni yozib oldi:
- Ba'zan server umuman javob bermaydi — "Yuklanmoqda..." abadiy aylanadi.
- Band server bir soniyadan keyin javob beradi, lekin sayt birinchi xatodayoq taslim bo'ladi.
- Galereyadagi 50 ta rasm bir vaqtda so'raladi va telefonda hammasi sekinlashadi.
- Bron formasidagi ikkita o'zgarish serverga teskari tartibda yetib boradi.
- Qidiruvga tez "osh" deb yozilsa, ba'zan "o" ning natijalari ko'rinib qoladi.
Har biri uchun tayyor naqsh (pattern) bor — tajribali dasturchilar har kuni ishlatadigan yechim shakli. Bugun beshtasini ham yozamiz. Serverni hozircha o'zimiz "o'ynaymiz" — fetch ni fetch asoslari darsida boshlaymiz. Lekin naqshlar fetch ga bog'liq emas: ular har qanday Promise bilan ishlaydi. Keyinroq API klient qatlami darsida ularni haqiqiy so'rovlarga ulaymiz.
2. Timeout: kutishga chegara
2.1 Promise.race bilan
Sekin server Promise'ni hech qachon bajarmasligi mumkin. await esa abadiy kutadi. Kutishga chegara qo'yishning birinchi yo'lini Promise kombinatorlari darsida yozgan edik — Promise.race bilan muddatBilan: ikki Promise "poyga"ga chiqadi — ish va taymer. Naqsh sifatida unga bitta narsa qo'shamiz — o'z xato klassi:
function kut(ms, qiymat) {
return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}
class VaqtTugadiXatosi extends Error {
constructor(ms) {
super(`${ms} ms ichida javob kelmadi`);
this.name = "VaqtTugadiXatosi";
}
}
function muddatBilan(promise, ms) {
let taymer;
const chegara = new Promise((_, reject) => {
taymer = setTimeout(() => reject(new VaqtTugadiXatosi(ms)), ms);
});
return Promise.race([promise, chegara]).finally(() => {
clearTimeout(taymer);
});
}
async function sekinServer() {
await kut(500);
console.log("server: javob tayyor (hech kim kutmayapti)");
return ["Osh", "Manti"];
}
async function menyuniOch() {
try {
const menyu = await muddatBilan(sekinServer(), 200);
console.log(menyu);
} catch (xato) {
console.log(`${xato.name}: ${xato.message}`);
}
}
menyuniOch();Konsolda:
VaqtTugadiXatosi: 200 ms ichida javob kelmadi
server: javob tayyor (hech kim kutmayapti)Kodni o'qiymiz:
VaqtTugadiXatosi— o'z xato klassimiz (O'z xato klasslaringiz). Chaqiruvchi uniinstanceofyokinamebilan boshqa xatolardan ajrata oladi.chegara— faqat rad etiladigan Promise. Birinchi parametr_— "resolvekerak emas".finallydaclearTimeout— Promise kombinatorlari dagi tuzoq: ish oldin tugasa ham taymer tozalanadi.
Endi ikkinchi qatorga qarang. 200 ms da biz voz kechdik, lekin server ishlashda davom etdi va 500 ms da javob tayyorladi. Promise.race ishni to'xtatmaydi — u faqat natijani kutishni to'xtatadi. Haqiqiy tarmoqda bu so'rov ochiq qoladi, trafik va server resursini yeydi.
2.2 AbortSignal.timeout: ishni ham to'xtatish
Bekor qilish: AbortController darsidagi AbortSignal.timeout(ms) — ms dan keyin o'zi bekor bo'ladigan signal. Agar ish signalni tinglasa, vaqt tugaganda u o'zi to'xtaydi:
function menyuniOl({ signal }) {
return new Promise((resolve, reject) => {
const taymer = setTimeout(() => {
console.log("server: javob tayyor");
resolve(["Osh", "Manti"]);
}, 500);
signal.addEventListener("abort", () => {
clearTimeout(taymer);
reject(signal.reason);
}, { once: true });
});
}
async function menyuniOch() {
try {
const signal = AbortSignal.timeout(200);
console.log(await menyuniOl({ signal }));
} catch (xato) {
console.log(`${xato.name}: ${xato.message}`);
}
}
menyuniOch();Konsolda:
TimeoutError: The operation was aborted due to timeout"server: javob tayyor" chiqmadi — ish haqiqatan to'xtadi. Xato nomi — TimeoutError ("vaqt tugadi xatosi"), xabari: "Amal vaqt tugagani sababli to'xtatildi". Qo'lda abort() qilinganda esa nom AbortError bo'lardi. Shu farq bilan "foydalanuvchi bekor qildi" va "server sekin" holatlarini ajratasiz.
Qoida: ish signal qabul qilsa (fetch, Node'ning ko'p API'lari) — AbortSignal.timeout. Qabul qilmasa (eski kutubxona, callback API) — Promise.race o'rami. Ikkala chegara kerak bo'lsa — foydalanuvchi bekor qilishi va vaqt — ularni AbortSignal.any([boshqaruv.signal, AbortSignal.timeout(5000)]) bilan birlashtirasiz.
Tekshirib ko'ring:
muddatBilandanfinally(() => clearTimeout(taymer))ni olib tashlasak va ish 10 ms da tugasa, natija o'zgaradimi?
Javob
Natija — yo'q, ish baribir yutadi. Lekin taymer qoladi va 200 ms da chegara Promise'ini rad etadi. U poygada yutqazgan, shuning uchun xato hech qayerga chiqmaydi — Promise.race unga allaqachon "kuzatuvchi" ulagan. Muammo boshqa: Node dasturi taymer tugashini kutadi, ko'p chaqiruvda esa keraksiz taymerlar to'planadi.
3. Retry: qayta urinish
3.1 Eng oddiy shakl
Server vaqtinchalik band. Uni o'zimiz yasaymiz: birinchi xatolarSoni ta chaqiruvda xato beradi, keyin ishlaydi:
function serverYarat(xatolarSoni) {
let chaqiruvlar = 0;
return async function menyuniOl() {
chaqiruvlar++;
await kut(50);
if (chaqiruvlar <= xatolarSoni) {
throw new Error("503 — server band");
}
return ["Osh", "Lag'mon", "Manti"];
};
}503 — HTTP holat kodi: "xizmat vaqtincha mavjud emas" (holat kodlari — HTTP metodlari va status kodlari darsida). Closure chaqiruvlar ni eslab qoladi — har server o'z hisobiga ega.
Qayta urinish — sikl ichida try/catch:
function kut(ms, qiymat) {
return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}
function serverYarat(xatolarSoni) {
let chaqiruvlar = 0;
return async function menyuniOl() {
chaqiruvlar++;
await kut(50);
if (chaqiruvlar <= xatolarSoni) {
throw new Error("503 — server band");
}
return ["Osh", "Lag'mon", "Manti"];
};
}
async function qaytaUrin(ish, urinishlar) {
for (let urinish = 1; ; urinish++) {
try {
return await ish();
} catch (xato) {
console.log(`${urinish}-urinish: ${xato.message}`);
if (urinish >= urinishlar) {
throw xato;
}
}
}
}
async function menyuniOch() {
const menyuniOl = serverYarat(2);
const menyu = await qaytaUrin(menyuniOl, 3);
console.log("Menyu:", menyu.join(", "));
}
menyuniOch();Konsolda:
1-urinish: 503 — server band
2-urinish: 503 — server band
Menyu: Osh, Lag'mon, Mantifor (let urinish = 1; ; urinish++)— shartsiz sikl: undan faqatreturn(muvaffaqiyat) yokithrow(urinishlar tugadi) bilan chiqiladi.return await ish()—awaitshart. U bo'lmasa, rad etilishtrydan qochib ketardi (Ushlanmagan xatolar darsidagireturn awaittuzog'i).- Oxirgi urinishda ham xato bo'lsa — asl xato qayta tashlanadi va chaqiruvchi uni ko'radi.
Bu kodda jiddiy kamchilik bor: urinishlar darhol ketma-ket ketadi. Server band — uni 50 ms ichida yana ikki marta bezovta qilish yordam bermaydi, aksincha, uni yanada og'irlashtiradi.
3.2 Eksponensial kutish
Har urinishdan oldin kutish kerak — va har safar ko'proq. Birinchi marta 100 ms, keyin 200, keyin 400 — har gal ikki baravar. Bunga eksponensial kutish (exponential backoff) deyiladi. Formula:
kechikish = Math.min(maks, asosiy * 2 ** (urinish - 1));2 ** n — 2 ning n-darajasi (Arifmetik operatorlar). Math.min(maks, ...) — shift (cap): kechikish cheksiz o'smasin. Masalan asosiy = 100, maks = 2000 bo'lsa:
for (let urinish = 1; urinish <= 7; urinish++) {
const kechikish = Math.min(2000, 100 * 2 ** (urinish - 1));
console.log(`${urinish}-urinishdan keyin: ${kechikish} ms`);
}Konsolda:
1-urinishdan keyin: 100 ms
2-urinishdan keyin: 200 ms
3-urinishdan keyin: 400 ms
4-urinishdan keyin: 800 ms
5-urinishdan keyin: 1600 ms
6-urinishdan keyin: 2000 ms
7-urinishdan keyin: 2000 msGrafikda doimiy va eksponensial kutishni taqqoslang:
- doimiy (100 ms)
- eksponensial (asosiy 100, maks 2000)
| Urinish | doimiy (100 ms) | eksponensial (asosiy 100, maks 2000) |
|---|---|---|
| 1 | 100 | |
| 2 | 100 | |
| 3 | 100 | |
| 4 | 100 | |
| 5 | 100 | |
| 6 | 100 | |
| 7 | 100 | |
| 1 | 100 | |
| 2 | 200 | |
| 3 | 400 | |
| 4 | 800 | |
| 5 | 1 600 | |
| 6 | 2 000 | |
| 7 | 2 000 |
Manba: darsdagi formula bo'yicha hisoblangan: Math.min(2000, 100 · 2^(urinish − 1))
Mantiq oddiy: server bir lahzaga band bo'lsa — tez qaytamiz. Uzoq band bo'lsa — uni tinch qo'yamiz va kamroq bezovta qilamiz. Shift esa foydalanuvchi minutlab kutib qolmasligi uchun.
3.3 Jitter: hamma bir vaqtda qaytmasin
Eksponensial kutishning yashirin muammosi bor. Server soat 13:00:00 da bir soniyaga "yiqildi". Shu paytda 1 000 ta mehmon xato oldi. Hammasining kodi bir xil: 100 ms kut, keyin qayta urin. Natijada 13:00:00.100 da serverga bir vaqtda 1 000 ta so'rov keladi — u yana yiqiladi. Keyin hammasi 200 ms kutadi va yana birga keladi. Bu hodisani "bo'kirgan poda" (thundering herd) deyishadi.
Yechim — jitter (tasodifiy tebranish): har mijoz kechikishni tasodifiy songa ko'paytiradi. Eng ko'p ishlatiladigan usul — "to'liq jitter" (full jitter): 0 dan kechikishgacha tasodifiy vaqt.
const kutish = Math.round(Math.random() * kechikish);Endi 1 000 mijoz 0–100 ms oralig'iga "sochiladi" va server ularni navbat bilan qabul qiladi.
Lekin Math.random() har ishga tushirishda boshqa son beradi — darsdagi chiqishni ham, testni ham tekshirib bo'lmaydi. Shuning uchun tasodif manbasini parametr qilamiz: odatda Math.random, sinovda esa o'zimiz bergan funksiya. Bu usulni "bog'liqlikni tashqaridan berish" (dependency injection) deyishadi — test darslarida (node:test) ko'p uchraydi. Uchta mijozni o'ylab ko'ring, har birining "tasodifi" boshqacha:
function kutishniHisobla(urinish, { asosiy, maks, tasodif }) {
const kechikish = Math.min(maks, asosiy * 2 ** (urinish - 1));
return Math.round(tasodif() * kechikish);
}
const sozlama = { asosiy: 100, maks: 2000 };
const mijozlar = [0.9, 0.2, 0.6];
for (const son of mijozlar) {
const tasodif = () => son;
const jadval = [1, 2, 3].map((u) =>
kutishniHisobla(u, { ...sozlama, tasodif }),
);
console.log(jadval.join(" → "));
}Konsolda:
90 → 180 → 360
20 → 40 → 80
60 → 120 → 240Jitter'siz uchalasi ham 100 → 200 → 400 bo'lardi — aynan bir xil lahzalarda. Endi ular sochilgan. Haqiqiy kodda tasodif berilmaydi va Math.random ishlaydi.
3.4 To'liq qaytaUrin
Uch g'oyani birlashtiramiz. Sozlamalar — parametrdagi destructuring va standart qiymatlar bilan:
function kut(ms, qiymat) {
return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}
function serverYarat(xatolarSoni) {
let chaqiruvlar = 0;
return async function menyuniOl() {
chaqiruvlar++;
await kut(50);
if (chaqiruvlar <= xatolarSoni) {
throw new Error("503 — server band");
}
return ["Osh", "Lag'mon", "Manti"];
};
}
async function qaytaUrin(ish, {
urinishlar = 3,
asosiy = 100,
maks = 2000,
tasodif = Math.random,
} = {}) {
for (let urinish = 1; ; urinish++) {
try {
return await ish();
} catch (xato) {
if (urinish >= urinishlar) {
throw xato;
}
const kechikish = Math.min(maks, asosiy * 2 ** (urinish - 1));
const kutish = Math.round(tasodif() * kechikish);
const izoh = `${xato.message}, ${kutish} ms`;
console.log(`${urinish}-urinish: ${izoh}`);
await kut(kutish);
}
}
}
async function menyuniOch() {
const menyuniOl = serverYarat(2);
const menyu = await qaytaUrin(menyuniOl, {
urinishlar: 4,
tasodif: () => 0.5,
});
console.log("Menyu:", menyu.join(", "));
}
menyuniOch();Konsolda:
1-urinish: 503 — server band, 50 ms
2-urinish: 503 — server band, 100 ms
Menyu: Osh, Lag'mon, Mantitasodif: () => 0.5 — faqat darsdagi chiqish har safar bir xil bo'lishi uchun. Ilovangizda uni bermaysiz.
3.5 Har xatoda qayta urinish mumkin emas
Qayta urinish faqat vaqtinchalik xatolarda ma'noli:
| Qayta urinish mumkin | Mumkin emas |
|---|---|
| tarmoq uzildi, timeout | 400 — so'rov noto'g'ri |
| 503 — server band | 401/403 — ruxsat yo'q |
| 429 — juda ko'p so'rov | 404 — topilmadi |
| 502, 504 — oraliq server xatosi | 422 — ma'lumot xato |
O'ng ustundagi xatolar qayta urinishdan tuzalmaydi: "Bunday taom yo'q" (404) degan server 400 ms dan keyin ham shuni aytadi. Ularga qayta urinish — foydalanuvchini bekorga kutdirish. Shuning uchun haqiqiy qaytaUrin "qayta urinsa bo'ladimi?" degan savolni beradigan funksiyani ham oladi — buni 3-mashqda qo'shasiz.
Ikkinchi xavf — takrorlanganda ziyon qiladigan so'rovlar. "Menyuni ber" ni ikki marta so'rash zararsiz. "Bron qil" ni ikki marta yuborish esa — ikki bron. Birinchi so'rov aslida serverga yetib borgan, faqat javob yo'lda yo'qolgan bo'lishi mumkin. Bir necha marta bajarilganda ham natijasi bir xil bo'ladigan amal idempotent deyiladi. Idempotent bo'lmagan so'rovni ko'r-ko'rona qayta yubormang. Server tomonda buning uchun maxsus "takrorlanmas kalit" (idempotency key) usuli bor — backend qismida o'rganamiz.
Maslahat: 429 ("juda ko'p so'rov") javobida server ko'pincha
Retry-Aftersarlavhasini yuboradi: "N soniyadan keyin keling". Bunday holda o'z formulangiz emas, server aytgan vaqt ustun. Javob sarlavhalarini o'qishniRequest,Response,Headersdarsida o'rganamiz.
Tekshirib ko'ring:
asosiy = 200,maks = 1000. To'rtinchi urinishdan keyingi kechikish (jitter'siz) necha ms? Javob:
Javob
1000. Formula: 200 * 2 ** 3 = 1600, lekin Math.min(1000, 1600) — shift ishladi. Jadval: 200 → 400 → 800 → 1000.
4. Parallellik cheklovi
4.1 Hovuz (pool)
Galereyada 50 ta rasm. Promise.all(rasmlar.map(yukla)) hammasini bir vaqtda boshlaydi (Ketma-ket va parallel bajarish). Server uchun bu — 50 ta bir vaqtdagi so'rov. Sekin internetli telefonda esa 50 ta yuklash bitta tarmoqni bo'lishadi va birinchi rasm ham kech ko'rinadi.
Ketma-ket yuklash ham yomon — juda sekin. O'rtacha yo'l: bir vaqtda ko'pi bilan N ta. Buni "parallellik cheklovi" (concurrency limit) deyishadi. Usuli — hovuz (pool): N ta "ishchi" bitta umumiy navbatdan ish oladi. Ishchi o'z ishini tugatgach, navbatdagisini oladi — xuddi oshxonadagi ikkita oshpaz buyurtmalar ilgichidan navbatdagi varaqni olgandek.
function kut(ms, qiymat) {
return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}
async function cheklanganParallel(ishlar, chegara) {
const natijalar = new Array(ishlar.length);
let keyingi = 0;
async function ishchi() {
while (keyingi < ishlar.length) {
const i = keyingi++;
natijalar[i] = await ishlar[i]();
}
}
const ishchilar = [];
for (let n = 0; n < Math.min(chegara, ishlar.length); n++) {
ishchilar.push(ishchi());
}
await Promise.all(ishchilar);
return natijalar;
}
async function galereyaniYukla() {
const vaqtlar = [300, 100, 150, 100];
const ishlar = vaqtlar.map((ms, i) => async () => {
console.log(`${i + 1}-rasm boshlandi`);
await kut(ms);
console.log(`${i + 1}-rasm tugadi`);
return `rasm${i + 1}.webp`;
});
console.log(await cheklanganParallel(ishlar, 2));
}
galereyaniYukla();Konsolda:
1-rasm boshlandi
2-rasm boshlandi
2-rasm tugadi
3-rasm boshlandi
3-rasm tugadi
4-rasm boshlandi
1-rasm tugadi
4-rasm tugadi
[ 'rasm1.webp', 'rasm2.webp', 'rasm3.webp', 'rasm4.webp' ]Asosiy joylar:
ishlar— Promise'lar emas, Promise qaytaradigan funksiyalar. Bu muhim: Promise yaratilishi bilan ish boshlanadi. Funksiya esa ishni "keyinga" qoldiradi — ishchi chaqirgandagina boshlanadi.const i = keyingi++— ishchi navbatdan raqam oladi va hisoblagichni oshiradi. Bu ikki ish bitta sinxron qatorda — oraliqda boshqa ishchi aralasha olmaydi. JavaScript bir oqimli, shuning uchun bu xavfsiz (Sinxron va asinxron kod).natijalar[i]— natija o'z joyiga yoziladi. Rasmlar har xil tartibda tugadi, lekin massiv asl tartibda —Promise.alldagidek.
4.2 Qadamma-qadam
Hovuzning ichki ishini soddalashtirilgan kodda kuzating. O'ngda — hozir ishlayotgan va navbatda turgan rasmlar:
E'tibor bering: bo'shagan ishchi darhol keyingisini oladi. Bu "partiyalab" (avval 1–2, keyin 3–4) bajarishdan tezroq: partiyada 2-rasm tugagach, 1-rasmni bekorga 200 ms kutish kerak bo'lardi.
Tekshirib ko'ring:
cheklanganParallel(ishlar, 10)— 4 ta ish uchun. Nechta ishchi yaratiladi va natijaPromise.alldan farq qiladimi?
Javob
To'rtta: Math.min(10, 4). Har ishchi bittadan ish oladi va hammasi birdan boshlanadi — xulqi Promise.all bilan bir xil. Cheklov faqat ishlar soni chegaradan ko'p bo'lganda ta'sir qiladi.
5. Navbat: birin-ketin
Dilshod aka telefonda bronini tahrirlayapti: avval odam sonini 4 ga, darhol keyin 6 ga o'zgartirdi. Ikkita "saqla" so'rovi ketdi. Birinchisi sekin, ikkinchisi tez yetib bordi — serverda 4 qoldi. Oxirgi o'zgarish yo'qoldi.
Bunday holatda ishlar navbat bilan bajarilishi kerak: keyingisi oldingisi tugagandan keyin boshlansin. Promise zanjiri aynan shunday ishlaydi — then oldingi Promise'ni kutadi:
function kut(ms, qiymat) {
return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}
function navbatYarat() {
let oxirgi = Promise.resolve();
return function navbatgaQoy(ish) {
const natija = oxirgi.then(ish);
oxirgi = natija.catch(() => {});
return natija;
};
}
let serverda = null;
async function odamSoniniSaqla(soni, ms) {
await kut(ms);
serverda = soni;
console.log(`saqlandi: ${soni}`);
}
const navbatgaQoy = navbatYarat();
navbatgaQoy(() => odamSoniniSaqla(4, 300));
navbatgaQoy(() => odamSoniniSaqla(6, 100));
setTimeout(() => console.log("Serverda:", serverda), 600);Konsolda:
saqlandi: 4
saqlandi: 6
Serverda: 6oxirgi— navbatdagi oxirgi ishning Promise'i. Har yangi ish ungathenbilan "ulanadi".oxirgi = natija.catch(() => {})— bitta ish xato bersa, navbat to'xtab qolmasin. Xatoni esa chaqiruvchinatijaorqali o'zi oladi.- Navbatsiz (ikkalasini to'g'ridan-to'g'ri chaqirsak) konsolda
saqlandi: 6, keyinsaqlandi: 4vaServerda: 4bo'lardi.
Navbat — chegarasi 1 bo'lgan hovuzning o'zi. Farqi: hovuzga ishlar ro'yxati bir marta beriladi, navbatga esa vaqt o'tib, birma-bir qo'shiladi.
6. Race condition: "oxirgi so'rov yutadi"
6.1 Muammo
Qidiruv maydoniga "osh" deb yozilyapti. Har harfda so'rov ketadi: "o", "os", "osh". Qisqa so'z ko'proq natija beradi va server uni sekinroq qaytaradi. Javoblar teskari tartibda keladi:
function kut(ms, qiymat) {
return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}
const javobVaqti = { o: 300, os: 200, osh: 100 };
function qidir(soz) {
return kut(javobVaqti[soz], `«${soz}» natijalari`);
}
let ekranda = "";
async function qidiruvniYangila(soz) {
const natija = await qidir(soz);
console.log("keldi:", natija);
ekranda = natija;
}
qidiruvniYangila("o");
setTimeout(() => qidiruvniYangila("os"), 50);
setTimeout(() => qidiruvniYangila("osh"), 100);
setTimeout(() => console.log("Ekranda:", ekranda), 500);Konsolda:
keldi: «osh» natijalari
keldi: «os» natijalari
keldi: «o» natijalari
Ekranda: «o» natijalariFoydalanuvchi "osh" deb yozdi, ekranda esa "o" ning natijalari. Eng eski javob eng oxirida kelib, to'g'ri natijani bosib ketdi.
Natija ishlar qaysi tartibda tugashiga bog'liq bo'lgan holat race condition (poyga holati) deyiladi. Uni topish qiyin: kompyuteringizda internet tez — hammasi to'g'ri; mehmonning telefonida — xato. Bir oqimli JavaScript'da ham race condition bo'ladi: kod parallel ishlamaydi, lekin asinxron ishlar har xil tartibda tugaydi.
6.2 1-yechim: so'rov raqami
Har so'rovga raqam beramiz va javob kelganda tekshiramiz: "men hali ham oxirgisimanmi?"
function kut(ms, qiymat) {
return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}
const javobVaqti = { o: 300, os: 200, osh: 100 };
function qidir(soz) {
return kut(javobVaqti[soz], `«${soz}» natijalari`);
}
let ekranda = "";
let oxirgiRaqam = 0;
async function qidiruvniYangila(soz) {
const raqam = ++oxirgiRaqam;
const natija = await qidir(soz);
if (raqam !== oxirgiRaqam) {
console.log("eskirgan, tashlandi:", natija);
return;
}
ekranda = natija;
}
qidiruvniYangila("o");
setTimeout(() => qidiruvniYangila("os"), 50);
setTimeout(() => qidiruvniYangila("osh"), 100);
setTimeout(() => console.log("Ekranda:", ekranda), 500);Konsolda:
eskirgan, tashlandi: «os» natijalari
eskirgan, tashlandi: «o» natijalari
Ekranda: «osh» natijalarioxirgiRaqam — closure'dagi umumiy hisoblagich, raqam — har chaqiruvning o'z nusxasi. Javob kelganda ular teng bo'lmasa, demak orada yangi so'rov ketgan — bu javob eskirgan.
6.3 2-yechim: oldingi so'rovni bekor qilish
1-yechimda eski so'rovlar baribir oxirigacha ishladi — biz faqat natijasini tashladik. Yaxshiroq: yangi so'rov ketishi bilan eskisini bekor qilish. Brauzerda sinang: maydonga o'zingiz ham yozib ko'ring. Sahifa ochilganda skript "tez yozish"ni o'zi taqlid qiladi — o, os, osh:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
input { font: inherit; padding: 0.3rem 0.5rem; }
</style>
<label for="qidiruv">Menyudan qidirish</label>
<input id="qidiruv" autocomplete="off">
<p id="natija" aria-live="polite">—</p>
<script>
const MENYU = ["Osh", "Lag'mon", "Manti", "Ko'k choy"];
const qidiruv = document.getElementById("qidiruv");
const natijaEl = document.getElementById("natija");
let joriy = null;
// Server o'rinbosari: qisqa so'z — sekinroq javob
function qidir(soz, signal) {
return new Promise((resolve, reject) => {
const taymer = setTimeout(() => {
resolve(MENYU.filter((t) => t.toLowerCase().includes(soz)));
}, Math.max(100, 400 - soz.length * 100));
signal.addEventListener("abort", () => {
clearTimeout(taymer);
reject(signal.reason);
}, { once: true });
});
}
qidiruv.addEventListener("input", async () => {
const soz = qidiruv.value.trim().toLowerCase();
joriy?.abort();
const boshqaruv = new AbortController();
joriy = boshqaruv;
try {
const topildi = await qidir(soz, boshqaruv.signal);
natijaEl.textContent = topildi.join(", ") || "Topilmadi";
console.log(`«${soz}»: ${natijaEl.textContent}`);
} catch (xato) {
if (xato.name !== "AbortError") {
throw xato;
}
console.log(`«${soz}» bekor qilindi`);
}
});
// Tez yozishni taqlid qilamiz: o → os → osh
[[0, "o"], [50, "os"], [100, "osh"]].forEach(([ms, matn]) => {
setTimeout(() => {
qidiruv.value = matn;
qidiruv.dispatchEvent(new Event("input"));
}, ms);
});
</script>Konsolda:
«o» bekor qilindi
«os» bekor qilindi
«osh»: OshHar input hodisasida: avvalgi so'rovni abort() qilamiz (?. — birinchi marta joriy hali null), yangisi uchun yangi AbortController yaratamiz. Bekor qilingan so'rovning await i AbortError bilan rad etiladi — biz uni "kutilgan" deb jim o'tkazamiz. Boshqa har qanday xatoni esa qayta tashlaymiz: u global tinglovchiga borsin (Ushlanmagan xatolar).
Haqiqiy fetch da bekor qilish so'rovni tarmoq darajasida uzadi — trafik ham, server ishi ham tejaladi. Qidiruvda odatda yana bir naqsh qo'shiladi — har harfda emas, yozish to'xtagach so'rash (Debounce va throttle).
6.4 Ikki marta bosish: bitta jarayon
Race condition'ning teskari ko'rinishi: Otabek kassadagi «Bron qilish» tugmasini shoshib ikki marta bosdi. Ikki so'rov — ikki bron. Bu yerda "oxirgisi" emas, birinchisi yutishi kerak: jarayon ketayotgan bo'lsa, yangisini boshlamaslik.
function kut(ms, qiymat) {
return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}
function birMartalik(ish) {
let jarayon = null;
return function () {
if (jarayon === null) {
jarayon = ish().finally(() => {
jarayon = null;
});
}
return jarayon;
};
}
let bronlarSoni = 0;
async function bronYubor() {
await kut(200);
bronlarSoni++;
return `Bron №${bronlarSoni}`;
}
const bronQil = birMartalik(bronYubor);
const birinchi = bronQil();
const ikkinchi = bronQil();
console.log(birinchi === ikkinchi);
birinchi.then((javob) => console.log(javob, "| jami:", bronlarSoni));Konsolda:
true
Bron №1 | jami: 1Ikkinchi bosish yangi so'rov yubormadi — u o'sha Promise'ni oldi. Jarayon tugagach (finally) jarayon yana null bo'ladi va keyingi bosish yangi bronni boshlaydi. Interfeysda bunga qo'shimcha ravishda tugma ham disabled qilinadi — lekin kod darajasidagi himoya ishonchliroq: tugmadan boshqa yo'llar (Enter tugmasi, ikkinchi tab) ham bor.
6.5 Sardorning beshta muammosi — beshta javob
«Nega bu kerak?» dagi ro'yxatga qaytamiz. Endi har muammoning o'z naqshi bor.
| Muammo | Naqsh |
|---|---|
| "Yuklanmoqda..." abadiy aylanadi | timeout (AbortSignal.timeout) |
| band server — birinchi xatoda taslim | retry + eksponensial kutish + jitter |
| 50 ta rasm bir vaqtda | hovuz (parallellik cheklovi) |
| ikki o'zgarish teskari tartibda | navbat |
| "o" natijasi "osh" ni bosib ketadi | so'rov raqami yoki abort() |
Ikki marta bosish — race condition'ning oltinchi, "birinchisi yutadi" ko'rinishi.
7. Og'ir ishni bo'laklash
Sinxron va asinxron kod darsida va'da bergan edik: og'ir ishni bo'laklash usullarini ko'ramiz. Avval Event loop darsining 3-mashqida bo'laklarni callback va setTimeout bilan yozgan edingiz. Keyin Microtask va macrotask chuqur darsida await kut(0) nega davomni task qilishini ko'rdik. Endi ikkalasini naqsh qilib birlashtiramiz: har bo'lakdan keyin await kut(0) — "event loop'ga nafas ber":
function kut(ms, qiymat) {
return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}
async function bolaklabHisobla(jami, bolak) {
let summa = 0;
for (let i = 0; i < jami; i++) {
summa += i % 100;
if ((i + 1) % bolak === 0) {
console.log(`${(i + 1) / bolak}-bo'lak tugadi`);
await kut(0);
}
}
return summa;
}
setTimeout(() => console.log("Yangi buyurtma qabul qilindi"), 0);
bolaklabHisobla(3_000_000, 1_000_000).then((summa) => {
console.log("Jami summa:", summa);
});Konsolda:
1-bo'lak tugadi
Yangi buyurtma qabul qilindi
2-bo'lak tugadi
3-bo'lak tugadi
Jami summa: 148500000Natija Event loop darsidagi mashq bilan bir xil, lekin kod oddiy for sikliga o'xshaydi: i va summa closure orqali emas, oddiy o'zgaruvchi sifatida yashaydi. Har bo'lakdan keyin await kut(0) siklni to'xtatadi, task navbatidagi "Yangi buyurtma" bajariladi, keyin sikl davom etadi.
Diqqat:
await nullyokiawait Promise.resolve()bilan bo'laklash ishlamaydi — ular faqat microtask beradi (Microtask va macrotask chuqur dagi microtask ochligi). Event loop'ga nafas berish uchun — task kerak. Brauzerlarda buning uchun maxsusscheduler.yield()ham bor — Asosiy oqimni bo'shatish darsida.
8. Ko'p uchraydigan xatolar
8.1 Promise'lar massivini hovuzga berish
cheklanganParallel(rasmlar.map((r) => yukla(r)), 2) — map hammasini darhol chaqirdi, 50 ta yuklash allaqachon boshlangan. Hovuz endi hech narsani cheklamaydi. Tuzatish: funksiyalar bering: rasmlar.map((r) => () => yukla(r)).
8.2 Kutishsiz qayta urinish
for siklida darhol qayta urinish — band serverni yanada bosadi. Tuzatish: eksponensial kutish + jitter + shift.
8.3 Hamma xatoda qayta urinish
404 ga uch marta qayta urinish — foydalanuvchini bekorga uch marta kutdirish. "Bron qil" ni qayta yuborish — ikki bron. Tuzatish: faqat vaqtinchalik xatolar va idempotent amallar.
8.4 Promise.race timeout'i ishni to'xtatadi deb o'ylash
U faqat kutishni to'xtatadi — ish davom etadi. Tuzatish: imkon bo'lsa AbortSignal.timeout va signal qabul qiladigan API.
8.5 Javob tartibiga ishonish
"Men avval «o» ni, keyin «osh» ni so'radim — demak javoblar ham shu tartibda keladi". Yo'q: tarmoqda tartib kafolatlanmaydi. Tuzatish: so'rov raqami yoki oldingisini bekor qilish; ketma-ket saqlash uchun — navbat.
9. Mashqlar
1-mashq (oson): Jadvalni to'ldiring
asosiy = 50, maks = 300, jitter yo'q. Har urinishdan keyingi kechikishni yozing:
- 1-urinishdan keyin: ms
- 2-urinishdan keyin: ms
- 3-urinishdan keyin: ms
- 4-urinishdan keyin: ms
Yechim
50 → 100 → 200 → 300. To'rtinchida formula 50 * 2 ** 3 = 400 beradi, lekin Math.min(300, 400) — shift. Bundan keyingi har urinish ham 300.
2-mashq (o'rta): Zaxira bilan kutish
Menyu 300 ms ichida kelmasa, ekranda bo'sh joy emas, kechagi saqlangan menyu ko'rinsin. zaxiraBilan(promise, ms, zaxira) funksiyasini yozing: promise ms ichida bajarilsa — uning qiymatini, aks holda zaxira ni qaytarsin. Taymerni tozalashni unutmang. Ishora: muddatBilan dagi Promise.race ni oling, faqat chegara Promise'i rad etilmasin, zaxira bilan bajarilsin.
Sinang: tez server (100 ms) va sekin server (500 ms), zaxira — ["Osh (kechagi menyu)"].
Yechim
function kut(ms, qiymat) {
return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}
function zaxiraBilan(promise, ms, zaxira) {
let taymer;
const chegara = new Promise((resolve) => {
taymer = setTimeout(resolve, ms, zaxira);
});
return Promise.race([promise, chegara]).finally(() => {
clearTimeout(taymer);
});
}
async function menyularniOch() {
const kechagi = ["Osh (kechagi menyu)"];
const tez = kut(100, ["Osh", "Manti"]);
const sekin = kut(500, ["Osh", "Manti"]);
console.log(await zaxiraBilan(tez, 300, kechagi));
console.log(await zaxiraBilan(sekin, 300, kechagi));
}
menyularniOch();Konsolda:
[ 'Osh', 'Manti' ]
[ 'Osh (kechagi menyu)' ]setTimeout(resolve, ms, zaxira) — Taymerlar darsidagi uchinchi argument: resolve(zaxira) ni ms dan keyin chaqiradi. Bu naqsh "eskirgan bo'lsa ham ko'rsat" deb ataladi — ilovalarda keshdagi ma'lumot shunday ko'rsatiladi, yangisi kelgach almashtiriladi. Bunday keshni yakuniy loyihada o'zingiz yasaysiz.
3-mashq (qiyin): Aqlli qaytaUrin
«To'liq qaytaUrin» bo'limidagi funksiyaga ikki imkoniyat qo'shing:
shart(xato)sozlamasi —trueqaytarsa qayta urinadi,falsebo'lsa xatoni darhol tashlaydi. Standart qiymati — har doimtrue.signalsozlamasi — bekor qilinsa, kutish paytida ham darhol to'xtasin.
Sinov uchun server xatoga holat xususiyatini qo'shsin: Object.assign(new Error("..."), { holat: 503 }). shart — (xato) => [429, 502, 503, 504].includes(xato.holat). Ikki holatni sinang: 503 ni ikki marta beradigan server (muvaffaqiyat) va 404 beradigan server (bitta urinish, xato). Ishora: signal bilan kutish uchun Bekor qilish darsidagi signal tinglaydigan kutish funksiyasini yozing. Faylni kurs/mashqlar/11/15-asinxron-naqshlar/qayta-urin.mjs ga saqlang.
Yechim
function signalliKut(ms, signal) {
return new Promise((resolve, reject) => {
signal?.throwIfAborted();
const taymer = setTimeout(resolve, ms);
signal?.addEventListener("abort", () => {
clearTimeout(taymer);
reject(signal.reason);
}, { once: true });
});
}
async function qaytaUrin(ish, {
urinishlar = 3,
asosiy = 100,
maks = 2000,
tasodif = Math.random,
shart = () => true,
signal,
} = {}) {
for (let urinish = 1; ; urinish++) {
signal?.throwIfAborted();
try {
return await ish();
} catch (xato) {
if (urinish >= urinishlar || !shart(xato)) {
throw xato;
}
const kechikish = Math.min(maks, asosiy * 2 ** (urinish - 1));
const kutish = Math.round(tasodif() * kechikish);
const izoh = `${xato.message}, ${kutish} ms`;
console.log(`${urinish}-urinish: ${izoh}`);
await signalliKut(kutish, signal);
}
}
}
function serverYarat(holatlar) {
let chaqiruv = 0;
return async function menyuniOl() {
const holat = holatlar[chaqiruv++] ?? 200;
if (holat !== 200) {
throw Object.assign(new Error(`${holat} xato`), { holat });
}
return ["Osh", "Manti"];
};
}
const VAQTINCHALIK = [429, 502, 503, 504];
const vaqtinchalikmi = (xato) => VAQTINCHALIK.includes(xato.holat);
const sozlama = {
urinishlar: 4,
tasodif: () => 0.5,
shart: vaqtinchalikmi,
};
async function sina() {
const band = serverYarat([503, 503]);
console.log("Menyu:", await qaytaUrin(band, sozlama));
try {
await qaytaUrin(serverYarat([404]), sozlama);
} catch (xato) {
console.log("Taslim bo'ldik:", xato.message);
}
}
sina();Konsolda:
1-urinish: 503 xato, 50 ms
2-urinish: 503 xato, 100 ms
Menyu: [ 'Osh', 'Manti' ]
Taslim bo'ldik: 404 xato404 da birorta "urinish" qatori yo'q: shart false qaytardi va xato darhol tashlandi. signal?. — signal berilmagan bo'lsa (undefined), chaqiruv o'tkazib yuboriladi (?? va ?.). holatlar[chaqiruv++] ?? 200 — ro'yxat tugagach server "200 — hammasi yaxshi" deb javob beradi.
git add 11/15-asinxron-naqshlar/qayta-urin.mjs
git commit -m "11/15: shart va signal bilan qaytaUrin"10. Real ishda
- TanStack Query (React va Vue'dagi eng mashhur so'rov kutubxonasi) — standart sozlamasi bilan brauzerda xato bergan so'rovni 3 marta qayta uradi, kutish:
Math.min(1000 * 2 ** urinish, 30000)ms (5.104 versiyasi manba kodidan). Bugungi formulaning o'zi. - npm paketlari.
p-limit(7.3 versiyasi) — hovuz,p-retry8.0-bob — qayta urinish. Ichida — bugungi kod. Paketlarni npm bilan o'rnatish 16-qismda chuqurlashadi. - To'lov va bulut API'lari. Katta xizmatlarning rasmiy kutubxonalari xatoda jitter bilan qayta urinadi va pul o'tkazmalari uchun idempotency key talab qiladi — ikki marta yechib olmaslik uchun.
- Telegram botlar. Telegram Bot API juda ko'p so'rovga 429 va "N soniya kut" javobini beradi — bot yozganda shu raqamni hurmat qiladigan qayta urinish kerak.
- Intervyu. "Exponential backoff nima va nega jitter kerak?", "N ta so'rovni bir vaqtda ko'pi bilan 3 tadan qanday bajarasiz?", "Qidiruvdagi race condition'ni qanday tuzatasiz?" — o'rta va yuqori darajadagi intervyularda tez-tez beriladi.
Xulosa
- Timeout:
Promise.racefaqat kutishni to'xtatadi;AbortSignal.timeoutishni ham to'xtatadi (TimeoutError). - Retry: faqat vaqtinchalik xatolarda va idempotent amallarda; kechikish
Math.min(maks, asosiy * 2 ** (n - 1)), ustiga jitter. - Tasodif — parametr: sinovda aniq funksiya, ilovada
Math.random. - Hovuz: N ta ishchi bitta navbatdan funksiyalarni oladi; natijalar asl tartibda.
- Navbat:
oxirgi = oxirgi.then(ish)— ishlar birin-ketin, xato navbatni to'xtatmaydi. - Race condition: javoblar tartibiga ishonmang — so'rov raqami yoki oldingisini
abort(); ikki marta bosishga — bitta umumiy jarayon.
Keyingi dars: Dinamik import() va top-level await — modulni kerak bo'lgandagina yuklash, await ni modulning eng yuqori qatorida yozish va vazifalar ilovasida eksport modulini bosilganda yuklash.
Manbalar
- Marc Brooker, "Exponential Backoff And Jitter" (AWS Architecture Blog, 2015) — aws.amazon.com/blogs/architecture
- MDN: "AbortSignal.timeout()", "AbortSignal.any()", "Promise.race()" — developer.mozilla.org
- TanStack Query 5.104.1 manba kodi:
packages/query-core/src/retryer.ts(defaultRetryDelay,retry) — github.com/TanStack/query - RFC 9110 "HTTP Semantics": 429, 503,
Retry-After, idempotent metodlar — rfc-editor.org
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!