همگام سازی (synchronizing) داده با نخ (Thread) ها در جاوا و کاتلین

همگام سازی (synchronizing) داده با نخ (Thread) ها در جاوا و کاتلین

مفهوم رقابت بین نخ ها (race condition) و همگام‌سازی (synchronizing)

زمانی که چند نخ به طور همزمان بخوان یک داده رو تغییر بدن، ممکنه باعث خراب شدن داده بشن و اصطلاحا میگیم data corruption رخ داده، فرض کنید میخوایم یک متغیر رو با دو عدد به طور همزمان جمع کنیم، برنامه نمیدونه کدوم عدد رو اضافه کنه و ممکنه به طور تصادفی عدد اول رو با متغیر جمع کنه و از عدد دوم صرف نظر کنه، به این وضعیت در چندنخی (multithreading) وضعیت رقابت بین نخ (Thread) ها (race condition) یا تداخل نخ (thread) ها میگیم.

در مثال زیر میخوایم امتیاز حساب یک دانش آموز رو به ۱۰۰ برسونیم و برای اینکار از چندنخی استفاده می کنیم.

public class StudentAccount{

  private static Account account = new Account();

      public static void main(String[] args) {

          ExecutorService service = Executors.newCachedThreadPool();

          for (int i=0; i<100; i++){
              service.execute(new IncrementScoreByOne());
          }

          service.shutdown();

          while (!service.isTerminated());

          System.out.println("student score is " + account.getScore());
      }


      private static class IncrementScoreByOne implements Runnable{
          @Override
          public void run() {
              account.updateScore(1);
          }
      }

      private static class Account{
          private Integer score = 0;

          public void updateScore(int v){
              int newScore = score + v;
              try {
                  Thread.sleep(5);
              }catch (InterruptedException e){
                  throw new RuntimeException(e);
              }

              score = newScore;
          }

          public int getScore() {
              return score;
          }
      }
  }
        
private val account = Account()

fun main() {
    val service = Executors.newCachedThreadPool()

    for (i in 1..100) {
        service.execute(IncrementScoreByOne())
    }

    service.shutdown()

    while (!service.isTerminated);

    println("The student score is " + account.score)
}

private class IncrementScoreByOne : Runnable {
    override fun run() {
        account.updateScore(1)
    }
}

private class Account {

    var score: Int = 0
        private set

    fun updateScore(v: Int) {

        val newScore = score + v

        try {
            Thread.sleep(5)
        } catch (e: InterruptedException) {
            throw RuntimeException(e)
        }

        score = newScore

    }
}
        

در مثال بالا برای بهتر مشخص شدن مشکل از متد (تابع) sleep استفاده کردیم، بعد از اجرای کد میبینیم یک سری اعداد پرت و پلا نشون میده که از ۱۰۰ کمترن علت این موضوع بخاطر تغییر دادن مقدار فیلد score به صورت همزمان توسط چند نخ است.

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

داده (متغیر) های volatile

دوتا نخ به نام های A و B رو در نظر بگیرید، فرض کنید هرکدوم از این دو نخ دارن یک کار روی داده ی مشترک انجام میدن، زمانی که تغییرات توسط نخ A روی داده انجام میشه این تغییرات توسط نخ B فورا خونده نمیشه چون زمانی که داده توسط نخ A دچار تغییر میشه تغییرات به طور موقت روی حافظه ی پنهان (cache) پردازنده ذخیره میشه و نخ B به تغییرات نخ A روی cache پردازنده دسترسی نداره.

برای اینکه تغییرات در داده توسط نخ های دیگه فورا خونده بشه از volatile در جاوا و کاتلین استفاده می کنیم؛ با استفاده از volatile تغییرات بجای حافظه ی پنهان (cache) پردازنده، مستقیم تو حافظه ی موقت (RAM) ذخیره میشن و سایر نخ ها به این تغییرات دسترسی دارن.

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

هنگام استفاده از volatile، مشکل دسترسی همزمان نخ ها به داده حل نمیشه، ولی تغییرات روی داده توسط نخ (thread) های دیگه فورا قابل خوندن است، معمولا زمانی که نیاز داریم از یک متغیر به عنوان flag استفاده کنیم، فیلد (متغیر) رو با volatile تعریف می کنیم.

در جاوا متغیر رو با کلیدواژه ی volatile و در کاتلین با @Volatile تعریف می کنیم.

در مثال زیر هردو کار TaskA و TaskB منتظر اجازه ی کاربر هستند، زمانی که کاربر y یا yes رو وارد کنه هردو کار TaskA و TaskB پیام رو نمایش میدن.

public static void main(String[] args){
    Permission permission = new Permission();

    ExecutorService service = Executors.newFixedThreadPool(3);
    service.execute(new TaskA(permission));
    service.execute(new TaskB(permission));
    service.execute(new UserInputPermissionTask(permission));

    service.shutdown();
}

private static class Permission {
    private volatile boolean granted;
}

private static class TaskA implements Runnable {
    private Permission permission;

    public TaskA(Permission permission) {
        this.permission = permission;
    }

    @Override
    public void run() {
        while (!permission.granted) ;

        System.out.println("A message from TaskA");
    }
}

private static class TaskB implements Runnable{
    private Permission permission;

    public TaskB(Permission permission){
        this.permission = permission;
    }

    @Override
    public void run() {
        while (!permission.granted);

        System.out.println("A message from TaskB");
    }
}

private static class UserInputPermissionTask implements Runnable{

    private Permission permission;

    public UserInputPermissionTask(Permission permission){
        this.permission = permission;
    }


    @Override
    public void run() {
        Scanner input = new Scanner(System.in);

        while (true) {
            System.out.println("Ask permission: Do u wanna print message?(yes/no)");
            String userInput = input.next();
            if (userInput.equalsIgnoreCase("y") || userInput.equalsIgnoreCase("yes")) {
                permission.granted = true;
                System.out.println("Permission Granted!");
                break;
            }
            else {
                System.out.println("Permission Denied!");
            }
        }
    }
}
        
fun main() {
    val permission = Permission()

    val service = Executors.newFixedThreadPool(3)
    service.execute(TaskA(permission))
    service.execute(TaskB(permission))
    service.execute(UserInputPermissionTask(permission))

    service.shutdown()
}


private class Permission {
    @Volatile
    var granted = false
}

private class TaskA(private val permission: Permission) : Runnable {
    override fun run() {
        while (!permission.granted);

        println("A message from TaskA")
    }
}

private class TaskB(private val permission: Permission) : Runnable {
    override fun run() {
        while (!permission.granted);

        println("A message from TaskB")
    }
}

private class UserInputPermissionTask(private val permission: Permission) : Runnable {
    override fun run() {
        val input = Scanner(System.`in`)

        while (true) {
            println("Ask permission: Do u wanna print message?(yes/no)")
            val userInput = input.next()
            if (userInput.equals("y", ignoreCase = true) || userInput.equals("yes", ignoreCase = true)) {
                permission.granted = true
                println("Permission Granted!")
                break
            } else {
                println("Permission Denied!")
            }
        }
    }
}
        

در بالا اگه متغیر granted با volatile تعریف نمیشد هردوکار TaskA و TaskB متوجه تغییرات در مقدار granted توسط UserInputPermissionTask نمیشدن.

داده ای که با volatile تعریف شده رو میتونیم با یک نخ تغییر بدیم و سپس با چند نخ به طور همزمان تغییرات رو بخونیم اما نمیتونیم همزمان با چند نخ داده رو تغییر بدیم، بنابراین volatile مشکل رقابت بین نخ ها رو حل نمیکنه

اگه مثال StudentAccount رو بخوایم با volatile حل کنیم مشابه زیر میشه.

public class StudentAccountVolatile {
    private static final Account account = new Account();

    public static void main(String[] args) {

        ExecutorService service = Executors.newCachedThreadPool();

        while (account.getScore() != 100)
            service.execute(new IncrementScoreByOne());

        service.shutdown();

        while (!service.isTerminated()) ;

        System.out.println(account.getScore());
    }

