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

Techboy

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

درک ScopedValue جدید جاوا

ScopedValue جایگزینی برای ThreadLocal است و با VirtualThreads و StructuredTaskScope جدید کار می کند. دریابید که مقادیر محدوده می توانند برای برنامه های چند رشته ای شما در جاوا چه کاری انجام دهند.

ScopedValue جایگزینی برای ThreadLocal است و با VirtualThreads و StructuredTaskScope جدید کار می کند. دریابید که مقادیر محدوده می توانند برای برنامه های چند رشته ای شما در جاوا چه کاری انجام دهند.

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

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

ScopedValue راه جدیدی برای دستیابی به رفتاری شبیه ThreadLocal است. هر دو عنصر نیاز به ایجاد داده‌هایی را که به طور ایمن در یک رشته به اشتراک گذاشته می‌شوند را برطرف می‌کنند، اما هدف ScopedValue سادگی بیشتر است. این طراحی شده است تا با VirtualThreads و StructuredTaskScope جدید کار کند، که در کنار هم threading را ساده کرده و آن را قدرتمندتر می کند. همانطور که این ویژگی های جدید به طور منظم استفاده می شوند، ویژگی مقادیر scoped برای رفع نیاز افزایش یافته به مدیریت اشتراک گذاری داده ها در رشته ها در نظر گرفته شده است.

ScopedValue به عنوان جایگزینی برای ThreadLocal

در یک برنامه چند رشته ای، اغلب باید متغیری را که به طور منحصر به فرد برای رشته فعلی وجود دارد، اعلام کنید. فکر کنید این شبیه به یک Singleton است – یک نمونه در هر برنامه – به جز اینکه یک نمونه در هر رشته باشد.

اگرچه ThreadLocal در بسیاری از موارد به خوبی کار می کند، اما محدودیت هایی دارد. این مشکلات به عملکرد نخ و بار ذهنی برای توسعه دهنده خلاصه می شود. هر دو مشکل احتمالاً با استفاده توسعه دهندگان از ویژگی جدید VirtualThreads و معرفی رشته های بیشتر به برنامه های خود افزایش خواهند یافت. JEP برای مقادیر scoped به خوبی محدودیت‌های رشته‌های مجازی را توصیف می‌کند.

یک نمونه ScopedValue آنچه را که با استفاده از ThreadLocal ممکن است به سه روش بهبود می‌بخشد:

  • غیرقابل تغییر است.
  • این توسط نخ های فرزند به ارث می رسد.
  • هنگامی که روش حاوی کامل شد، به طور خودکار حذف می شود.

همانطور که مشخصات ScopedValue می گوید:

طول عمر این متغیرهای هر رشته باید محدود باشد: هر داده ای که از طریق متغیر هر رشته به اشتراک گذاشته می شود، پس از اتمام روشی که در ابتدا داده ها را به اشتراک گذاشته بود، غیرقابل استفاده می شود.

این با مراجع ThreadLocal متفاوت است، که تا زمانی که خود رشته به پایان برسد یا متد ThreadLocal.remove() فراخوانی شود، ادامه دارد.

تغییر ناپذیری هم پیروی از منطق یک نمونه ScopedValue را آسان‌تر می‌کند و هم JVM را قادر می‌سازد تا آن را به شدت بهینه کند.

6 بهترین روش برای تحت کنترل نگه داشتن هزینه های Kubernetes

مقادیر Scoped از یک فراخوانی تابعی (lambda) برای تعریف عمر متغیر استفاده می‌کنند که یک رویکرد غیرمعمول در جاوا است. ممکن است در ابتدا عجیب به نظر برسد، اما در عمل، بسیار خوب کار می کند.

نحوه استفاده از یک نمونه ScopedValue

دو جنبه برای استفاده از یک نمونه ScopedValue وجود دارد: ارائه و مصرف. ما می توانیم این را در سه قسمت در کد زیر مشاهده کنیم.

مرحله ۱: ScopedValue را اعلام کنید:


final static ScopedValue<...> MY_SCOPED_VALUE = ScopedValue.newInstance();

مرحله ۲: نمونه ScopedValue را پر کنید:


ScopedValue.where(MY_SCOPED_VALUE, ACTUAL_VALUE).run(() -> { 
  /* ...Code that accesses ACTUAL_VALUE... */
});

مرحله ۳: نمونه ScopedValue را مصرف کنید (کدی که در مرحله ۲ در انتهای خط فراخوانی می شود):


var fooBar = DeclaringClass.MY_SCOPED_VALUE

جالب ترین بخش این فرآیند فراخوانی به ScopedValue.where() است. این به شما امکان می‌دهد ScopedValue اعلام‌شده را با یک مقدار واقعی مرتبط کنید و سپس متد .run() را فراخوانی کنید و یک تابع callback ارائه دهید که با مقدار تعریف شده برای نمونه ScopedValue.

