CodeGym /Kurslar /JAVA 25 SELF /Structured Concurrency

Structured Concurrency

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

1. Giriş

Axınlar hərəsi öz partiyasını çalanda

Adi çoxaxınlılıq bəzən dirijorsuz məşqi xatırladır. Hər axın — sanki digərini dinləmədən öz melodiyasını çalan bir musiqiçidir. Kimsə daha tez qurtarır və siqaret çəkməyə gedir, kimsə bir akkorda ilişib qalır, kimsə ümumiyyətlə notları səhv salır və xəta verir. Nəticədə simfoniya yox, kakofoniya alınır: kim harada çaşdı — bunu anlamaq demək olar ki, mümkünsüzdür, hamını birdən dayandırmaq isə ayrıca bir macəradır.

Structured Concurrency bu problemi həll edir. O, pərakəndə axınlardan həqiqi ansambl yaradır: bütün tapşırıqlar bir “dirijor”un altında birləşdirilir. O “dayan” deyən kimi — orkestr susur. Bir musiqiçi büdrəsə — qalanlar ümumi harmoniyanı pozmadan səliqə ilə dayanırlar. Bütün nəticələr və xətalar mərkəzləşdirilmiş şəkildə toplanır, kodun künc-bucağına səpələnmir.

Təsəvvür edin: Musiqiçiləri “kim nə bacarır” prinsipi ilə buraxmırsınız, hamısını bir zalda toplayırsınız. Dirijor var, partiturası var və hətta truba fals da etsə — orkestr dağılmır, çıxışı səliqə ilə tamamlayır.

Structured Concurrency nə verir

  • Tapşırığın vahid “scope”u: bütün subtapşırıqlar bir kod blokunun daxilində yaşayır, onların həyat dövrü bu blokla məhduddur.
  • Proqnozlaşdırılan tamamlanma: valideyn axın bütün subtapşırıqlar bitmədən tamamlanmır.
  • Mərkəzləşdirilmiş ləğv: əgər bir tapşırıq yıxılarsa və ya valideyn bitməyə qərar verərsə — bütün subtapşırıqlar düzgün şəkildə ləğv olunur.
  • Sistemli xəta emalı: subtapşırıqların xətaları aqreqasiya olunur, “səbəblər ağacı”nı (tree of causes) əldə etmək olar.
  • Təmiz və oxunaqlı kod: “asılı qalan” axınlar yoxdur, unudulmuş tapşırıqlar yoxdur, ləğv üzrə yarışlar yoxdur.

Structured Concurrency sadəcə yeni API deyil, yeni düşünmə tərzidir: tapşırıqlar adi kod blokları kimi strukturlandırılmalıdır (məsələn, try-with-resources).

2. Java-da Structured Concurrency statusu

Bu dərs yazılan anda Structured Concurrency statusu Preview (Java 21–23) mərhələsindədir, lakin Java 24/25-də GA (General Availability) mərhələsinə keçidi gözlənilir. API jdk.incubator.concurrent paketində yerləşir. Prod-da istifadə etməzdən əvvəl mütləq JDK versiyanızın aktual reliz qeydlərini yoxlayın!

Əsas siniflər:

  • StructuredTaskScope — tapşırıqlar qrupunu idarə etmək üçün baza sinfi.
  • Variantlar: StructuredTaskScope.ShutdownOnFailure, StructuredTaskScope.ShutdownOnSuccess — tapşırıqların tamamlanma siyasətləri.

StructuredTaskScope-un əsas anlayışları

Model: fork, join və nəticələrin ümumi baxışı

Dirijor (yəni valideyn tapşırıq) işarə verdikdə — subtapşırıqlar öz partiyalarına dağılışır. Bu an fork adlanır — sanki musiqiçiləri müxtəlif zallarda öz hissələrini ifa etməyə buraxırsınız.

Sonra join vaxtı gəlir — dirijor çubuğunu qaldırır və hamı qayıdıb birlikdə final akordunu çalır.