    private static class IncrementScoreByOne implements Runnable{

        @Override
        public void run() {
            if (account.getScore() != 100)
                account.updateScore(1);
        }
    }

    private static class Account {
        private volatile Integer score = 0;

        public void updateScore(int v) {
            int newScore = score + v;
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            score = newScore;
        }

        public int getScore() {
            return score;
        }
    }
}
        
private val account = Account()

fun main() {
    val service = Executors.newCachedThreadPool()

    while (account.score != 100) service.execute(IncrementScoreByOne())

    service.shutdown()

    while (!service.isTerminated);

    println(account.score)
}

private class IncrementScoreByOne : Runnable {
    override fun run() {
        if (account.score != 100) account.updateScore(1)
    }
}

private class Account {
    @Volatile
    var score: Int = 0
        private set

    fun updateScore(v: Int) {
        val newScore = score + v
        try {
            Thread.sleep(5)
        } catch (e: InterruptedException) {
            throw RuntimeException(e)
        }

        score = newScore
    }
}
        

با توجه به منطق کد باید انتظار داشته باشیم 100 نخ (thread) ایجاد بشه اما مقدار نخ های ایجاد شده بیشتر از 100 خواهد بود، چون ممکنه چندتا نخ بخوان به طور همزمان مقدار score رو افزایش بدن، بنابراین چندتا نخ اجرا میشه ولی فقط یک واحد به score اضافه میشه، چون تداخل وجود داره و تا زمانی که مقدار score به 100 نرسیده باشه این اتفاق ممکنه بارها تکرار میشه و بعضی نخ (thread) ها بیهوده ایجاد و اجرا میشن.

با اینکه با volatile مشکل تا حد زیادی حل شده، ولی همین تداخل ممکنه باعث بشه گاهی هرچند به ندرت خطا هم وجود داشته باشه، مثلا به 100 نرسه و تا 99 یا 98 کار تموم بشه.

یک راه حل بهتر برای حل 100 درصدی مشکل رسیدن score به 100 وجود داره اونم استفاده از قفل هاست، قفل ها رو میتونیم با synchronized یا با کلاس ReentrantLock ایجاد کنیم که در ادامه بهش میپردازیم.

همگام‌سازی با synchronized

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

برای جلوگیری از race condition میتونیم از synchronized برای همگام‌سازی استفاده کنیم، زمانی که از synchronized استفاده بشه، داده قفل میشه و تا زمانی که کار نخ (thread) تموم نشده، نخ های دیگه به داده دسترسی ندارن.

از کلیدواژه ی synchronized میتونیم برای همگام‌سازی آبجکت و متد در جاوا استفاده کنیم و برای همگام‌سازی تابع در کاتلین از @Synchronized و برای همگام‌سازی آبجکت در کاتلین از تابع خطی (inline function) synchronized استفاده کنیم.

همگام‌سازی آبجکت با synchronized

با synchronized میتونیم آبجکت رو قفل کنیم تا هنگام استفاده از آبجکت توسط چند نخ (Thread) دچار race condition نشیم، هنگام استفاده ی نخ (thread) از آبجکت synchronized، آبجکت قفل میشه و نخ های دیگه تا پایان کار به آبجکت دسترسی ندارن.

فرم کلی:

synchronized (expression){
   //statement
}
          
synchronized(lock: Any, block: () -> R)
          

بجای expression متغیری که به آبجکت اشاره می کنه رو می نویسیم و داخل بلوک توابع یا فیلد های آبجکت رو که میخوایم قفل بشن صدا میزنیم.

در مثال StudentAccount میتونیم یک عبارت synchronized برای account تعریف کنیم و از race condition جلوگیری کنیم.

private static class IncrementScoreByOne implements Runnable{
    @Override
    public void run() {
        synchronized (account) {
            account.updateScore(1);
        }
    }
}
          
private class IncrementScoreByOne : Runnable {

    override fun run() {
        synchronized(account, fun(){
            account.updateScore(1)
        })
    }
}
          

همگام‌سازی متد (تابع) با synchronized

با synchronized میتونیم متد (تابع) رو قفل کنیم تا هنگام استفاده از تابع توسط چند نخ دچار race condition نشیم. هنگام استفاده ی نخ (thread) از متد synchronized، متد قفل میشه و تا پایان کار نخ های دیگه ازش نمیتونن استفاده کنن.

در مثال StudentAccount برای اینکه race condition به وجود نیاد کافیه متد (تابع) updateScore رو با synchronized تعریف کنیم.

private static class Account{
  private Integer score = 0;
  public synchronized void updateScore(int v){
      int newScore = score + v;
      try {
          Thread.sleep(5);
      }catch (InterruptedException e){
          throw new RuntimeException(e);
      }
      score = newScore;
  }
  public int getScore() {
      return score;
  }
}
        
private class Account {

    var score: Int = 0
        private set

    @Synchronized
    fun updateScore(v: Int) {
        val newScore = score + v
        try {
            Thread.sleep(5)
        } catch (e: InterruptedException) {
            throw RuntimeException(e)
        }

        score = newScore
    }
}
        

همکاری بین نخ (Thread) ها با متد های wait, notify و notifyAll

فرض کنید دو نخ A و B همزمان دارن اجرا میشن، میتونیم نخ A رو متوقف کنیم و به نخ B اجازه ی ادامه بدیم و برعکس.

برای بدست اوردن چنین عملکردی در کلاس Object جاوا سه متد (تابع) به نام های wait, notify و notifyAll تعریف شده، از این سه تابع میتونیم داخل متد هایی که با synchronized تعریف شده باشن یا عبارت synchronized استفاده کنیم.

متد (تابع) wait

از wait برای متوقف کردن نخ هایی که دارن از آبجکت مورد نظر استفاده میکنن، استفاده کنیم؛ و برای ادامه نخ (thread) منتظر سیگنال میمونه.

متد (تابع) notify

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

متد (تابع) notifyAll

با notifyAll سیگنال ادامه ی کار رو به تمام نخ های متوقف شده با wait روی آبجکت مورد نظر میدیم.

مثال زیر یک مثال کلاسیک برنامه ی تولید کننده و مصرف کننده است (Consumer-Producer) برای درک مفهوم همکاری بین نخ (thread) ها؛ در این مثال یک کلاس داریم و دوتابع به اسم produce و consume، زمانی که مقدار data صفر باشه تابع consume متوقف میشه و سیگنال ادامه ی کار رو به produce می فرسته و تابع produce شروع به کار میکنه تا مقدار داده تغییر کنه، زمانی که مقدار داده تغییر کرد، تابع produce متوقف میشه و سیگنال ادامه ی کار رو به تابع consume میفرسته و consume شروع به ادامه ی کار میکنه.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ConsumerProducerDemo {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);

        ConsumerProducer cp = new ConsumerProducer();

            service.execute(() -> {
                while (true)
                    cp.consume();
            });


            service.execute(() -> {
                while (true)
                    cp.produce();
            });

            service.shutdown();

    }

    private static class ConsumerProducer{
        private int data;


        public synchronized void consume(){
            try {

                while (data == 0) {
                    wait();
                }
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }

            System.out.println("Consuming " + data);
            data = 0;
            notify();
        }

        public synchronized void produce(){

            try {
                while (data != 0) {
                    wait();
                }

                Thread.sleep(2000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }

            data = (int) (1 + (Math.random() * 126));
            System.out.println("Producing " + data);
            notify();
        }
    }
}       
        
fun main(){
    val service = Executors.newFixedThreadPool(2)
    val cp = ConsumerProducer()
    service.execute(Runnable {
        while (true) cp.consume()
    })
    service.execute(Runnable {
        while (true) cp.produce()
    })
    service.shutdown()
}