به خاطر داشته باشید: مقدار واقعی مرتبط با نمونه ScopedValue با توجه به رشته ای که در حال اجرا است تغییر می کند. به همین دلیل است که ما همه این کارها را انجام می دهیم! (مجموعه متغیر ویژه رشته در ScopedValue گاهی اوقات تجسم آن نامیده می شود.)

یک مثال کد اغلب ارزش هزار کلمه را دارد، پس بیایید نگاهی بیندازیم. در کد زیر چندین رشته ایجاد می کنیم و برای هر کدام یک عدد تصادفی ایجاد می کنیم. سپس از یک نمونه ScopedValue برای اعمال آن مقدار به یک متغیر مرتبط با رشته استفاده می کنیم:


import java.util.concurrent.ThreadLocalRandom;

public class Simple {
  static final ScopedValue<Integer> RANDOM_NUMBER = ScopedValue.newInstance();

  public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
       int randomNumber = ThreadLocalRandom.current().nextInt(1, 101);

       ScopedValue.where(RANDOM_NUMBER, randomNumber).run(() -> {
         System.out.printf("Thread %s: Random number: %d\n", Thread.currentThread().getName(), RANDOM_NUMBER.get());
        });
      }).start();
    }
  }
}

تماس نهایی ثابت ScopedValue RANDOM_NUMBER = ScopedValue.newInstance(); RANDOM_NUMBER ScopedValue را به ما می دهد تا در هر جایی از برنامه استفاده شود. در هر رشته، یک عدد تصادفی تولید می‌کنیم و آن را به RANDOM_NUMBER مرتبط می‌کنیم.

سپس، داخل ScopedValue.where() اجرا می کنیم. همه کدهای داخل کنترل کننده RANDOM_NUMBER را به کد خاصی که در رشته فعلی تنظیم شده است، حل می کند. در مورد ما، ما فقط thread و شماره آن را به کنسول خروجی می دهیم.

یک اجرا به این صورت است:


$ javac --release 23 --enable-preview Simple.java 

Note: Simple.java uses preview features of Java SE 23.
Note: Recompile with -Xlint:preview for details.

$ java --enable-preview Simple

Thread Thread-1: Random number: 45
Thread Thread-2: Random number: 100
Thread Thread-3: Random number: 51
Thread Thread-4: Random number: 74
Thread Thread-5: Random number: 37
Thread Thread-0: Random number: 32
Thread Thread-6: Random number: 28
Thread Thread-7: Random number: 43
Thread Thread-8: Random number: 95
Thread Thread-9: Random number: 21

توجه داشته باشید که برای اجرای این کد در حال حاضر باید سوئیچ enable-preview را روشن کنیم. پس از ارتقاء ویژگی مقادیر دامنه، این کار ضروری نخواهد بود.

هر رشته یک نسخه مجزا از RANDOM_NUMBER دریافت می کند. هر جا که به آن مقدار دسترسی داشته باشید، مهم نیست که چقدر عمیق تو در تو باشد، همان نسخه را دریافت می کند — تا زمانی که از داخل آن run() callback نشات می گیرد.

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

استفاده از ScopedValue با StructuredTaskScope

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

فرآیند کلی ترکیب ScopedValue با StructuredTaskScope شبیه به مثال Thread قبلی ما است. فقط نحو متفاوت است:


import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.StructuredTaskScope;

public class ThreadScoped {
  static final ScopedValue<Integer> RANDOM_NUMBER = ScopedValue.newInstance();

  public static void main(String[] args) throws Exception {
    try (StructuredTaskScope scope = new StructuredTaskScope()) {
      for (int i = 0; i < 10; i++) {
        scope.fork(() -> {
          int randomNumber = ThreadLocalRandom.current().nextInt(1, 101);
          ScopedValue.where(RANDOM_NUMBER, randomNumber).run(() -> { 
            System.out.printf("Thread %s: Random number: %d\n", Thread.currentThread().threadId(), RANDOM_NUMBER.get());
          });
          return null;
        });
      }
      scope.join();
    }
  }
}

ساختار کلی یکسان است: ScopedValue را تعریف می کنیم و سپس رشته هایی ایجاد می کنیم و از ScopedValue (RANDOM_NUMBER) در آنها استفاده می کنیم. به جای ایجاد اشیاء Thread، از scope.fork() استفاده می کنیم.

توجه کنید که ما null را از لامبدا که به scope.fork() ارسال می‌کنیم، برمی‌گردانیم، زیرا در مورد ما از مقدار بازگشتی استفاده نمی‌کنیم – فقط یک رشته را به کنسول خروجی می‌دهیم. ممکن است مقداری را از scope.fork() برگردانید و از آن استفاده کنید.

همچنین، توجه داشته باشید که من به تازگی Exception را از main() پرتاب کردم. روش scope.fork() یک InterruptedException را پرتاب می کند که باید در کد تولید به درستی مدیریت شود.

نمونه بالا برای StructuredTaskScope و ScopedValue معمولی است. همانطور که می بینید، آنها به خوبی با هم کار می کنند – در واقع، آنها برای آن طراحی شده اند.

مقادیر محدوده در دنیای واقعی

ما نمونه‌های ساده را بررسی کرده‌ایم تا ببینیم ویژگی مقادیر دامنه‌دار چگونه کار می‌کند. اکنون بیایید به این فکر کنیم که چگونه در سناریوهای پیچیده تر کار می کند. به ویژه، مقدارهای محدوده JEP استفاده را در یک برنامه وب بزرگ که در آن بسیاری از مؤلفه‌ها در حال تعامل هستند، برجسته می‌کند. مؤلفه رسیدگی به درخواست می تواند مسئول به دست آوردن یک شی کاربر (یک «اصل») باشد که نشان دهنده مجوز برای درخواست حاضر است. این یک مدل رشته به ازای درخواست است که توسط بسیاری از فریم ورک ها استفاده می شود. رشته‌های مجازی با جدا کردن رشته‌های JVM از رشته‌های سیستم عامل، این را بسیار مقیاس‌پذیرتر می‌کنند.

هنگامی که کنترل کننده درخواست شی کاربر را به دست آورد، می تواند آن را با حاشیه نویسی ScopedValue در معرض بقیه برنامه قرار دهد. سپس، هر مؤلفه دیگری که از داخل callback where() فراخوانی می‌شود، می‌تواند به شی کاربر thread خاص دسترسی داشته باشد. به عنوان مثال، در JEP، نمونه کد یک مؤلفه DBAccess را نشان می دهد که برای تأیید مجوز به PRINCIPAL ScopedValue متکی است:


/** https://openjdk.org/jeps/429 */
class Server {
  final static ScopedValue<Principal> PRINCIPAL =  ScopedValue.newInstance();

  void serve(Request request, Response response) {
    var level     = (request.isAdmin() ? ADMIN : GUEST);
    var principal = new Principal(level);
    ScopedValue.where(PRINCIPAL, principal)
      .run(() -> Application.handle(request, response));
    }
}

class DBAccess {
    DBConnection open() {
        var principal = Server.PRINCIPAL.get();
        if (!principal.canOpen()) throw new  InvalidPrincipalException();
        return newConnection(...);
    }
}

در اینجا می توانید همان خطوط کلی نمونه اول ما را ببینید. تنها تفاوت این است که اجزای مختلف وجود دارد. متد serve() تصور می‌شود که در هر درخواست رشته‌هایی ایجاد می‌کند، و در جایی پایین تر، فراخوانی Application.handle() با DBAccess.open تعامل خواهد کرد. (). از آنجا که تماس از داخل ScopedValue.where() منشا می گیرد، می دانیم که PRINCIPAL به مقدار تنظیم شده برای این رشته توسط Principal(level) جدید حل می شود. تماس بگیرید.

نکته مهم دیگر این است که تماس‌های بعدی به scope.fork() نمونه‌های ScopedValue تعریف‌شده توسط والدین را به ارث خواهند برد. بنابراین، برای مثال، حتی اگر روش serve() بالا scope.fork() را فراخوانی کند و به وظیفه فرزند در PRINCIPAL.get()< دسترسی داشته باشد. /code>، همان مقدار thread-bound را به عنوان والد دریافت می کند. (شما می توانید شبه کد این مثال را در بخش "ارث بری مقادیر محدوده" JEP ببینید.)

تغییرناپذیری مقادیر دامنه به این معنی است که JVM می‌تواند این اشتراک‌گذاری نخ‌های فرزند را بهینه کند، بنابراین می‌توانیم در این موارد انتظار سربار کارایی پایینی داشته باشیم.

نتیجه گیری

اگرچه همزمانی چند رشته ای ذاتاً پیچیده است، ویژگی های جدیدتر جاوا کمک زیادی به ساده تر و قدرتمندتر کردن آن می کند. ویژگی جدید scoped values ​​یکی دیگر از ابزارهای موثر برای جعبه ابزار توسعه دهنده جاوا است. در مجموع، جاوا ۲۲ یک رویکرد هیجان‌انگیز و به‌طور اساسی بهبود یافته برای threading در جاوا ارائه می‌دهد.

شاید به این مطالب علاقمند باشید