جاوا: چند نخی(Multithreading) - بخش 2 - وضعیت رقابت

دوشنبه ۰۱ مهر ۹۸ توسط گیتی قاسمی

جاوا: چند نخی _ بخش 2 _ وضعیت رقابت

بخش اول این مقاله را می‌توانید از اینجا مطالعه کنید.

تصویر توسط patrik perkins در unsplash

"ما، حتی در اینجا، قدرت را در دست داریم و مسئولیت آن را بدوش می کشیم "__ Abraham Lincoln, 1862

قیاس

قیاس1

زمانی که شما می خواهید به شخصی که واقعا به او اهمیت می دهید توصیه ارزشمندی بدهید، اولین قدم گوش دادن به آن ها است. زمانی که شما هر دو بطور همزمان صحبت کنید، در پایان شخص را در وضعیت بدی ترک خواهید کرد.

قیاس 2

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

همین مورد در مورد برنامه های چند نخی نیز صدق می کند. زمانی که شما می خواهید وضعیت یک شی را (موضوعی در زندگی واقعی!)، که مشترک و قابل تغییر است را تغییر دهید و آن را در وضعیت دلخواه بگذارید، مجبورید اینکار را بطور متوالی انجام دهید.

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

" به همراه قدرت بزرگ مسئولیت بزرگ می آید"__ عمو بن، مرد عنکبوتی

اجازه دهید ورود پیدا کنیم به یک سناریوی نمونه ای که توانایی ترک وضعیت یک شی مشترک قابل تغییر، را در یک وضعیت نامطلوب دارد.

از برنامه بانکداری موبایلم، در حال برداشتن 100 دلار هستم. برداشت شامل توالی غیر اتمی از مراحل زیر است:

  1. موجودی در دسترس جاری را بگیر
  2. بررسی کن آیا موجودی بزرگتر یا مساوی مقدار برداشت است یا خیر؟
  3. اگر بله، سپس 100 دلار را از موجودی کم کن.
  4. موجودی را بروز رسانی کن (برای مشاهده دیگر نخهای هیپ)

بطور تصادفی، دوستم همزمان با برداشتم 100 دلار به حسابم واریز می کند.

سپرده شامل دنباله غیر اتمی از مراحل زیر است:

5. موجودی در دسترس جاری را دریافت کن.

6. 100 دلار به موجودی اضافه کن

7. موجودی را بروز رسانی کن (برای مشاهده دیگر نخهای هیپ)

برداشت و واریز در 2 نخ برنامه متفاوت اتفاق می افتد و آن ها هر دو باید یک شی حساب (حساب بانکی) را به اشتراک بگذارند. از آنجا که عملیات واریز و برداشت متشکل از بیش از یک مرحله است و نتیجه پایانی این عملیات وضعیت شی مشترک را تغییر می دهد، سناریوی زیر ممکن است اتفاق بیافتد:

تصویر: تداخل

موجودی باید 100 دلار بوده باشد، اما آن حالا 200 دلار دارد بخاطر اینکه عملیات برداشت و واریز در همان زمان روی حساب مشابهی انجام می شود بدون بررسی ماهیت غیر اتمی عملیات.

به عبارت دیگر، عملیات بدلیل ماهیت غیر اتمی برداشت و واریز و بدلیل دسترسی همزمان در هم تنیده می شود(دچار تداخل می شود); ترک کردن موجودی حساب (وضعیت قابل تغییر مشترک) در یک وضعیت خطرناک متناقض.

بطور موثر، 2 نخ با یکدیگر به سمت هدف نهایی تغییر وضعیت قابل تغییر مشترک رقابت میکنند که منجر به وضعیت رقابت می شود.

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

تصویر توسط noah silliman در unsplash

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

وضعیت قابل تغییر مشترک(Shared Mutable State)

یک نمونه(Instance) یا یک متغیر کلاس که می تواند بوسیله بیش از یک نخ اجرا بطور همزمان تغییر داده شود.__  زیرا آنها در سطح نمونه یا کلاس هستند (هیپ) بجای سطح متد (پشته).

اتمیسیته (یا عملیات اتمی)

توالی عملیات اجرا شده به عنوان یک واحد تنها به منظور تغییر وضعیت سیستم. یا تمام مراحل در عملیات اجرا شده یا در صورت عدم موفقیت، هیچ یک از آنها اجرا نمی شوند و وضعیت سیستم را در حالت مطمئن قرار می دهند.

دخالت(Interference)

چندین نخ بطور همزمان سعی می کنند وضعیت قابل تغییر مشترک را بوسیله اجرای غیر اتمی مجموعه ای از دستورالعمل ها تغییر دهند __ دخالت یکدیگر در فرآیند.

همه عوامل فوق در وضعیت رقابت نقش دارند.

وضعیت رقابت(Race condition)

هنگامیکه وضعیت قابل تغییر مشترک بوسیله دنباله ای از عملیات غیر اتمی بطور همزمان و بوسیله دخالت نخ های اجرا تغییر می کند، وضعیت رقابت موظف است اتفاق بیافتد; رها کردن شی در یک وضعیت نامطلوب.

حالا بگذارید به شرایط این چیزها برسیم ما اکنون می خواهیم به قدرت چند وظیفه ای مان اشاره کنیم.

وضعیت قابل تغییر مشترک: شی حساب

1. نخ برداشت: موجودی بگیر (100)

2. نخ برداشت: چک کن اگر موجودی بالاتر یا مساوی مقدار برداشت بود (100) = true

5. نخ واریز: موجودی را بگیر (100) *دخالت* --اکنون شاهد رقابت هستیم

3. نخ برداشت: کم کن 100 از 100

4. نخ برداشت: بروز رسانی کن موجودی را به 0

نخ واریز نگرانی ندارد زیرا قبلا موجودی را خوانده است

  • نخ واریز: اضافه کن 100 را به 100 که آن را از موجودی مرحله 5 بیاد می آورد. اگرچه موجودی فعلی 0 است.
  • نخ واریز: ذخیره کن موجودی جدید 200 را

من اینبار برنده قرعه کشی 100 دلاری شده ام.

دو نخ – نخ های واریز و برداشت، شی حساب قابل تغییر را به اشتراک می گذارند

من کدهای بالا را در یک حلقه نامحدود اجرا کردم. به طور متوسط، وضعیت رقابت 1 در 500 بار اتفاق می افتد. یعنی، وضعیت نهایی مطلوب موجودی موجود = 100 بدلیل تغییر همزمان همان حساب از چندین Thread مربوط به UserSession نقض شده است (نخ 1 و نخ 2 در این مورد).

براستی قدرت زیاد مسئولیت زیاد هم می آورد.

سناریوی ایدآل

  1. موجودی اولیه 100
  2. برداشت 100
  3. واریز 100
  4. وضعیت پایانی مطلوب : موجودی جاری 100

اجازه دهید اکنون قدرتمان را بطور مسئولانه با هماهنگی دسترسی به وضعیت قابل تغییر مشترک، بگونه ای که خواندن/نوشتن بوسیله چندین نخ تنها بطور متوالی اتفاق می افتد، بکار گیریم.

پلتفرم زبان جاوا مکانیزمی داخلی برای دستیابی به این هماهنگی میان نخ های رقیب که اصطلاحا ناظر(Monitor) یا قفل(Lock)، گفته می‌شود دارد. هر شی یک قفل ذاتی به نام Monitor دارد. ما می توانیم به نخها اجازه دهیم که از آن برای دسترسی پی در پی بوسیله علامتگذاری متد ها باعنوان "همگام شده"(Synchronized) استفاده کنند.

دسترسی همگام با استفاده از قفل ذاتی یا ناظر

 

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

ما میتوانیم حالا استحکام برنامه و خواب بدون کابووس از مشکلات تولید غیر قابل تکرار را تضمین کنیم.

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

با استفاده از یک قفل سفارشی برای محافظت از وضعیت مشترک در برابر دسترسی همزمان

لطفا توجه داشته باشید که وضعیتی که ما میخواهیم از آن در مقابل خواندن/نوشتن محافظت کنیم باید توسط همان شی تضمین شود.

"قفل موجودی جاری" در این حالت هم خواندن و هم نوشتن را محافظت می کند. با استفاده از هماهنگ سازی سطح متد(method level synchronization)، کل متد و سایر متدهای هماهنگ شده قفل می شود; که ممکن است اگر آن متد ها بر دیگر وضعیت های غیر مرتبط عمل کنند مطلوب نباشد.

در چنین مواردی، استفاده از اشیای قفل سفارشی برای هر یک از وضعیت های مستقل معقول است زیرا همیشه توصیه می شود که محدوده هماهنگ سازی باید محدود شود به یک حد ممکن مینیمم تا بتواند پاسخگویی و کارایی برنامه را بهبود بخشد.

عملیات تغییر وضعیت غیر اتمی شکل های زیر را به خود می گیرد:

بررسی_ سپس_عمل (Check-then-Act)

خواندن_ اصلاح_ نوشتن (Read-Modify-Write)

بیایید آنها را سریع در عمل ببینیم قبل از اینکه بحث را ببندیم.

بررسی_ سپس_عمل

 

Singleton: مورد کلاسیک بررسی_سپس_ عمل

  • نخ 1 بررسی می کند و میفهمد نمونه null است.
  • نخ 2 بررسی می کند و می فهمد نمونه null است.
  • نخ 1 متغیر نمونه را مقداردهی اولیه می کند.
  • نخ 1 نمونه تازه ایجاد شده را می گیرد.
  • نخ 2 مجددا متغیر نمونه را مقداردهی اولیه می کند.
  • نخ 2 نمونه تازه ایجاد شده را می گیرد.

وضعیت مطلوب:

تنها یک نمونه از کلاس Singleton در هر لحظه باید موجود باشد.

وضعیت واقعی:

حالا بیش از یک نمونه از Singleton بدلیل رقابت نخ ها ایجاد شده. 

 

خط زمان: بررسی_سپس_عمل

خواندن_تغییر_نوشتن

 

شمارنده :  مورد کلاسیک خواندن_تغییر_نوشتن

  • نخ 1 مقدار شمارنده را 0 می خواند.
  • نخ 2 مقدار شمارنده را 0 می خواند.
  • نخ 1 مقدار شماره را به 1 افزایش می دهد.
  • نخ 2 مقدار شمارنده را به 1 افزایش می دهد.
  • نخ 1 مقدار شماره را 1 می نویسد.
  • نخ 2 مقدار شمارنده را 1 می نویسد.

وضعیت مطلوب:

counter = 2

حالت واقعی:

counter = 1

خلاصه

تغییر همزمان وضعیت قابل تغییر مشترک شامل عملیات غیر اتمی، منجر به وضعیت رقابتی می شود; رها کردن وضعیت در یک وضعیت نامطلوب. بمنظور محافظت در برابر این خطر، برنامه های چند نخی باید همزمان به وضعیت قابل تغییر مشترک دسترسی داشته باشند.

با همگام سازی دسترسی به وضعیت قابل تغییر مشترک، به موارد زیر می رسیم:

  • قفل کردن ممانعت دوجانبه(Mutual exclusion locking) __  مانع دخالت (تداخل) نخ در هنگام اصلاح وضعیت همزمان شامل عملیات های غیر اتمی، بوسیله قفل کردن بلوک بحرانی شود تا بطور دو جانبه مانع دسترسی همزمان نخ ها شود.
  • ثبات حافظه(Memory consistency) __ قابلیت مشاهده وضعیت اصلاح شده بوسیله نخ برنامه را به نخ هایی که متعاقبا به همان وضعیت دسترسی دارند تضمین می کند.

Race Condition — Demo gist

 

package org.practice.java.multithreading;
import java.util.function.*;
public class RaceConditionDemo {
	public static void main(String [] args){
		while(true){
			handleUserSession();
		}
        }

	static void handleUserSession(){
	    Account account = new Account(100.0);
	    UserSession session1 = new UserSession("withdraw", 100.0);
	    UserSession session2 = new UserSession("deposit", 100.0);
	    BiFunction<Account, UserSession, Boolean> accountOpertion
		= new PersonalAccountOperation();
		Thread thread1 = new Thread1(accountOpertion, account, session1);
		Thread thread2 = new Thread2(accountOpertion, account, session2);
		thread1.start();
		thread2.start();
		try {
		    thread1.join();
		    thread2.join();
		} catch (InterruptedException ex){
		    System.out.println(ex);
		}
		if(account.getCurrentBalance() != 100){
		    System.out.println("Race condition has occured: "+ account.getCurrentBalance());
		    System.exit(1);
		} else {
		    System.out.println("Current balance after withdrawal and deposit is" 
			+ account.getCurrentBalance());
		}
	}

	static class Thread1 extends Thread {
	    private final BiFunction<Account, UserSession, Boolean> accountOperation;
	    private final Account account;
	    private final UserSession session;
	    Thread1(BiFunction<Account, UserSession, Boolean> accountOperation,
			    Account account,
			    UserSession session){
		    this.accountOperation = accountOperation;
		    this.account = account;
		    this.session = session;
	    }
	    @Override
	    public void run(){
	    this.accountOperation.apply(account, session);
	    }
	}

	static class Thread2 extends Thread {
	    private final BiFunction<Account, UserSession, Boolean> accountOperation;
	    private final Account account;
	    private final UserSession session;
	    Thread2(BiFunction<Account, UserSession, Boolean> accountOperation,
			    Account account,
			    UserSession session){
		    this.accountOperation = accountOperation;
		    this.account = account;
		    this.session = session;
	    }
	    @Override
	    public void run(){
	    this.accountOperation.apply(account, session);
	    }
	}

	static class PersonalAccountOperation implements BiFunction<Account, UserSession, Boolean>{
	    @Override
	    public Boolean apply(Account account, UserSession session){
		String action = session.getAction();
		double amount = session.getAmount();
	    if("withdraw".equalsIgnoreCase(action)){
		account.decrementCurrentBalance(amount);
	    } else if ("deposit".equalsIgnoreCase(action)) {
		account.incrementCurrentBalance(amount);
	    } 
	    return true;                        
	    }	    
	}

	static class Account {
	    private double currentBalance;

	    Account(double currentBalance){
		this.currentBalance = currentBalance;
	    }

	    public void decrementCurrentBalance(double amount){
		if(currentBalance >= amount) {
			this.currentBalance = this.currentBalance - amount;
			System.out.println("Decrementing from thread: " + Thread.currentThread().getName()
				+ ": " + this.currentBalance);
		}
	    }
	    public void incrementCurrentBalance(double amount){
		this.currentBalance = this.currentBalance + amount;
		System.out.println("Incrementing from thread: " + Thread.currentThread().getName()
			+ ": " + this.currentBalance);
	    }	    

	    public double getCurrentBalance(){
		return this.currentBalance;
	    }
	}

	static class UserSession {	    
	    private final String action;
	    private final double amount;
	    UserSession(String action, double amount){
		this.action = action;
		this.amount = amount;
	    }

	    public String getAction(){
		return this.action;
	    }

	    public double getAmount(){
		return this.amount;
	    }
	}
}

کلیدواژه: چندنخی در جاوا MultiThreading در جاوا آموزش مالتی ترد در جاوا

منابع: blog.usejournal.com

ارسال دیدگاه:
برای ارسال دیگاه باید به سیستم وارد شوید و یا ثبت نام کنید. ثبت نام چند لحظه بیشتر زمان شما را نمیگیرد.