CodeGym /Kurslar /JAVA 25 SELF /Parallel proqramlaşdırmanın ən yaxşı təcrübələri

Parallel proqramlaşdırmanın ən yaxşı təcrübələri

JAVA 25 SELF
Səviyyə , Dərs
Mövcuddur

1. Paralelləşdirmək mümkün olan hər şeyi paralelləşdirmək lazım deyil

Java-da tapşırıqları paralelləşdirməyin bir çox yolu var. Lakin “paralellik = həmişə daha sürətlidir” — bu, şorbaya bir az da duz əlavə etsən, dadının yaxşılaşacağını düşünməyə bənzəyir: müəyyən yerə qədər — bəli, ondan sonra isə — sınamamaq daha yaxşıdır.

ExecutorService açıq şəkildə müəyyən edilmiş tapşırıqları işə salmaq və idarə etmək lazım olanda əladır: sorğuların işlənməsi, asinxron məlumat yüklənməsi, müstəqil hesablamalar. Siz özünüz hovuzda neçə thread olacağına qərar verirsiniz və tapşırıqların həyat dövrünü idarə edirsiniz.

parallelStream — əməliyyatlar müstəqil və yan təsirsiz olduqda kolleksiyaların emalını tez paralelləşdirməyin yoludur. “Ağır” kolleksiyalar üçün sərfəlidir (on minlərlə element və daha çox).

ForkJoinPool — alt tapşırıqlara yaxşı bölünən (divide & conquer) tapşırıqlar üçün seçimdir: çeşidləmə, axtarış, böyük massivlərin aqreqasiyası. parallelStream daxilində istifadə olunur, lakin onu birbaşa da idarə etmək olar.

Paralelliyi “hər ehtimala qarşı” istifadə etməyin. Tapşırıq kiçikdirsə, planlaşdırma, kontekst dəyişimi və sinxronizasiya xərcləri bütün qazancı yeyə bilər.

Nümunə: paralellik lazım olmadıqda

List<Integer> smallList = List.of(1, 2, 3, 4, 5);
int sum = smallList.parallelStream()
    .mapToInt(x -> x)
    .sum(); // 5 ədəd üçün paralelləşdirmək — artıqlıqdır!

2. Thread-safety: ümumi dəyişən məlumatlardan qaçın

Parallel dünyada əsas təhdid — məlumat yarışları (race conditions). Bir neçə thread eyni dəyişəni dəyişirsə, nəticə gözlənilməz ola bilər.

  • Ümumi mutable dəyişənlərdən qaçının. Hətta counter++ ifadəsi belə atomik deyil.
  • Thread-safe kolleksiyalardan və atomik əməliyyatlardan istifadə edin. Məsələn, ConcurrentHashMap, CopyOnWriteArrayList, AtomicInteger, AtomicLong.
  • Paralel stream-lərdə yan təsirlərsiz. parallelStream-dən xarici strukturları dəyişməyin.

Pis kod nümunəsi

List<Integer> numbers = Arrays.asList(1,2,3,4,5);
List<Integer> result = new ArrayList<>();
numbers.parallelStream().forEach(n -> result.add(n * 2)); // TEHLÜKƏLİ!

Burada result.add() — thread-safe deyil. Nəticə — itirilmiş elementlər və ya istisnalar.

Necə düzgün?

List<Integer> result = numbers.parallelStream()
    .map(n -> n * 2)
    .collect(Collectors.toList());

3. Performans: daha çox thread həmişə daha yaxşı deyil

Xırda tapşırıqları paralelləşdirmək sərfəli deyil. İş millisanilərlə ölçülürsə, paralel işə salma tez-tez əlavə xərclərə görə icranı yalnız ləngidir.

Performansı ölçün. Sürətli ölçmələr üçün System.nanoTime() uyğundur:

long start = System.nanoTime();
// ... sizin kodunuz ...
long end = System.nanoTime();
System.out.println("İcra vaxtı: " + (end - start) + " ns");

Ciddi mikrobençmarklar üçün JMH (Java Microbenchmark Harness) istifadə edin.

Nümunə: ardıcıl və paralel stream-in müqayisəsi

List<Integer> bigList = IntStream.range(0, 1_000_000)
    .boxed().collect(Collectors.toList());

long t1 = System.nanoTime();
long sum1 = bigList.stream().mapToLong(x -> x).sum();
long t2 = System.nanoTime();
long sum2 = bigList.parallelStream().mapToLong(x -> x).sum();
long t3 = System.nanoTime();

System.out.println("Ardıcıl: " + (t2 - t1) / 1_000_000 + " ms");
System.out.println("Paralel:     " + (t3 - t2) / 1_000_000 + " ms");

Öz kompüterinizdə sınayın — fayda həqiqətən böyük kolleksiyalarda və ağır əməliyyatlarda nəzərə çarpır.

4. Xətaların işlənməsi: thread-lərdə istisnaları görməməzlikdən gəlməyin

Future və istisnaların işlənməsi

Əgər tapşırığı ExecutorService.submit() ilə işə salmısınızsa, istisnalar avtomatik “ötürülməyəcək” — onları Future.get() vasitəsilə emal etmək lazımdır:

Future<Integer> future = executor.submit(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("Ups!");
    return 42;
});
try {
    Integer result = future.get(); // ExecutionException ata bilər
} catch (ExecutionException e) {
    System.err.println("Tapşırıqda xəta: " + e.getCause());
}

ForkJoin və xətaların işlənməsi

ForkJoinPool-da istisnalar tapşırıq daxilində “qablaşdırılır”. join()/get() çağırışında onlar üzə çıxacaq:

ForkJoinPool pool = new ForkJoinPool();
RecursiveTask<Integer> task = new MyTask();
try {
    int result = pool.invoke(task);
} catch (Exception e) {
    System.err.println("ForkJoin-də xəta: " + e);
}

InterruptedException-in işlənməsini unutmayın

Bir çox metodlar (məsələn, Future.get(), Thread.sleep()) InterruptedException ata bilər. Onu “udmayın” — düzgün reaksiya verin: kəsilmə bayrağını bərpa edin və ya tapşırığı tamamlayın.

5. Paralel kodun sazlanması və test edilməsi

Loglama və debug

Paralel səhvlər kələkbazdır və çox vaxt stabil təkrarlanmır. Thread göstərilərək log yazın: Thread.currentThread().getName(). Bu, kodu kimin və nə zaman icra etdiyini anlamağa kömək edir.

Çətin hallarda çoxthread-li debug dəstəyi olan debugger istifadə edin (məsələn, IntelliJ IDEA). Müvəqqəti Thread.sleep() bəzən nadir data race-i “tutmağa” kömək edir.

Çoxthread-li ssenarilərin test edilməsi

Paralel əməliyyatlar üçün ayrıca testlər ayırın və şərtlərin gözlənməsi üçün utilitilərdən istifadə edin, məsələn Awaitility. Bu cür testləri dəfələrlə işə salın: bəzi problemlər yalnız 100‑cü və ya 1000‑ci işə salmada üzə çıxır.

6. Faydalı nüanslar və məsləhətlər

Oxunaqlılıq və dəstək: anlaşılan paralel kod yazın

  • Sənədləşdirin. Çətin hissələri və alət seçimini şərh edin.
  • Yüksək səviyyəli abstraksiyalardan istifadə edin. Əl ilə thread idarəçiliyindənsə, ExecutorService, parallelStream, ForkJoinPool üstünlük verin.
  • “Sehr”dən qaçın. Mümkündürsə, sinxronizasiyanı çətinləşdirməyin.

Cədvəl: hansı aləti nə vaxt istifadə etmək

Ssenari Tövsiyə olunan alət
Çox sayda müstəqil tapşırıq
ExecutorService
Böyük kolleksiyanın işlənməsi parallelStream və ya ForkJoin
“Böl və hökm sür” tapşırığı
ForkJoinPool + RecursiveTask
Sadə asinxron tapşırıq
CompletableFuture
Çoxlu kiçik tapşırıqlar Ardıcıl stream
Yan təsirli tapşırıqlar Yalnız thread-safe kolleksiyalar!

Paralel proqramçının “qaydaları”

  • Ümumi mutable dəyişənlərdən istifadə etməyin — əgər onların thread-safe olduğuna əmin deyilsinizsə.
  • Təkcə paralelləşdirmək xətrinə paralelləşdirməyin: potensial qazancı qiymətləndirin.
  • Pool-ları bağlamağı unutmayın: shutdown()/shutdownNow().
  • Yan təsirli əməliyyatlar üçün parallelStream istifadə etməyin.
  • FutureForkJoinTask istisnalarını işləməyi unutmayın.
  • InterruptedException-i “udmayın” — tapşırığı düzgün tamamlayın.

7. Parallel proqramlaşdırmada tipik səhvlər

Səhv №1: Kiçik tapşırıqların paralelləşdirilməsi. Tez-tez yeni başlayanlar hər şeyi paralelləşdirirlər, hətta iş mikrosaniyələr çəkəndə belə. Nəticə — əlavə xərclərə görə daha yavaşdır.

Səhv №2: Stream-lərdə yan təsirlər. parallelStream-də xarici dəyişənləri və ya kolleksiyaları dəyişmək olmaz — data race və proqnozlaşdırılmayan səhvlər alacaqsınız.

Səhv №3: İstisnaların görməməzlikdən gəlinməsi. Əgər Future.get() və ya ForkJoinTask-dan gələn xətaları emal etməsəniz, tapşırığın niyə yerinə yetirilmədiyini bilməyəcəksiniz.

Səhv №4: ExecutorService üçün unudulmuş shutdown(). Açıq tamamlanma olmadan tətbiq çıxışda “asılı” qala bilər.

Səhv №5: Thread-safe olmayan kolleksiyaların istifadəsi. Adi ArrayList-ə bir neçə thread-dən yazmaq — səhvlərə birbaşa yoldur.

Səhv №6: InterruptedException-i “udmaq”. Əgər thread kəsilibsə — bunu nəzərə alın və işi düzgün tamamlayın.

Səhv №7: Həddindən artıq mürəkkəb sinxronizasiya məntiqi. Artıq synchronized blokları deadlock/livelock-a aparır. Yüksək səviyyəli abstraksiyalara üstünlük verin.

1
Sorğu/viktorina
, səviyyə, dərs
Əlçatan deyil
Parallellik və ForkJoin
Parallellik və ForkJoin
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION