Java-ning Fork-Join Freymvorki bo'yicha keng qamrovli qo'llanma yordamida parallel qayta ishlash quvvatini oching. Global ilovalaringizda maksimal unumdorlik uchun vazifalarni samarali bo'lish, bajarish va birlashtirishni o'rganing.
Parallel Vazifalarni Bajarishni O'zlashtirish: Fork-Join Freymvorkiga Chuqur Nazar
Bugungi kunda ma'lumotlarga asoslangan va global miqyosda o'zaro bog'langan dunyoda samarali va tezkor ilovalarga bo'lgan talab juda muhim. Zamonaviy dasturiy ta'minot ko'pincha katta hajmdagi ma'lumotlarni qayta ishlashi, murakkab hisob-kitoblarni amalga oshirishi va ko'plab bir vaqtda bajariladigan operatsiyalarni boshqarishi kerak. Ushbu qiyinchiliklarni yengish uchun dasturchilar parallel qayta ishlashga – katta muammoni bir vaqtning o'zida yechilishi mumkin bo'lgan kichikroq, boshqariladigan kichik muammolarga bo'lish san'atiga tobora ko'proq murojaat qilmoqdalar. Java-ning konkurentlik vositalari orasida Fork-Join Freymvorki parallel vazifalarni, ayniqsa hisoblashga yo'naltirilgan va tabiiy ravishda "bo'lib tashla va hukmronlik qil" strategiyasiga mos keladigan vazifalarni bajarishni soddalashtirish va optimallashtirish uchun mo'ljallangan kuchli vosita sifatida ajralib turadi.
Parallelizm Zarurligini Tushunish
Fork-Join Freymvorkining o'ziga xos xususiyatlariga kirishishdan oldin, parallel qayta ishlash nima uchun bunchalik muhim ekanligini anglab olish juda zarur. An'anaga ko'ra, ilovalar vazifalarni ketma-ket, birin-ketin bajarar edi. Garchi bu yondashuv oddiy bo'lsa-da, zamonaviy hisoblash talablari bilan ishlaganda u to'siqqa aylanadi. Millionlab tranzaktsiyalarni qayta ishlashi, turli mintaqalardagi foydalanuvchilarning xatti-harakatlarini tahlil qilishi yoki real vaqtda murakkab vizual interfeyslarni render qilishi kerak bo'lgan global elektron tijorat platformasini tasavvur qiling. Bir oqimli bajarish juda sekin bo'lib, bu foydalanuvchi tajribasining yomonlashishiga va biznes imkoniyatlarining boy berilishiga olib keladi.
Ko'p yadroli protsessorlar hozirda mobil telefonlardan tortib ulkan server klasterlarigacha bo'lgan ko'pgina hisoblash qurilmalarida standart hisoblanadi. Parallelizm bizga ushbu ko'p yadrolarning quvvatidan foydalanish imkonini beradi, bu esa ilovalarga bir xil vaqt ichida ko'proq ish bajarishga imkon beradi. Bu quyidagilarga olib keladi:
- Yaxshilangan unumdorlik: Vazifalar ancha tezroq bajariladi, bu esa ilovaning tezkorroq ishlashiga olib keladi.
- Oshirilgan o'tkazuvchanlik: Muayyan vaqt oralig'ida ko'proq operatsiyalarni qayta ishlash mumkin.
- Resurslardan yaxshiroq foydalanish: Barcha mavjud qayta ishlash yadrolaridan foydalanish bo'sh turgan resurslarning oldini oladi.
- Masshtablashuvchanlik: Ilovalar ko'proq qayta ishlash quvvatidan foydalanib, ortib borayotgan ish yuklamalarini samaraliroq boshqarish uchun masshtablasha oladi.
"Bo'lib Tashla va Hukmronlik Qil" Paradigmasi
Fork-Join Freymvorki taniqli "bo'lib tashla va hukmronlik qil" algoritmik paradigmasiga asoslangan. Bu yondashuv quyidagilarni o'z ichiga oladi:
- Bo'lish (Divide): Murakkab muammoni kichikroq, mustaqil kichik muammolarga ajratish.
- Yengish (Conquer): Ushbu kichik muammolarni rekursiv tarzda yechish. Agar kichik muammo yetarlicha kichik bo'lsa, u to'g'ridan-to'g'ri yechiladi. Aks holda, u yana bo'linadi.
- Birlashtirish (Combine): Kichik muammolarning yechimlarini birlashtirib, asl muammoning yechimini shakllantirish.
Bu rekursiv tabiat Fork-Join Freymvorkini quyidagi kabi vazifalar uchun ayniqsa mos qiladi:
- Massivlarni qayta ishlash (masalan, saralash, qidirish, o'zgartirishlar)
- Matritsa operatsiyalari
- Tasvirlarni qayta ishlash va manipulyatsiya qilish
- Ma'lumotlarni agregatsiya qilish va tahlil qilish
- Fibonachchi ketma-ketligini hisoblash yoki daraxtlarni aylanib chiqish kabi rekursiv algoritmlar
Java'da Fork-Join Freymvorkini Tanishtirish
Java 7 da taqdim etilgan Java'ning Fork-Join Freymvorki "bo'lib tashla va hukmronlik qil" strategiyasiga asoslangan parallel algoritmlarni amalga oshirishning tizimli usulini taqdim etadi. U ikkita asosiy mavhum sinfdan iborat:
RecursiveTask<V>
: Natija qaytaradigan vazifalar uchun.RecursiveAction
: Natija qaytarmaydigan vazifalar uchun.
Ushbu sinflar ForkJoinPool
deb nomlangan maxsus turdagi ExecutorService
bilan ishlatish uchun mo'ljallangan. ForkJoinPool
fork-join vazifalari uchun optimallashtirilgan va uning samaradorligining kaliti bo'lgan ishni o'g'irlash (work-stealing) deb nomlanuvchi texnikadan foydalanadi.
Freymvorkning Asosiy Komponentlari
Keling, Fork-Join Freymvorki bilan ishlashda duch keladigan asosiy elementlarni ko'rib chiqaylik:
1. ForkJoinPool
ForkJoinPool
freymvorkning yuragi hisoblanadi. U vazifalarni bajaradigan ishchi oqimlar pulini boshqaradi. An'anaviy oqimlar pulidan farqli o'laroq, ForkJoinPool
maxsus fork-join modeli uchun mo'ljallangan. Uning asosiy xususiyatlari quyidagilardan iborat:
- Ishni o'g'irlash (Work-Stealing): Bu juda muhim optimallashtirish. Ishchi oqim o'ziga yuklatilgan vazifalarni tugatganda, u bo'sh turmaydi. Buning o'rniga, u boshqa band ishchi oqimlarning navbatlaridan vazifalarni "o'g'irlaydi". Bu barcha mavjud qayta ishlash quvvatidan samarali foydalanishni ta'minlaydi, bo'sh vaqtni minimallashtiradi va o'tkazuvchanlikni maksimallashtiradi. Katta loyiha ustida ishlayotgan jamoani tasavvur qiling; agar bir kishi o'z qismini erta tugatsa, u ishi ko'p bo'lgan kishidan ish olishi mumkin.
- Boshqariladigan Bajarish: Pul oqimlar va vazifalarning hayotiy siklini boshqaradi, bu esa konkurent dasturlashni soddalashtiradi.
- Sozlanuvchi Adolatlilik: U vazifalarni rejalashtirishda turli darajadagi adolatlilik uchun sozlanishi mumkin.
Siz ForkJoinPool
ni shunday yaratishingiz mumkin:
// Umumiy puldan foydalanish (ko'p hollarda tavsiya etiladi)
ForkJoinPool pool = ForkJoinPool.commonPool();
// Yoki maxsus pul yaratish
// ForkJoinPool customPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
commonPool()
bu statik, umumiy pul bo'lib, uni o'zingiz yaratmasdan va boshqarmasdan foydalanishingiz mumkin. U ko'pincha oqilona miqdordagi oqimlar bilan (odatda mavjud protsessorlar soniga qarab) oldindan sozlangan bo'ladi.
2. RecursiveTask<V>
RecursiveTask<V>
bu V
turidagi natijani hisoblaydigan vazifani ifodalovchi mavhum sinf. Undan foydalanish uchun sizga kerak bo'ladi:
RecursiveTask<V>
sinfidan meros olish.protected V compute()
metodini amalga oshirish.
compute()
metodi ichida siz odatda quyidagilarni bajarasiz:
- Asosiy holatni tekshirish: Agar vazifa to'g'ridan-to'g'ri hisoblash uchun yetarlicha kichik bo'lsa, shuni qiling va natijani qaytaring.
- Fork (Bo'lish): Agar vazifa juda katta bo'lsa, uni kichikroq kichik vazifalarga bo'ling. Ushbu kichik vazifalar uchun o'zingizning
RecursiveTask
sinfingizning yangi nusxalarini yarating. Kichik vazifani asinxron tarzda bajarishga rejalashtirish uchunfork()
metodidan foydalaning. - Join (Birlashtirish): Kichik vazifalarni fork qilgandan so'ng, ularning natijalarini kutishingiz kerak bo'ladi. Fork qilingan vazifaning natijasini olish uchun
join()
metodidan foydalaning. Bu metod vazifa tugaguncha bloklanadi. - Birlashtirish: Kichik vazifalardan natijalarni olgandan so'ng, joriy vazifa uchun yakuniy natijani hosil qilish uchun ularni birlashtiring.
Misol: Massivdagi Raqamlar Yig'indisini Hisoblash
Keling, klassik misol bilan ko'rib chiqaylik: katta massivdagi elementlarni yig'ish.
import java.util.concurrent.RecursiveTask;
public class SumArrayTask extends RecursiveTask<Long> {
private static final int THRESHOLD = 1000; // Bo'lish uchun chegara
private final int[] array;
private final int start;
private final int end;
public SumArrayTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
// Asosiy holat: Agar qism-massiv yetarlicha kichik bo'lsa, uni to'g'ridan-to'g'ri yig'ing
if (length <= THRESHOLD) {
return sequentialSum(array, start, end);
}
// Rekursiv holat: Vazifani ikkita kichik vazifaga bo'lish
int mid = start + length / 2;
SumArrayTask leftTask = new SumArrayTask(array, start, mid);
SumArrayTask rightTask = new SumArrayTask(array, mid, end);
// Chap vazifani fork qilish (uni bajarish uchun rejalashtirish)
leftTask.fork();
// O'ng vazifani to'g'ridan-to'g'ri hisoblash (yoki uni ham fork qilish)
// Bu yerda biz bir oqimni band saqlash uchun o'ng vazifani to'g'ridan-to'g'ri hisoblaymiz
Long rightResult = rightTask.compute();
// Chap vazifani join qilish (uning natijasini kutish)
Long leftResult = leftTask.join();
// Natijalarni birlashtirish
return leftResult + rightResult;
}
private Long sequentialSum(int[] array, int start, int end) {
Long sum = 0L;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
}
public static void main(String[] args) {
int[] data = new int[1000000]; // Misol uchun katta massiv
for (int i = 0; i < data.length; i++) {
data[i] = i % 100;
}
ForkJoinPool pool = ForkJoinPool.commonPool();
SumArrayTask task = new SumArrayTask(data, 0, data.length);
System.out.println("Yig'indini hisoblash...");
long startTime = System.nanoTime();
Long result = pool.invoke(task);
long endTime = System.nanoTime();
System.out.println("Yig'indi: " + result);
System.out.println("Ketgan vaqt: " + (endTime - startTime) / 1_000_000 + " ms");
// Taqqoslash uchun, ketma-ket yig'indi
// long sequentialResult = 0;
// for (int val : data) {
// sequentialResult += val;
// }
// System.out.println("Ketma-ket Yig'indi: " + sequentialResult);
}
}
Ushbu misolda:
THRESHOLD
vazifa qachon ketma-ket qayta ishlanishi uchun yetarlicha kichik ekanligini belgilaydi. To'g'ri chegarani tanlash unumdorlik uchun juda muhimdir.compute()
agar massiv segmenti katta bo'lsa, ishni bo'ladi, bir kichik vazifani fork qiladi, ikkinchisini to'g'ridan-to'g'ri hisoblaydi va keyin fork qilingan vazifani join qiladi.invoke(task)
ForkJoinPool
dagi qulay metod bo'lib, u vazifani topshiradi va uning bajarilishini kutadi, natijasini qaytaradi.
3. RecursiveAction
RecursiveAction
RecursiveTask
ga o'xshaydi, lekin u qaytariladigan qiymat hosil qilmaydigan vazifalar uchun ishlatiladi. Asosiy mantiq bir xil bo'lib qoladi: agar vazifa katta bo'lsa, uni bo'ling, kichik vazifalarni fork qiling va agar ularning bajarilishi davom etishdan oldin zarur bo'lsa, ularni join qiling.
RecursiveAction
ni amalga oshirish uchun siz:
RecursiveAction
dan meros olasiz.protected void compute()
metodini amalga oshirasiz.
compute()
ichida siz kichik vazifalarni rejalashtirish uchun fork()
va ularning bajarilishini kutish uchun join()
dan foydalanasiz. Qaytariladigan qiymat bo'lmagani uchun siz ko'pincha natijalarni "birlashtirish"ga hojat qolmaydi, lekin harakatning o'zi tugashidan oldin barcha bog'liq kichik vazifalar tugaganligiga ishonch hosil qilishingiz kerak bo'lishi mumkin.
Misol: Massiv Elementlarini Parallel O'zgartirish
Keling, massivning har bir elementini parallel ravishda o'zgartirishni, masalan, har bir raqamni kvadratga oshirishni tasavvur qilaylik.
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.ForkJoinPool;
public class SquareArrayAction extends RecursiveAction {
private static final int THRESHOLD = 1000;
private final int[] array;
private final int start;
private final int end;
public SquareArrayAction(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected void compute() {
int length = end - start;
// Asosiy holat: Agar qism-massiv yetarlicha kichik bo'lsa, uni ketma-ket o'zgartiring
if (length <= THRESHOLD) {
sequentialSquare(array, start, end);
return; // Qaytariladigan natija yo'q
}
// Rekursiv holat: Vazifani bo'lish
int mid = start + length / 2;
SquareArrayAction leftAction = new SquareArrayAction(array, start, mid);
SquareArrayAction rightAction = new SquareArrayAction(array, mid, end);
// Ikkala kichik harakatni ham fork qilish
// Ko'p fork qilingan vazifalar uchun invokeAll dan foydalanish odatda samaraliroq
invokeAll(leftAction, rightAction);
// Agar biz oraliq natijalarga bog'liq bo'lmasak, invokeAll dan keyin aniq join kerak emas
// Agar siz alohida fork qilib, keyin join qilganingizda:
// leftAction.fork();
// rightAction.fork();
// leftAction.join();
// rightAction.join();
}
private void sequentialSquare(int[] array, int start, int end) {
for (int i = start; i < end; i++) {
array[i] = array[i] * array[i];
}
}
public static void main(String[] args) {
int[] data = new int[1000000];
for (int i = 0; i < data.length; i++) {
data[i] = (i % 50) + 1; // 1 dan 50 gacha qiymatlar
}
ForkJoinPool pool = ForkJoinPool.commonPool();
SquareArrayAction action = new SquareArrayAction(data, 0, data.length);
System.out.println("Massiv elementlarini kvadratga oshirish...");
long startTime = System.nanoTime();
pool.invoke(action); // invoke() harakatlar uchun ham bajarilishni kutadi
long endTime = System.nanoTime();
System.out.println("Massivni o'zgartirish tugallandi.");
System.out.println("Ketgan vaqt: " + (endTime - startTime) / 1_000_000 + " ms");
// Tekshirish uchun birinchi bir nechta elementni ixtiyoriy ravishda chop etish
// System.out.println("Kvadratga oshirilgandan keyingi birinchi 10 element:");
// for (int i = 0; i < 10; i++) {
// System.out.print(data[i] + " ");
// }
// System.out.println();
}
}
Bu yerdagi asosiy nuqtalar:
compute()
metodi massiv elementlarini to'g'ridan-to'g'ri o'zgartiradi.invokeAll(leftAction, rightAction)
ikkala vazifani fork qiladigan va keyin ularni join qiladigan foydali metoddir. Bu ko'pincha alohida fork va join qilishdan ko'ra samaraliroq.
Ilg'or Fork-Join Konsepsiyalari va Eng Yaxshi Amaliyotlar
Fork-Join Freymvorki kuchli bo'lsa-da, uni o'zlashtirish yana bir nechta nozikliklarni tushunishni o'z ichiga oladi:
1. To'g'ri Chegarani Tanlash
THRESHOLD
(chegara) juda muhim. Agar u juda past bo'lsa, siz ko'plab kichik vazifalarni yaratish va boshqarishdan ortiqcha yuklamaga duch kelasiz. Agar u juda yuqori bo'lsa, siz bir nechta yadrolardan samarali foydalana olmaysiz va parallelizmning afzalliklari kamayadi. Universal sehrli raqam yo'q; optimal chegara ko'pincha ma'lum bir vazifaga, ma'lumotlar hajmiga va asosiy apparat ta'minotiga bog'liq. Tajriba qilish muhim. Yaxshi boshlanish nuqtasi ko'pincha ketma-ket bajarish bir necha millisekund vaqt oladigan qiymatdir.
2. Ortiqcha Fork va Join Qilishdan Saqlanish
Tez-tez va keraksiz fork va join qilish unumdorlikning pasayishiga olib kelishi mumkin. Har bir fork()
chaqiruvi pulga vazifa qo'shadi va har bir join()
potentsial ravishda oqimni bloklashi mumkin. Qachon fork qilish va qachon to'g'ridan-to'g'ri hisoblash kerakligini strategik jihatdan hal qiling. SumArrayTask
misolida ko'rinib turganidek, bir shoxni to'g'ridan-to'g'ri hisoblash va boshqasini fork qilish oqimlarni band saqlashga yordam beradi.
3. invokeAll
dan Foydalanish
Sizda bir nechta mustaqil kichik vazifalar bo'lsa va davom etishdan oldin ularni bajarish kerak bo'lsa, har bir vazifani qo'lda fork va join qilishdan ko'ra invokeAll
dan foydalanish odatda afzalroqdir. Bu ko'pincha oqimlardan yaxshiroq foydalanish va yukni muvozanatlashga olib keladi.
4. Istisnolarni Boshqarish
compute()
metodi ichida yuzaga kelgan istisnolar, siz vazifani join()
yoki invoke()
qilganingizda RuntimeException
(ko'pincha CompletionException
) ga o'raladi. Siz ushbu istisnolarni ochib, tegishli tarzda boshqarishingiz kerak bo'ladi.
try {
Long result = pool.invoke(task);
} catch (CompletionException e) {
// Vazifa tomonidan yuzaga kelgan istisnoni boshqarish
Throwable cause = e.getCause();
if (cause instanceof IllegalArgumentException) {
// Maxsus istisnolarni boshqarish
} else {
// Boshqa istisnolarni boshqarish
}
}
5. Umumiy Pulni Tushunish
Ko'pgina ilovalar uchun ForkJoinPool.commonPool()
dan foydalanish tavsiya etilgan yondashuvdir. Bu bir nechta pullarni boshqarish yuklamasidan saqlaydi va ilovangizning turli qismlaridagi vazifalarga bir xil oqimlar pulidan foydalanish imkonini beradi. Biroq, ilovangizning boshqa qismlari ham umumiy puldan foydalanayotgan bo'lishi mumkinligini yodda tuting, bu esa ehtiyotkorlik bilan boshqarilmasa, potentsial ravishda ziddiyatga olib kelishi mumkin.
6. Fork-Join'dan Qachon Foydalanmaslik Kerak
Fork-Join Freymvorki samarali ravishda kichikroq, rekursiv qismlarga bo'linishi mumkin bo'lgan hisoblashga yo'naltirilgan vazifalar uchun optimallashtirilgan. U odatda quyidagilar uchun mos emas:
- I/O ga bog'liq vazifalar: Vaqtining ko'p qismini tashqi resurslarni (tarmoq chaqiruvlari yoki diskdan o'qish/yozish kabi) kutish bilan o'tkazadigan vazifalar asinxron dasturlash modellari yoki hisoblash uchun zarur bo'lgan ishchi oqimlarni band qilmasdan bloklovchi operatsiyalarni boshqaradigan an'anaviy oqimlar pullari bilan yaxshiroq boshqariladi.
- Murakkab bog'liqliklarga ega vazifalar: Agar kichik vazifalar murakkab, norekursiv bog'liqliklarga ega bo'lsa, boshqa konkurentlik naqshlari mosroq bo'lishi mumkin.
- Juda qisqa vazifalar: Vazifalarni yaratish va boshqarish yuklamasi juda qisqa operatsiyalar uchun afzalliklardan ustun kelishi mumkin.
Global Mulohazalar va Foydalanish Holatlari
Fork-Join Freymvorkining ko'p yadroli protsessorlardan samarali foydalanish qobiliyati uni ko'pincha quyidagilar bilan shug'ullanadigan global ilovalar uchun bebaho qiladi:
- Katta hajmdagi ma'lumotlarni qayta ishlash: Kontinentlar bo'ylab yetkazib berish marshrutlarini optimallashtirishi kerak bo'lgan global logistika kompaniyasini tasavvur qiling. Fork-Join freymvorki marshrutni optimallashtirish algoritmlarida ishtirok etadigan murakkab hisob-kitoblarni parallellashtirish uchun ishlatilishi mumkin.
- Real vaqtdagi tahlil: Moliya instituti uni turli global birjalardan bozor ma'lumotlarini bir vaqtning o'zida qayta ishlash va tahlil qilish uchun ishlatishi mumkin, bu esa real vaqtda tushunchalarni taqdim etadi.
- Tasvir va media qayta ishlash: Dunyo bo'ylab foydalanuvchilar uchun tasvir hajmini o'zgartirish, filtrlash yoki video transkodlashni taklif qiladigan xizmatlar ushbu operatsiyalarni tezlashtirish uchun freymvorkdan foydalanishi mumkin. Masalan, kontent yetkazib berish tarmog'i (CDN) uni foydalanuvchi joylashuvi va qurilmasiga qarab turli xil tasvir formatlari yoki o'lchamlarini samarali tayyorlash uchun ishlatishi mumkin.
- Ilmiy simulyatsiyalar: Murakkab simulyatsiyalar (masalan, ob-havoni bashorat qilish, molekulyar dinamika) ustida ishlayotgan dunyoning turli burchaklaridagi tadqiqotchilar freymvorkning og'ir hisoblash yukini parallellashtirish qobiliyatidan foyda olishlari mumkin.
Global auditoriya uchun dastur ishlab chiqishda unumdorlik va sezgirlik juda muhimdir. Fork-Join Freymvorki sizning Java ilovalaringizning samarali masshtablashishini va foydalanuvchilaringizning geografik taqsimotidan yoki tizimlaringizga yuklangan hisoblash talablaridan qat'i nazar, uzluksiz tajribani taqdim etishini ta'minlash uchun mustahkam mexanizmni taqdim etadi.
Xulosa
Fork-Join Freymvorki zamonaviy Java dasturchisining hisoblashga yo'naltirilgan vazifalarni parallel ravishda hal qilish uchun qurol arsenalidagi ajralmas vositadir. "Bo'lib tashla va hukmronlik qil" strategiyasini o'zlashtirib va ForkJoinPool
ichidagi ishni o'g'irlash qudratidan foydalanib, siz ilovalaringizning unumdorligi va masshtablashuvchanligini sezilarli darajada oshirishingiz mumkin. RecursiveTask
va RecursiveAction
ni to'g'ri aniqlash, tegishli chegaralarni tanlash va vazifa bog'liqliklarini boshqarishni tushunish sizga ko'p yadroli protsessorlarning to'liq salohiyatini ochish imkonini beradi. Global ilovalar murakkablik va ma'lumotlar hajmi jihatidan o'sishda davom etar ekan, Fork-Join Freymvorkini o'zlashtirish butun dunyo bo'ylab foydalanuvchilar bazasiga xizmat ko'rsatadigan samarali, sezgir va yuqori unumdorlikka ega dasturiy yechimlarni yaratish uchun zarurdir.
Ilovangizdagi rekursiv ravishda bo'linishi mumkin bo'lgan hisoblashga yo'naltirilgan vazifalarni aniqlashdan boshlang. Freymvork bilan tajriba o'tkazing, unumdorlik o'sishini o'lchang va optimal natijalarga erishish uchun o'z ilovalaringizni nozik sozlang. Samarali parallel bajarishga bo'lgan sayohat davom etmoqda va Fork-Join Freymvorki bu yo'lda ishonchli hamrohdir.