۳۰ شهریور ۱۴۰۳

Techboy

اخبار و اطلاعات روز تکنولوژی

با مدل جدید همزمانی ساختاریافته جاوا شروع کنید

جاوا یک مدل همزمان قدرتمند جدید دارد و با موضوعات سنتی و مجازی کار می کند. در اینجا اولین نگاهی به همزمانی ساختاریافته است.

جاوا یک مدل همزمان قدرتمند جدید دارد و با موضوعات سنتی و مجازی کار می کند. در اینجا اولین نگاهی به همزمانی ساختاریافته است.

همزمانی ساختاریافته راه جدیدی برای استفاده از چند رشته ای در جاوا است. این به توسعه دهندگان اجازه می دهد تا در مورد کار در گروه های منطقی فکر کنند و در عین حال از مزیت های سنتی و رشته های مجازی استفاده کنند. در پیش‌نمایش در جاوا ۲۱ موجود است، همزمانی ساختاریافته یکی از جنبه‌های کلیدی آینده جاوا است، بنابراین اکنون زمان خوبی برای شروع کار با آن است.

چرا به همزمانی ساختاریافته نیاز داریم

نوشتن نرم افزار همزمان یکی از بزرگترین چالش ها برای توسعه دهندگان نرم افزار است. مدل رشته جاوا آن را به یک رقیب قوی در بین زبان های همزمان تبدیل می کند، اما چند رشته ای همیشه ذاتاً مشکل بوده است. همزمانی ساختاریافته به شما این امکان را می دهد که از چندین رشته با دستور برنامه نویسی ساخت یافته استفاده کنید. در اصل، راهی برای نوشتن نرم‌افزار همزمان با استفاده از جریان‌ها و ساختارهای برنامه آشنا فراهم می‌کند. این به توسعه‌دهندگان اجازه می‌دهد تا به‌جای هماهنگ‌سازی رشته‌ها، روی کسب‌وکار مورد نظر تمرکز کنند. همانطور که JEP برای همزمانی ساختاریافته می‌گوید، “اگر یک کار به وظایف فرعی همزمان تقسیم شود، همه آنها به همان کار باز می‌گردند. مکان، یعنی بلوک کد کار.”

رشته‌های مجازی، که اکنون یکی از ویژگی‌های رسمی جاوا است، امکان تولید رشته‌های ارزان قیمت را برای دستیابی به عملکرد همزمان ایجاد می‌کند. همزمانی ساختاریافته، نحو ساده ای را برای انجام این کار فراهم می کند. در نتیجه، جاوا اکنون دارای یک سیستم رشته ای منحصر به فرد و بسیار بهینه شده است که درک آن نیز آسان است.

کلاس جدید StructuredTaskScope

کلاس اصلی در همزمانی ساختاریافته java.util.concurrent.StructuredTaskScope است. اسناد جاوا ۲۱ شامل نمونه‌هایی از نحوه استفاده از همزمانی ساختاریافته است< /a>. در زمان نوشتن این مقاله، باید از --enable-preview و --source 21 یا --source 22 استفاده کنید. همزمانی ساختاریافته را در برنامه های جاوا خود فعال کنید. $java --نسخه من openjdk 22-ea است، بنابراین مثال ما با استفاده از Maven --enable-preview --source 22 را برای مرحله کامپایل و --enable-preview برای مرحله اجرا. (توجه داشته باشید که SDKMan گزینه خوبی برای مدیریت چندین نصب JDK است.)

می‌توانید کد نمونه را در مخزن GitHub برای این مقاله پیدا کنید. به فایل .mvn/jvm.config توجه کنید که --enable-preview را برای اجرا تنظیم می کند. برای اجرای کد، از $mvn clean compile exec:java استفاده کنید.

چند نخی با همزمانی ساختاریافته

برای مثال‌های خود، چندین درخواست از API جنگ ستارگان (SWAPI) می‌کنیم تا اطلاعاتی درباره سیارات دریافت کنیم. با شناسه آنها اگر این کار را در جاوای همگام استاندارد انجام می‌دادیم، احتمالاً با استفاده از Apache HTTPClient کاری مانند Listing 1 انجام می‌دادیم.