Daha sonra hər iştirakçıdan necə getdiyini soruşa bilərsiniz:

  • resultNow() vasitəsilə — hər şey xətasız ifa edilibsə nəticəni dərhal almaq;
  • throwIfFailed() ilə — heç kimin fals etmədiyinə əmin olmaq. Kimsə notlarda dolaşıbsa, vahid istisna atılır — sanki dirijor deyir: “Orkestrdə nasazlıq var, yenidən başlayırıq”.

Tamamlama siyasətləri

Hər bir dirijorun musiqini nə vaxt dayandırmaq barədə öz qaydası var. Structured Concurrency-də bu, tamamlama siyasəti ilə təyin olunur:

  • ShutdownOnFailure — əgər ən azı bir musiqiçi ritmdən düşərsə, dirijor əlini yelləyir: “Stop! Başdan başlayırıq”. Qalanlar dərhal çalmağı dayandırır.
  • ShutdownOnSuccess — əksinə, kimsə partiyasını ideal ifa edən kimi, dirijor məmnundur: “Bəsdir, davamına ehtiyac yoxdur, artıq qalibimiz var”. Qalanlar susur — ilk uğurlu cavab siyasəti.

Virtual axınlarla iş

Hər subtapşırıq StructuredTaskScope tərəfindən virtual axında işə salınır. Bu, elə bil hər musiqiçiniz anlayışlı və çevikdir, səhnə üçün xüsusi tələbləri yoxdur. Rahatlıqla yüzlərlə, minlərlə belə icraçı yarada bilərsiniz — bunlar ağır “thread”lər deyil, demək olar ki, çəkisiz notlardır və məhz lazım olan anda səslənirlər.

3. Nümunə: HTTP sorğuları aqreqatoru

Praktik tapşırıqa baxaq: üç məlumat mənbəyimiz var (məsələn, üç fərqli server) və cavabı ya hamısından (və aqreqasiya edib), ya da ilk uğurla cavab verəndən almaq istəyirik.

Variant 1: “Hamısı uğurlu olmalıdır” (ShutdownOnFailure)

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

public class AggregatorAllSuccess {
    public static void main(String[] args) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            Future<String> f1 = scope.fork(() -> fetchFromSource1());
            Future<String> f2 = scope.fork(() -> fetchFromSource2());
            Future<String> f3 = scope.fork(() -> fetchFromSource3());

            scope.join(); // bütün tapşırıqların tamamlanmasını gözləyirik
            scope.throwIfFailed(); // ən azı biri uğursuz olarsa — istisna atırıq

            // Bütün tapşırıqlar uğurludursa — nəticələri aqreqasiya etmək olar
            String result = f1.resultNow() + f2.resultNow() + f3.resultNow();
            System.out.println("Aqreqasiya edilmiş nəticə: " + result);
        }
    }

    static String fetchFromSource1() { /* ... */ return "A"; }
    static String fetchFromSource2() { /* ... */ return "B"; }
    static String fetchFromSource3() { /* ... */ return "C"; }
}

Nə baş verir:

  • Hər üç tapşırıq paralel (virtual axınlarda) işə salınır.
  • Ən azı biri yıxılarsa — qalanlar ləğv olunur, istisna atılır.
  • Hamısı uğurludursa — nəticələri təhlükəsiz şəkildə aqreqasiya etmək olar.

Variant 2: “İlk etibarlı olan uğurdur” (ShutdownOnSuccess)

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

public class AggregatorFirstSuccess {
    public static void main(String[] args) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
            Future<String> f1 = scope.fork(() -> fetchFromSource1());
            Future<String> f2 = scope.fork(() -> fetchFromSource2());
            Future<String> f3 = scope.fork(() -> fetchFromSource3());

            scope.join(); // ilk uğurlunu gözləyirik
            scope.throwIfFailed(); // hamısı uğursuz olarsa — istisna atırıq

            String result = scope.result(); // ilk uğurlu tapşırığın nəticəsi
            System.out.println("İlk uğurlu nəticə: " + result);
        }
    }

    static String fetchFromSource1() { /* ... */ return "A"; }
    static String fetchFromSource2() { /* ... */ return "B"; }
    static String fetchFromSource3() { /* ... */ return "C"; }
}

Nə baş verir:

  • Kiminsə tapşırığı uğurla bitən kimi — qalanları ləğv olunur.
  • Hamısı uğursuz olarsa — istisna atılır.

4. Avtomatik ləğv və deqradasiya

StructuredTaskScope siyasət tələb edirsə, qalan tapşırıqların ləğvini özü idarə edir. Məsələn, bir tapşırıq yıxılarsa (ShutdownOnFailure) və ya biri uğurla tamamlanarsa (ShutdownOnSuccess), digər tapşırıqlar ləğv siqnalı (interrupt) alır.

Nümunə: vaxt məhdudu ilə səliqəli tamamlanma

import jdk.incubator.concurrent.StructuredTaskScope;
import java.time.Instant;
import java.util.concurrent.Future;

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> fetchWithTimeout());
    Future<String> f2 = scope.fork(() -> fetchWithTimeout());

    scope.joinUntil(Instant.now().plusSeconds(2)); // maksimum 2 saniyə gözləyirik
    scope.throwIfFailed();

    String result = f1.resultNow() + f2.resultNow();
    System.out.println(result);
}

Tapşırıqlar 2 saniyəyə tamamlanmadıqda — istisna atılacaq, bütün tapşırıqlar ləğv ediləcək.

5. Xətalar və istisnaların emalı

Subtapşırıqların istisnaları scope daxilində necə yönləndirilir

Bəzən konsert zamanı kimsə yenə də notdan yanılır — StructuredTaskScope heç nə olmamış kimi davranmır. Kim dəqiq “məşqdən kənar” ifa etdiyini səliqə ilə qeyd edir və sonra dirijora tam hesabat verir. Siz throwIfFailed() çağırdıqda, o, aqreqasiya olunmuş istisna atır — sanki ümumi haqq-hesab: “Bu gün fals not verənlərin siyahısı budur”. Lazım gələrsə, bu “səbəblər ağacı”nı açıb konkret kimin günahkar olduğunu görə bilərsiniz. Konkret ifaçı haqqında bilmək istəsəniz — Future.exceptionNow() həmin partiyanın necə bitdiyini deyəcək.

Ləğv nə vaxt — uğursuzluq sayılmır

Unutmayın: tapşırığın ləğvi həmişə xəta demək deyil. Dirijor “hamısı, konsert bitdi” deyibsə, musiqiçilər sadəcə alətləri yığırlar — bu, cancelled deməkdir, failed deyil. Xəta yalnız həqiqətən nəyinsə səhv ifa olunduğu halda hesab edilir və həmin istisna ümumi xülasəyə düşəcək.

Nümunə: səbəblər ağacı

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> { throw new RuntimeException("Xəta 1"); });
    Future<String> f2 = scope.fork(() -> { throw new RuntimeException("Xəta 2"); });

    scope.join();
    scope.throwIfFailed(); // hər iki səbəbi olan istisnanı atacaq
} catch (Exception e) {
    e.printStackTrace();
    // Yastılanmış (suppressed) istisnaları e.getSuppressed() vasitəsilə əldə etmək olar
}

6. CompletableFuture ilə müqayisə

StructuredTaskScopeCompletableFuture — hər ikisi paralel tapşırıqları işə salmağa imkan verir, lakin:

  • StructuredTaskScope tapşırıqlar məntiqi bağlı olduqda və birlikdə tamamlanmalı/ləğv olunmalı olduqda (tapşırıqlar iyerarxiyası) əlverişlidir.
  • CompletableFuture iyerarxiya olmadan kompozisiya üçün yaxşıdır (məsələn, transformasiya zəncirləri, reaktiv ssenarilər).

StructuredTaskScope kodu nə zaman sadələşdirir:

  • Bütün subtapşırıqların blokdan çıxmazdan əvvəl tamamlanmasına zəmanət lazım olanda.
  • Mərkəzləşdirilmiş ləğv və xəta emalı tələb olunanda.
  • “Asılı qalan” tapşırıqların qalmaması vacib olanda.

CompletableFuture nə vaxt daha uyğundur:

  • Tapşırıqlar əlaqəsiz olduqda və öz həyatını ayrıca yaşaya bildikdə.
  • Mürəkkəb kompozisiya lazım olduqda (thenCombine, thenCompose və s.).

7. Təcrübə: HTTP sorğuları aqreqatoru

Tapşırıq: 3 mənbəyə sorğu göndərmək, ilk uğurlu cavabı almaq

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

public class HttpAggregator {
    public static void main(String[] args) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
            Future<String> f1 = scope.fork(() -> httpRequest("https://api1.example.com"));
            Future<String> f2 = scope.fork(() -> httpRequest("https://api2.example.com"));
            Future<String> f3 = scope.fork(() -> httpRequest("https://api3.example.com"));

            scope.join();
            scope.throwIfFailed();

            String result = scope.result();
            System.out.println("İlk uğurlu cavab: " + result);
        }
    }

    static String httpRequest(String url) throws Exception {
        // Sorğunun imitasiya edilməsi (HttpClient istifadə etmək olar)
        Thread.sleep((long) (Math.random() * 1000));
        if (Math.random() < 0.3) throw new RuntimeException("Sorğu xətası: " + url);
        return "Cavab: " + url;
    }
}

Tapşırıq: bir subtapşırıq uğursuz olarsa — qalanları düzgün söndürmək

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> httpRequest("https://api1.example.com"));
    Future<String> f2 = scope.fork(() -> httpRequest("https://api2.example.com"));

    scope.join();
    scope.throwIfFailed();

    String result = f1.resultNow() + f2.resultNow();
    System.out.println("Hər iki cavab: " + result);
} catch (Exception e) {
    System.err.println("Tapşırıqlardan birində xəta: " + e.getMessage());
}

8. StructuredTaskScope ilə işləyərkən tipik səhvlər

Xəta №1: join() və ya throwIfFailed() çağırılmayıb.
Əgər join() çağırılmasa, tapşırıqlar blokdan çıxana kimi tamamlanmaya bilər. throwIfFailed() çağırılmasa, subtapşırıqların xətaları nəzərdən qaça bilər.

Xəta №2: tapşırıq tamamlanmadan nəticəni almağa cəhd.
resultNow() tapşırıq tamamlanmadan çağırılsa, IllegalStateException atılacaq. Əvvəlcə join() vasitəsilə tamamlanmasını gözləyin.

Xəta №3: ləğvin görməməzlikdən gəlməsi.
Tapşırıq ləğv olunubsa (məsələn, scope siyasətinə görə), onun nəticəsini almağa cəhd etməyin — istisna alınacaq.

Xəta №4: müxtəlif tamamlama siyasətlərini qarışdırmaq.
Scope daxilində tapşırıqları əl ilə ləğv etməyə çalışmayın — ShutdownOnFailure və ya ShutdownOnSuccess siyasətlərindən istifadə edin.

Xəta №5: uzun CPU-əsaslı tapşırıqları virtual axınlarda işə salmaq.
StructuredTaskScope standart olaraq virtual axınlardan istifadə edir — onlar I/O-bound tapşırıqlar üçün idealdır, lakin ağır hesablamaları sürətləndirmir.

Xəta №6: scope-u bağlamağı unutmaq (try-with-resources yoxdur).
StructuredTaskScope AutoCloseable-ı reallaşdırır — bütün tapşırıqların tamamlanmasına zəmanət vermək üçün həmişə try-with-resources-dan istifadə edin.

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION