چندنخی (multithreading) در جاوا و کاتلین

چندنخی (multithreading) در جاوا و کاتلین

معرفی نخ (Thread)

چندنخی به برنامه این امکان رو میده که چند کار رو به صورت همزمان انجام بده.

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

حتی زمانی که پردازنده فقط یک هسته و یک نخ فیزیکی داشته باشه، باز فرایند چند نخی میتونه اجرا بشه، سیستم عامل مدیریت منابع رو در دست میگیره و از فضای خالی پردازنده برای اجرای چندین نخ استفاده میکنه که اصطلاحا بهش تسهیم زمانی (time slicing) یا به اشتراک گذاری زمان (time sharing) میگیم، این فرایند همزمان نیست اما بخاطر سرعت بالا، همزمان به نظر میرسه؛ در مورد این موضوع به طور مفصل در قسمت نخ ها در برنامه نویسی صحبت شد.

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

اینترفیس Runnable

برای اجرای هر کاری توسط یک نخ ابتدا باید اون کار رو ایجاد کنیم، برای ایجاد یک کار از اینترفیس Runnable در جاوا و کاتلین استفاده می کنیم؛اینترفیس Runnable دارای یک متد (تابع) به نام run() است و اجرای کد ها داخل این متد اتفاق می افته، متد run مقداری رو بر نمیگردونه و تنها متد (تابع) اینترفیس Runnable است.

<<java.lang.Runnable>>
+run(): void کد ها داخل این متد اجرا میشن.

میخوایم یک کار تعریف کنیم که اعداد صفر تا n رو نمایش بده.

class PrintNumbersTask implements Runnable{

  private int n;

  public PrintNumbersTask(int n){
    this.n = n;
  }

  @Override
  public void run(){
        
    for(int i=0; i<= n; i++){
      
      if(i % 10 == 0) System.out.println();

      System.out.print(i + " ");
    }
    
  }

}
        
class PrintNumbersTask(private val n: Int): Runnable{
    
    override fun run() {
        for (i in 0 until n){
            if (i % 10 == 0) println()
            print("$i ")
        }
    }
}
        

میخوایم یک کار تعریف کنیم که یک کاراکتر رو n بار نمایش بده.

public class PrintCharTask implements Runnable{

   private int numberOfChars;
   private char ch;

   public PrintCharTask(char ch, int numberOfChars){
        this.numberOfChars = numberOfChars;
        this.ch = ch;
   }

   @Override
   public void run(){

     for(int i=0; i<numberOfChars; i++){

        if(i % 10 == 0) System.out.println();

        System.out.print(ch);
     }

   }

}
        
class PrintCharTask(private val ch: Char, private val n: Int): Runnable {
    override fun run() {
        for (i in 0 until n){
            if (i % 10 == 0) println()
            print("$ch ")
        }
    }
}
        

کلاس Thread

تمام نخ (Thread) هایی که در جاوا ایجاد می کنیم، مستقیم یا غیر مستقیم روی نخ (Thread) اصلی (main thread) شروع به کار میکنن.

برای اجرای کار های تعریف شده با Runnable، از کلاس Thread در جاوا و کاتلین استفاده می کنیم.

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

در زیر کلاس Thread با متد (تابع) های پرکاربرد و کانستراکتور هاش شرح داده شده است.

java.lang.Thread
Thread(runnable: Runnable) یک نمونه از thread ایجاد می کنه و پیاده سازی Runnable رو به عنوان پارامتر بهش پاس میدیم.
Thread() یک نمونه از Thread ایجاد می کنه.
+start():void Thread رو شروع می کنه.
+isAlive():boolean بررسی میکنه ببینه آیا Thread در حال اجراست یا کارش تموم شده.
+setPeriority(periority: int): void برای شروع Thread نسبت به بقیه thread ها اولویت قرار میده، یک عدد بین 1 تا 10 رو به عنوان پارامتر میتونیم بهش پاس بدیم و هرچی این عدد کمتر باشه Thread زودتر از بقیه ی Thread ها اجرا میشه.
+join(): void نخ های دیگه رو متوقف میکنه تا کار نخی که باهاش صداش زدیم تموم بشه؛ این متد رو باید داخل نخی که میخوایم به طور موقت متوقف بشه صدا بزنیم.
+interrupt(): void اجرای نخ رو قطع می کنه.
+sleep(millis: long): void کلاس Thread رو برای مدت زمان مشخص متوقف میکنه. (این متد استاتیک است)

مثال

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

Thread thread0 = new Thread(new PrintNumbersTask(200));
Thread thread1 = new Thread(new PrintCharTask('a', 100));

thread0.start();
thread1.start();
        
val thread0 = Thread(PrintNumbersTask(200))
val thread1 = ThreaD(PrintCharTask('a', 100)
        

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

public class PrintNumbersThread extends Thread{
    
    private int n;

    public PrintNumbersThread(int n){
       this.n = n;
    }

    @Override
    public void run() {
        for (int i = 0; i < n; i++) {

            if (i % 10 == 0) System.out.println();

            System.out.print(i + " ");
        }
    }
}

public class PrintCharThread extends Thread {

    private int n;
    private char ch;

    public PrintCharThread(char ch, int n){
       this.n = n;
       this.ch = ch;
    }

    public PrintCharThread()
    
    @Override
    public void run() {
        for (int i=0; i<n; i++){
            
            if(i % 10 == 0) System.out.println();

            System.out.print(ch + "");
        }
    }
    
}
        


class PrintNumbersThread(private val n: Int): Thread(){
    override fun run() {
       for (i in 0 until n){
           if (i % 10 == 0) println()

           print("$i ")
       }
    }
}


class PrintCharThread(private val ch: Char, private val n: Int): Thread() {

    override fun run() {
        for (i in 0 until n){
            if (i % 10 == 0) println()

            print("$ch ")
        }
    }
}
        

متد (تابع) join

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

میخوایم در کار PrintNumbersTask یک Thread از کار PrintRandomCharTask ایجاد کنیم و متد join رو صدا بزنیم؛ بنابراین کد PrintNumbersTask رو به صورت زیر تغییر میدیم.

private static class PrintNumbersTask implements Runnable {

    private int n;
    
    public PrintNumbersTask(int n){
       this.n = n;
    }

    @Override
    public void run() {

        Thread printCharThread = new Thread(new PrintCharTask('a', 1000));
        printCharThread.start();

        try {

            for (int i = 0; i <n; i++) {

                if (i == 50) 
                    printCharThread.join();
                
                System.out.print(i + " ");

            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
        
private class PrintNumbersTask(private val n: Int) : Runnable {
    override fun run() {
        val printCharThread = Thread(PrintCharTask('a', 1000))
        printCharThread.start()

        try {

            for (i in 0..<n) {
                
                if (i == 50) printCharThread.join()

                if(i % 10 == 0) println()

                print("$i ")
            }

        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
    }
        

سپس یک نخ برای اجرای کار PrintNumbersTask ایجاد میکنیم، زمانی که کار PrintNumbersTask شروع کنه به اجرا شدن کار PrintRandomCharsTask هم شروع به اجرا شدن میکنه چون داخل PrintNumbersTask با یک Thread اجراش کردیم.

public class TestJoin {

    public static void main(String[] args) {
        Thread t = new Thread(new PrintNumbersTask(1000));
        t.start();
    }

    private static class PrintCharTask implements Runnable{
        private int numberOfChars;
        private char ch;

        public PrintCharTask(char ch, int numberOfChars){
            this.numberOfChars = numberOfChars;
            this.ch = ch;
        }
        @Override
        public void run(){
            for(int i=0; i<numberOfChars; i++){
                if(i % 10 == 0) System.out.println();
                System.out.print(ch);
            }
        }
    }
    private static class PrintNumbersTask implements Runnable {
        private int n;
        
        public PrintNumbersTask(int n){
            this.n = n;
        }
        
        @Override
        public void run() {
            Thread printCharThread = new Thread(new PrintCharTask('a', 1000));
            printCharThread.start();
            try {
                for (int i = 0; i <n; i++) {
                    
                    if (i == 50)
                        printCharThread.join();
                    
                    if(i % 10 == 0) System.out.println();

                    System.out.print(i + " ");

                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
        
fun main() {
    val t = Thread(PrintNumbersTask(1000))
    t.start()
}

private class PrintCharTask(private val ch: Char, private val numberOfChars: Int) : Runnable {
    override fun run() {
        for (i in 0..<numberOfChars) {
            if (i % 10 == 0) println()

            print(ch)
        }
    }
}

private class PrintNumbersTask(private val n: Int) : Runnable {
    override fun run() {
        val printCharThread = Thread(PrintCharTask('a', 1000))
        printCharThread.start()

        try {

            for (i in 0..<n) {
                
                if (i == 50) printCharThread.join()

                if(i % 10 == 0) println()

                print("$i ")
            }

        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
    }
}
        

ابتدا دو کار نمایش کاراکتر های تصادفی و چاپ اعداد تقریبا همزمان اجرا میشن (کار نمایش کاراکتر های تصادفی کمی زودتر)، سپس زمانی که چاپ اعداد به 50 میرسه متوقف شده، تا کار نمایش کاراکتر ها تموم بشه و بعد از تموم شدنش کار چاپ اعداد ادامه پیدا می کنه.

متد (تابع) sleep در Thread

با استفاده از متد (تابع) sleep میتونیم یک نخ (Thread) رو تا یک بازه ی زمانی مشخصی متوقف کنیم،، زمانی که داخل یک کار صدا زده بشه، Thread اجرا کننده ی کار رو متوقف میکنه و Thread هایی که داخل کار اجرا شدن هم به طور موقت متوقف میشن؛ متد sleep استاتیکه

در مثال قبل بجای join از Thread.sleep(millis) استفاده می کنیم.

public class TestSleep {
public static void main(String[] args) {
        Thread t = new Thread(new SecondCounter());
        t.start();

        System.out.println("Hello from main thread");
    }

    private static class SecondCounter implements Runnable {

        @Override
        public void run() {

            System.out.println("20 seconds to end of the loop");
            try {

                for (int i = 1; i <= 20; i++) {
                    Thread.sleep(1000);
                    System.out.print(i + " ");
                }

                Thread t = new Thread(() -> System.out.println("\nEnd of the loop reached"));
                t.start();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
}
          
@Throws(InterruptedException::class)
fun main() {
    val t = Thread(PrintNumbersTask(1000))
    t.start()

    println("Hello from main thread")
}

private class PrintNumbersTask : Runnable {
    override fun run() {
        

        try {
            for (i in 0..<20) {
                Thread.sleep(1000)
                print("$i ")
            }

            val thread = Thread{
              println("\nEnd of the loop reached")
            }

        } catch (e: InterruptedException) {
            e.printStackTrace()
        }    }
}
          

مفهوم Thread Pool

زمانی که بخوایم یکی دو نخ ایجاد کنیم، کلاس Thread کاملا مناسبه ولی اگه بخوایم برای چند کار تعداد زیادی نخ ایجاد کنیم، ایجاد یک Thread Pool بهینه تره.

برای ایجاد Thread Pool در جاوا یک اینترفیس به نام ExecutorService وجود داره، میتونیم پیاده سازی های مختلف این اینترفیس رو با کلاس Executors به صورت استاتیک صدا بزنیم.

java.util.concurrent.ExecutorService
+shutdown(): void اجرای کار ها توسط ExecutorService رو متوقف میکنه ولی به کار هایی که هم اکنون در حال اجرا شدن هستن اجازه میده تا فعالیتشون تموم بشه.
+shutdownNow(): List<Runnable> اجرای تمام کار ها توسط ExecutorService رو فورا متوقف میکنه و در آخر یک لیست از کار های اجرا نشده بر میگردونه.
+isShutdown(): boolean اگه ExecutorService متوقف شده باشه مقدار true رو بر میگردونه.
+isTerminated(): boolean اگه همه ی کارها متوقف شده باشن یا به اتمام رسیده باشن مقدار true رو بر میگردونه.
+close(): void بعد از اجرای کار های اضافه شده با صدا زدن close،‌ کلاس ExecutorService بسته میشه و اگه بعد از close کاری رو بهش اضافه کنیم دچار RejectedExecutionException میشیم. (متد close از jdk 19 به بعد در دسترس است)

پیاده سازی های مختلفی از ExecutorService در کلاس Executors وجود داره که بر اساس نیاز میتونیم از هرکدوم که خواستیم استفاده کنیم.

java.util.concurrent.Executors
+newFixedThreadPool(numberOfThreads: int): ExecutorService یک ExecutorService ایجاد میکنه با تعداد مشخصی نخ (Thread) زمانی که یک نخ (Thread) کارش تموم میشه، میتونه کار جدید رو شروع کنه.
+newCachedThreadPool(): ExecutorService یک ExecutorService ایجاد میکنه و به اندازه ی نیاز برنامه نخ (Thread) ایجاد میکنه، زمانی که یک نخ (Thread) کارش تموم بشه کار جدید رو میتونه با نخی که خالی شده شروع کنه.
+newSingleThreadPool(): ExecutorService یک نمونه از ExecutorService ایجاد میکنه با فقط یک نخ، زمانی که کار نخ تموم بشه، کار جدید رو همون نخ شروع میکنه.

در بخش اینترفیس Runnable در جاوا دو کار PrintNumbersTask و PrintCharTask رو پیاده کردیم، حالا میخوایم این دو کار رو با ExecutorService اجرا کنیم.

در مثال زیر بعد از تموم شدن یکی از کارها، کار مربوط به نمایش حرف d شروع به اجرا شدن میکنه.

public class ThreadPool {

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

        service.execute(new PrintNumbers(1000));
        service.execute(new PrintChar('a',400));
        service.execute(new PrintChar('b', 300));
        service.execute(new PrintChar('c', 200));
        service.execute(new PrintChar('d', 100));

        service.close();

        //RejectedExecutionException
        service.execute(new PrintChar('e', 100));

    }


    private static class PrintChar implements Runnable{
        private final int n;
        private final char ch;

        PrintChar(char ch, int n){
            this.n = n;
            this.ch = ch;
        }

        @Override
        public void run() {
            for (int i=0; i<n; i++) {
                if (i % 10 == 0) System.out.println();
                System.out.print(ch + "");
            }
        }
    }

    private static class PrintNumbers implements Runnable{
        private final int n;

        PrintNumbers(int n){
            this.n = n;
        }

        @Override
        public void run() {
            for (int i=0; i<n; i++){
                if (i % 10 == 0) System.out.println();
                System.out.print(i);
            }
        }
    }
}
        
@Throws(InterruptedException::class)
fun main() {
    val service = Executors.newFixedThreadPool(3)

    service.execute(PrintNumbers(1000))
    service.execute(PrintChar('a', 400))
    service.execute(PrintChar('b', 300))
    service.execute(PrintChar('c', 200))
    service.execute(PrintChar('d', 100))

    service.close()

    //RejectedExecutionException
    service.execute(PrintChar('e', 100))
}


private class PrintChar(private val ch: Char, private val n: Int) : Runnable {
    override fun run() {
        for (i in 0..<n) {
            if (i % 10 == 0) println()
            print(ch.toString() + "")
        }
    }
}

private class PrintNumbers(private val n: Int) : Runnable {
    override fun run() {
        for (i in 0..<n) {
            if (i % 10 == 0) println()
            print(i)
        }
    }
}
        

خلاصه

  • از نخ ها برای انجام چند کار به صورت همزمان در برنامه استفاده می کنیم.
  • برای ایجاد کار در جاوا از اینترفیس Runnable استفاده میکنیم.
  • برای اجرای کار ها در جاوا از کلاس Thread استفاده می کنیم.
  • زمانی که بخوایم تعداد زیادی کار رو اجرا کنیم بجای کلاس Thread با یکی از پیاده سازی های ExecutorService یک Thread Pool ایجاد میکنیم.
  • زمانی که چند نخ به صورت همزمان بخوان یک داده رو تغییر بدن دچار race condition میشیم.
  • از کلیدواژه ی synchronized در جاوا برای جلوگیری از race condition استفاده می کنیم.

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

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

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

arrow_drop_up
کپی شد!