package com.infoworld;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class App {
  public String getPlanet(int planetId) throws Exception {
    System.out.println("BEGIN getPlanet()");
    String url = "https://swapi.dev/api/planets/" + planetId + "/";
    String ret = "?";

    CloseableHttpClient httpClient = HttpClients.createDefault();
 
    HttpGet request = new HttpGet(url);
    CloseableHttpResponse response = httpClient.execute(request);

    // Check the response status code
    if (response.getStatusLine().getStatusCode() != 200) {
      System.err.println("Error fetching planet information for ID: " + planetId);
      throw new RuntimeException("Error fetching planet information for ID: " + planetId);
    } else {
      // Parse the JSON response and extract planet information
        ret = EntityUtils.toString(response.getEntity());
        System.out.println("Got a Planet: " + ret);
      }

      // Close the HTTP response and client
      response.close();
      httpClient.close();
      return ret;
    }   
    void sync() throws Exception {
      int[] planetIds = {1,2,3,4,5};
      for (int planetId : planetIds) {
        getPlanet(planetId);
      }
    }
    public static void main(String[] args) {
        var myApp = new App();
        System.out.println("\n\r-- BEGIN Sync");
        try {
          myApp.sync();
        } catch (Exception e){
          System.err.println("Error: " + e);
        }
    }
}

در فهرست ۱، ما یک متد اصلی داریم که متد sync() را فراخوانی می‌کند، که به سادگی روی مجموعه‌ای از شناسه‌ها در حین صدور تماس با "https://swapi تکرار می‌شود. dev/api/planets/" + planetId نقطه پایانی. این تماس ها از طریق روش getPlanet() صادر می شوند که از کتابخانه HTTP Apache برای رسیدگی به درخواست، پاسخ و رسیدگی به خطاها استفاده می کند. اساساً، روش هر پاسخ را دریافت می کند و در صورت خوب بودن آن را در کنسول چاپ می کند (۲۰۰). در غیر این صورت، خطا می دهد. (این نمونه‌ها از حداقل خطا استفاده می‌کنند، بنابراین ما فقط RuntimeException را در آن مورد پرتاب می‌کنیم.)

اوراکل پشتیبانی جاوا اسکریپت را در MySQL معرفی می کند

خروجی چیزی شبیه به این است:


-- BEGIN Sync
BEGIN getPlanet()
Got a Planet: {"name":"Tatooine"} 
BEGIN getPlanet()
Got a Planet: {"name":"Alderaan"}
BEGIN getPlanet()
Got a Planet: {"name":"Yavin”}
BEGIN getPlanet()
Got a Planet: {"name":"Hoth"}
BEGIN getPlanet()
Got a Planet: {"name":"Dagobah"}

اکنون بیایید همان مثال را با استفاده از همزمانی ساختاریافته امتحان کنیم. همانطور که در فهرست ۲ نشان داده شده است، همزمانی ساختاریافته به ما امکان می دهد تماس ها را به درخواست های همزمان تقسیم کنیم و همه چیز را در همان فضای کد نگه داریم. در فهرست ۲، وارد کردن StructuredTaskScope لازم را اضافه می کنیم، سپس از روش های اصلی آن، fork() و join() برای شکستن هر درخواست استفاده می کنیم. وارد رشته خودش شود و سپس روی همه آنها صبر کنید.


package com.infoworld;

import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.*;
//...

public class App {
    public String getPlanet(int planetId) throws Exception {
      // ... same ...
    }
    
     void sync() throws Exception {
        int[] planetIds = {1,2,3,4,5};
        for (int planetId : planetIds) {
          getPlanet(planetId);
        }
      }
    void sc() throws Exception {
      int[] planetIds = {1,2,3,4,5};
        try (var scope = new StructuredTaskScope<Object>()) {
          for (int planetId : planetIds) {
            scope.fork(() -> getPlanet(planetId));
          } 
          scope.join();
        }catch (Exception e){
          System.out.println("Error: " + e);
        }
    }
    public static void main(String[] args) {
      var myApp = new App();
      // ...
      System.out.println("\n\r-- BEGIN Structured Concurrency");
      try {
        myApp.sc();
      } catch (Exception e){
        System.err.println("Error: " + e);
      }    
    }
}

اگر لیست ۲ را اجرا کنیم، خروجی مشابهی دریافت خواهیم کرد، اما بسیار سریعتر است زیرا درخواست ها به طور همزمان صادر می شوند و به طور همزمان ادامه می یابند. تفاوت بین روش sc() (با استفاده از multithreading) در مقابل روش sync() که از کد همزمان استفاده می کند را در نظر بگیرید. تفکر همزمانی ساختاریافته چندان سخت‌تر نیست، اما نتایج بسیار سریع‌تری ارائه می‌دهد.

نحوه کار با String.Create در سی شارپ

کار با وظایف و وظایف فرعی

به‌طور پیش‌فرض، وقتی StructuredTaskScope ایجاد می‌شود، از رشته‌های مجازی استفاده می‌کند، بنابراین ما در اینجا رشته‌های سیستم عامل را تهیه نمی‌کنیم. در عوض، ما به JVM می گوییم که درخواست ها را به کارآمدترین روش هماهنگ کند. (سازنده StructuredTaskScope همچنین یک ThreadFactory را می پذیرد.)

در فهرست ۲، ما شی StructuredTaskScope را در یک بلوک try-with-resource ایجاد می کنیم، که روشی است که برای استفاده طراحی شده است. با استفاده از fork() می‌توانیم به تعداد مورد نیاز شغل ایجاد کنیم. متد fork() هر چیزی را که Callable را پیاده‌سازی کند، یعنی هر متد یا تابعی را می‌پذیرد. در اینجا روش getPlanet() خود را در یک تابع ناشناس قرار می دهیم: () -> getPlanet(planetId)—یک نحو مفید برای ارسال آرگومان به تابع هدف.

وقتی join() را فرا می‌خوانیم، به scope می‌گوییم که در تمام کارهایی که فورک شده‌اند منتظر بمانند. اساساً، join() ما را به حالت همزمان برمی‌گرداند. کارهای انشعابی طبق پیکربندی TaskScope ادامه خواهند یافت.

بستن محدوده کار

از آنجایی که TaskScope را در یک بلوک try-with-resource ایجاد کردیم، وقتی آن بلوک به پایان رسید، محدوده به طور خودکار بسته می شود. این فرآیند shutdown() را برای scope فراخوانی می‌کند، که می‌توان آن را سفارشی کرد تا در صورت نیاز، از بین بردن رشته‌های در حال اجرا مدیریت شود. روش shutdown() همچنین می‌تواند به صورت دستی فراخوانی شود، اگر بخواهید دامنه را قبل از بسته شدن خاموش کنید.

StructuredTaskScope شامل دو کلاس است که سیاست‌های خاموش کردن داخلی را اجرا می‌کنند: ShutDownOnSuccess و ShutDownOnFailure. اینها به دنبال یک کار فرعی موفق یا خطا هستند، و سپس بقیه رشته‌های در حال اجرا را لغو می‌کنند. با استفاده از تنظیمات فعلی، می‌توانیم از این کلاس‌ها به صورت زیر استفاده کنیم:


void failFast() throws ExecutionException, InterruptedException {
   int[] planetIds = {1,2,3,-1,4};
   try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
     for (int planetId : planetIds) {
       scope.fork(() -> getPlanet(planetId));
     } 
     scope.join();
   }
 }
 void  succeedFast() throws ExecutionException, InterruptedException {
   int[] planetIds = {1,2};
   try (var scope = new StructuredTaskScope.ShutdownOnSuccess()) {
     for (int planetId : planetIds) {
       scope.fork(() -> getPlanet(planetId));
     } 
     scope.join();
   } catch (Exception e){
     System.out.println("Error: " + e);
  }
} 
public static void main(String[] args) {
  var myApp = new App();
  System.out.println("\n\r-- BEGIN succeedFast");
  try {
    myApp. succeedFast();
  } catch (Exception e) {
    System.out.println(e.getMessage());
  }      
  System.out.println("\n\r-- BEGIN failFast");
        try {
            myApp.failFast();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

    }

این خط‌مشی‌ها خروجی مشابه زیر را ارائه می‌دهند:


-- BEGIN succeedFast
BEGIN getPlanet()
BEGIN getPlanet()
Got a Planet: {"name":"Alderaan"}
org.apache.http.impl.execchain.RetryExec execute
INFO: I/O exception (java.net.SocketException) caught when processing request to {s}->https://swapi.dev:443: Closed by interrupt

-- BEGIN failFast
BEGIN getPlanet()
BEGIN getPlanet()
BEGIN getPlanet()
BEGIN getPlanet()
BEGIN getPlanet()
Got a Planet: {"name":"Hoth"}
Got a Planet: {"name":"Tatooine"}
Error fetching planet information for ID: -1
org.apache.http.impl.execchain.RetryExec execute
INFO: I/O exception (java.net.SocketException) caught when processing request to {s}->https://swapi.dev:443: Closed by interrupt

بنابراین آنچه ما داریم یک مکانیسم ساده برای شروع همه درخواست‌ها به صورت همزمان است، و سپس در صورت موفقیت یا شکست از طریق استثنا، بقیه درخواست‌ها را لغو می‌کنیم. از اینجا می توان هر گونه سفارشی سازی را انجام داد. اسناد همزمانی ساختاریافته شامل یک مثال از جمع آوری است به محض موفقیت یا شکست، نتایج زیر کار را نشان می دهد و سپس نتایج را برمی گرداند. این کار به سادگی با نادیده گرفتن روش join() و مشاهده نتایج هر کار انجام می شود.

TypeScript 4.5 با بهبودهای Promise می آید

StructuredTaskScope.Subtask

چیزی که در مثال خود ندیده‌ایم، مشاهده مقادیر بازگشتی وظایف فرعی است. هر بار که StructuredTaskScope.fork() فراخوانی می شود، یک شی StructuredTaskScope.SubTask برگردانده می شود. ما می توانیم از این برای تماشای وضعیت وظایف استفاده کنیم. برای مثال، در روش sc()، می‌توانیم کارهای زیر را انجام دهیم:


import java.util.concurrent.StructuredTaskScope.Subtask;
import java.util.ArrayList;

void sc() throws Exception {
      int[] planetIds = {1,2,3,4,5};
      ArrayList<Subtask> tasks = new ArrayList<Subtask>(planetIds.length); 
        try (var scope = new StructuredTaskScope<Object>()) {
          for (int planetId : planetIds) {
            tasks.add(scope.fork(() -> getPlanet(planetId)));
          } 
          scope.join();
        }catch (Exception e){
          System.out.println("Error: " + e);
        }
      for (Subtask t : tasks){
        System.out.println("Task: " + t.state());
      }
    }

در این مثال، هر کار را می‌گیریم و آن را در ArrayList نگه می‌داریم، سپس وضعیت را پس از join() روی آن‌ها خروجی می‌دهیم. توجه داشته باشید که حالت های موجود برای Subtask بر روی آن به عنوان enum تعریف شده است. این روش جدید چیزی شبیه به این خروجی خواهد داشت:


-- BEGIN Structured Concurrency
BEGIN getPlanet()
BEGIN getPlanet()
BEGIN getPlanet()
BEGIN getPlanet()
BEGIN getPlanet()
Got a Planet: {"name":"Dagobah"}
Got a Planet: {"name":"Hoth"}
Got a Planet: {"name":"Tatooine"}
Got a Planet: {"name":"Yavin IV"}
Got a Planet: {"name":"Alderaan"}
Task: SUCCESS
Task: SUCCESS
Task: SUCCESS
Task: SUCCESS
Task: SUCCESS

درخت های رشته را در همزمانی ساختاریافته

هم‌زمانی ساختاریافته شامل پشتیبانی از اشکال‌زدایی و درک چگونگی ارتباط رشته‌ها می‌شود. به طور خاص، همزمانی ساختاریافته، همه رشته‌ها را به یک ساختار درختی مرتبط می‌کند که دامنه آن در ریشه است. این امر باعث می‌شود که ببینید رشته‌ها چگونه به هم مرتبط هستند، حتی در هنگام استفاده از دامنه‌های تودرتو. اسناد نمونه خوبی از استفاده از دستور عیب یابی جاوا (jcmd ابزار برای ریختن طرح زمان اجرا رشته ها به کنسول. 

نتیجه گیری

بین رشته‌های مجازی و همزمانی ساختاریافته، توسعه‌دهندگان جاوا مکانیزم جدید قانع‌کننده‌ای برای تقسیم تقریباً هر کدی به وظایف همزمان بدون سربار زیاد دارند. زمینه و الزامات مهم هستند، بنابراین فقط از این ابزارهای همزمانی جدید استفاده نکنید زیرا وجود دارند. در عین حال، این ترکیب قدرت جدی را ارائه می دهد. هر زمان که با گلوگاهی روبرو می شوید که در آن وظایف زیادی انجام می شود، می توانید به راحتی همه آنها را به موتور رشته مجازی بسپارید، که بهترین راه را برای هماهنگ کردن آنها پیدا می کند. مدل رشته جدید با همزمانی ساختاریافته نیز سفارشی سازی و تنظیم دقیق این رفتار را آسان می کند.

بسیار جالب خواهد بود که ببینیم توسعه‌دهندگان چگونه از این قابلیت‌های همزمانی جدید در برنامه‌ها، چارچوب‌ها و سرورهای ما در آینده استفاده می‌کنند.