@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private class ConsumerProducer{
    private var data = 0

    @Synchronized
    fun consume() {

        try {
            while (data == 0) {
                (this as Object).wait()
            }
            Thread.sleep(1000)
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
        
        println("Consuming $data")
        data = 0
        (this as Object).notify()
    }

    @Synchronized
    fun produce() {
        try {
            while (data != 0) {
                (this as Object).wait()
            }
            Thread.sleep(2000)
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
        data = (1 + (Math.random() * 126)).toInt()
        println("Producing $data")
        (this as Object).notify()
    }

}
        

همگام‌سازی با کلاس ReentrantLock

با استفاده از Lock و condition میتونیم به صورت آشکار (explicitly) داده ها رو با نخ ها همگام‌سازی کنیم.

با synchronized یک قفل پنهانی (implicit lock) روی نمونه قرار میدیم و متد یا آبجکت برای نخ های دیگه قفل میشه،

با استفاده از کلاس ReentrantLock میتونیم بدون نیاز به synchronized یک متد یا بلوک رو به صورت آشکار (explicitly) قفل کنیم؛ یکی از پیاده سازی های داخل کلاس ReentrantLock، اینترفیس Lock است که متد های پرکاربرد lock, unlock و newCondition داخل اینترفیس lock تعریف شده اند.

در زیر نمودار uml ارتباط ReentrantLock با اینترفیس Lock و متد های پیاده سازی شده ی داخلش شرح داده شده است.

نمودار uml رابطه ی بین ReentrantLock و Lock
نمودار uml رابطه ی بین ReentrantLock و Lock

در زیر متد های تعریف شده در اینترفیس Lock اورده شده است.

java.util.concurrent.locks.Lock
+lock(): void یک قفل ایجاد میکنه.
+unlock(): void قفل رو آزاد میکنه.
+newCondition(): Condition یک نمونه از Condition رو بر میگردونه.

اینترفیس Lock در کلاس ReentrantLock پیاده شده است.

java.util.concurrent.locks.ReentrantLock
+ReentrantLock() یک نمونه از کلاس ReentrantLock ایجاد میکنه.
+ReentrantLock(fair: boolean) یک نمونه از کلاس ReentrantLock ایجاد میکنه و زمانی که مقدار پارامتر true باشه و نخی که بیشتر از بقیه ی نخ ها تو صف استفاده از داده قرار گرفته، اولین اولویت استفاده از داده بعد از آزاد شدن داده رو داره.

برای جلوگیری از race condition میتونیم مثال StudentAccount رو با ReentrantLock بنویسیم، کافیه یک Lock در کلاس Account تعریف کنیم.

private static class Account{
  private Integer score = 0;

  private Lock lock = new ReentrantLock();

  public void updateScore(int v){
      lock.lock();
      try {
          int newScore = score + v;
          Thread.sleep(5);
          score = newScore;
      }catch (InterruptedException e){
          throw new RuntimeException(e);
      }finally{
        lock.unlock();
      }
  }
  public int getScore() {
      return score;
  }
}
        
private class Account {
  val lock: Lock = ReentrantLock()
  var score: Int = 0
      private set

  fun updateScore(v: Int) {
      lock.lock()
      try {
          val newScore = score + n
          Thread.sleep(5)
          score = newScore
      } catch (e: InterruptedException) {
          throw RuntimeException(e)
      }finally{
          lock.unlock()
      }
  }
}
        

همینطور که در بالا مشخصه بدون استفاده از synchronized تونستیم یک قفل روی متد updateScore ایجاد کنیم که بعد از پایان کار متد با unlock، متد برای نخ (thread) های دیگه آزاد میشه.

اینترفیس Condition

هنگام استفاده از کلاس ReentrantLock بجای متد های wait، notify و notifyAll باید از متدهای اینترفیس Condition استفاده کنیم، برای ایجاد یک نمونه از condition کافیه متد newCondition رو با ReentrantLock صدا بزنیم.

<<java.util.concurrent.Condition>>
+await(): void باعث میشه نخ (thread) فعلی به طور موقت متوقف بشه تا وقتی که سیگنال ادامه بهش داده بشه.
+signal(): void به یکی از نخ (thread) های متوقف شده با await سیگنال اجرا میده.
+signalAll(): void به تمام نخ (thread) های متوقف شده با await سیگنال اجرا میده.

میخوایم یک Buffer رو شبیه سازی کنیم و با دو کار ProducerTask و ConsumerTask مقادیر رو با Buffer نوشته و هنگامی که ظرفیت Buffer پر شد مقادیر نوشته شده رو بخونیم.

در مثال زیر در داخل کلاس Buffer دو نمونه از Condition ایجاد می کنیم یکی برای متد (تابع) write و اون یکی برای متد (تابع) read؛ هنگامی که ظرفیت Buffer پر باشه، با writeCondition متد write رو متوقف میکنیم و داخل متد write با readCondition سیگنال ادامه رو به متد read میدیم تا ظرفیت Buffer با متد read خالی بشه و زمانی که buffer با read خالی شد، داخل تابع read با writeCondition دوباره سیگنال ادامه ی کار write رو رو میدیم و با readCondition کار متد read رو متوقف می کنیم؛ متغیر CAPACITY ظرفیت Buffer و مقدارش ثابت و برابر با 1 است

در زیر به صورت تصویری عملکرد این برنامه شرح داده شده است.

نمودار عملکرد برنامه ی شبیه سازی Buffer با condition
نمودار عملکرد برنامه ی شبیه سازی Buffer با condition
public class ConsumerProducerReentrantLockDemo {

    private static final Buffer buffer = new Buffer();

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);
        service.execute(new ProducerTask());
        service.execute(new ConsumerTask());

        service.shutdown();
    }

    private static class ProducerTask implements Runnable{
        @Override
        public void run() {
            while (true){
                int value = (int)(Math.random() * 256);
                System.out.println("Producer writes " + value);
                buffer.write(value);
                try {
                    Thread.sleep((long)(Math.random() * 10000));
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

        }
    }

    private static class ConsumerTask implements Runnable{
        @Override
        public void run() {
            while (true){
                int value = buffer.read();
                System.out.println("\t\t\tConsumer reads " + value);
                try {
                    Thread.sleep((int)(Math.random() * 10000));
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    private static class Buffer{
        private static final int CAPACITY = 1;
        private final Queue<Integer> queue = new LinkedList<>();

        private final Lock lock = new ReentrantLock();

        private final Condition writeCondition = lock.newCondition();
        private final Condition readCondition = lock.newCondition();

        public void write(int value){
            lock.lock();

            try{
                while (isFull()) {
                    System.out.println("Wait for reading buffer");
                    writeCondition.await();
                }

                queue.offer(value);
                readCondition.signal();
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }


        public int read(){
            int value = 0;
            lock.lock();
            try {
                while (isEmpty()){
                    System.out.println("\t\t\tWait for writing buffer");
                    readCondition.await();
                }

                value = queue.remove();
                writeCondition.signal();

            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }

            return value;
        }

        private boolean isEmpty(){
            return queue.isEmpty();
        }

        private boolean isFull(){
            return queue.size == CAPACITY;
        }
    }
}

        
private val buffer = Buffer()

fun main() {
    val service = Executors.newFixedThreadPool(2)
    service.execute(Producer())
    service.execute(Consumer())

    service.shutdown()
}

private class ProducerTask : Runnable {
    override fun run() {
        while (true) {
            val value = (Math.random() * 256).toInt()
            println("Producer writes $value")
            buffer.write(value)
            try {
                Thread.sleep((Math.random() * 10000).toLong())
            } catch (e: InterruptedException) {
                throw RuntimeException(e)
            }
        }
    }
}

private class ConsumerTask : Runnable {
    override fun run() {
        while (true) {

            val value = buffer.read()
            println("\t\t\tConsumer reads $value")

            try {
                Thread.sleep((Math.random() * 10000).toInt().toLong())
            } catch (e: InterruptedException) {
                throw RuntimeException(e)
            }

        }
    }
}

private class Buffer {
    private val queue: Queue<Int> = LinkedList()

    private val lock: Lock = ReentrantLock()

    private val writeCondition: Condition = lock.newCondition()
    private val readCondition: Condition = lock.newCondition()

    private val isEmpty get() = queue.isEmpty()
    private val isFull get() = queue.size == CAPACITY

    fun write(value: Int) {
        lock.lock()

        try {
            while (isFull) {
                println("Wait for reading buffer")
                writeCondition.await()
            }

            queue.offer(value)
            readCondition.signal()
        } catch (e: InterruptedException) {
            e.printStackTrace()
        } finally {
            lock.unlock()
        }
    }


    fun read(): Int {
        var value = 0
        lock.lock()
        try {
            while (isEmpty) {
                println("\t\t\tWait for writing buffer")
                readCondition.await()
            }

            value = queue.remove()!!
            writeCondition.signal()
        } catch (e: InterruptedException) {
            e.printStackTrace()
        } finally {
            lock.unlock()
        }

        return value
    }

    companion object {
        private const val CAPACITY = 1
    }
}
        

محدود کردن دسترسی تعداد نخ (thread) ها به داده با کلاس Semaphore

با استفاده از کلاس Semaphore در جاوا و کاتلین میتونیم تعداد نخ هایی که به داده همزمان دسترسی دارن رو محدود کنیم، برخلاف synchronized و ReentrantLock که فقط به یک نخ دسترسی میدن با Semaphore میتونیم دسترسی به داده رو محدود به یک یا چند نخ کنیم، به عبارتی خودمون تعیین میکنیم که چندتا نخ همزمان میتونه به یک داده دسترسی داشته باشه.

java.util.concurrent.Semaphore
+Semaphore(numberOfPermits) یک نمونه از Semaphore ایجاد می کنه و داخل پارامتر کانستراکتور تعداد نخ هایی که میتونن از داده ها (منابع) استفاده کنن رو تعیین میکنیم.
+Semaphore(numberOfPermits: int, fair: boolean) یک نمونه از Semaphore ایجاد میکنه و تعداد نخ هایی که میتونن از داده (منابع) استفاده کنن رو تعیین میکنیم و همینطور با boolean مشخص میکنیم نوبت دهی منصفانه باشه یعنی نخی که بیشتر از همه منتظر مونده اولویت بالاتری نسبت به بقیه داره برای شروع داره.
+acquire(): void محدودیت استفاده از داده (منابع) رو ایجاد میکنه.
+release(): void محدودیت ایجاد شده رو بر میداره.

برای جلوگیری از race condition میتونیم مثال StudentAccount رو با Semaphore بنویسیم.

private static class Account{
  private int score = 0;

  private final Semaphore semaphore = new Semaphore(1);

  public void updateScore(int v){
      
      try {

        semaphore.acquire();
        int newScore = score + v;
        Thread.sleep(5);
        score = newScore;

      }catch (InterruptedException e){
          throw new RuntimeException(e);
      }finally{
        semaphore.release();
      }
  }
  public int getScore() {
      return score;
  }
}
        
private class Account {
  val semaphore = Semaphore(1)
  var score: Int = 0
      private set

  fun updateScore(v: Int) {
      
      try {
          semaphore.acquire()
          val newScore = score + n
          Thread.sleep(5)
          score = newScore
      } catch (e: InterruptedException) {
          throw RuntimeException(e)
      }finally{
          semaphore.release()
      }
  }
}        
        

مفهوم Deadlock

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

در مثال زیر از sleep داخل عبارت synchronized مربوط به object استفاده کردیم تا Deadlock شکل بگیره.

ExecutorService service = Executors.newFixedThreadPool(2);

Object object = new Object();
Object object1 = new Object();


service.execute(() -> {
  synchronized (object) {

      System.out.println("From object");

      try {
          Thread.sleep(200);
      } catch (InterruptedException e) {

      }

      synchronized (object1) {
          System.out.println("Causes dead lock");
      }
  }
});


service.execute(() -> {

  synchronized (object1) {
      System.out.println("From object1");

      synchronized (object) {
          System.out.println("Causes dead lock");
      }
  }
});

service.shutdown();
        
fun main(){
    val service = Executors.newFixedThreadPool(2)
    
    val any = Any()
    val any1 = Any()


    service.execute(Runnable {
        synchronized(any) {
            println("From object")
            try {
                Thread.sleep(200)
            } catch (e: InterruptedException) {
            }
            synchronized(any1) {
                println("Causes dead lock")
            }
        }
    })


    service.execute(Runnable {
        synchronized(any1) {
            println("From object1")
            synchronized(any) {
                println("Causes dead lock")
            }
        }
    })

    service.shutdown()

}
        

یکی از کار هایی که ممکنه باعث deadlock بشه صدا زدن notify یا notifyAll توسط یک آبجکت دیگه برای ادامه ی اجرای thread یک آبجکت دیگس.

در مثال ConsumerProducer با synchronized میخوایم برای هرکدوم از متد های consume و produce یک کلاس جدا تعریف کنیم.

public class ConsumerProducerDemo {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);

        Producer producer = new Producer();
        Consumer consumer = new Consumer();

        //ConsumerProducer cp = new ConsumerProducer();

            service.execute(() -> {
                while (true)
                    consumer.consume(producer);
            });


            service.execute(() -> {
                while (true)
                    producer.produce();
            });

            service.shutdown();

    }


    private static class Producer{
        private int data ;

        public synchronized void produce(){
            try {
                while (data != 0)
                    wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }

            data = (int) (1 + (Math.random() * 126));
            System.out.println("Producing " + data);
            notifyAll();

        }

        public void resetData(){
            data = 0;
        }
    }

    private static class Consumer{

        public synchronized void consume(Producer producer){
            try {

                while (producer.data == 0) {
                    wait();
                }
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }

            System.out.println("Consuming " + producer.data);
            producer.resetData();
            notifyAll();
        }
    }

}
        
fun main(){

    val service = Executors.newFixedThreadPool(2)

    val producer = Producer()
    val consumer = Consummer()

    service.execute({
        while (true) consumer.consume(producer)
    })
    service.execute({
        while (true) producer.produce()
    })
    service.shutdown()

}

private class Producer {
    private var data = 0

    @Synchronized
    fun produce() {
        try {
            while (data != 0) (this as Object).wait()
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
        data = (1 + (Math.random() * 126)).toInt()
        println("Producing " + data)
        (this as Object).notifyAll()
    }

    fun resetData() {
        data = 0
    }
}

private class Consumer {
    @Synchronized
    fun consume(producer: Producer) {
        try {
            while (producer.data == 0) {
                (this as Object).wait()
            }
            Thread.sleep(1000)
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
        println("Consuming " + producer.data)
        producer.resetData()
        (this as Object).notifyAll()
    }
}
        

برای جلوگیری از deadlock از روش ترتیب منابع (resource ordering) استفاده کنید هر چیزی که قفل روشه رو به ترتیب با نخ (thread) اجرا کنید و از اجرای قفل های تو در تو پرهیز کنید و همینطور موقع صدا زدن wait و notify دقت کنید که این متد ها پس زمینه ی آبجکتی باشن که داریم از متد هاش استفاده می کنیم.

خلاصه

  • زمانی که چندتا نخ به صورت همزمان دارن از یک داده استفاده میکنن، race condition پیش میاد.
  • زمانی که سایر نخ ها بخوان از وضعیت تغییرات در یک داده ی مشترک توسط یک نخ فورا با خبر بشن، داده رو باید با volatile تعریف کنیم.
  • برای جلوگیری از race condition در جاوا و کاتلین میتونیم از synchronized یا کلاس های ReentrantLock و Semaphore استفاده کنیم.
  • زمانی که از کلاس ReentrantLock استفاده میکنیم بجای wait, notify و notifyAll باید از متد های await, signal و signalAll در اینترفیس condition استفاده کنیم.
  • با استفاده از متد newCondition در Lock میتونیم یک نمونه از Condition استفاده کنیم.
  • با استفاده از کلاس Semaphore میتونیم تعداد نخ هایی که میتونن به یک داده دسترسی داشته باشن رو محدود کنیم.

بازخورد و دیدگاه‌ ها

اولین نفری باشید که در این صفحه نظر می‌دهید

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

arrow_drop_up
کپی شد!