[Interview must know] Kotlin parallel tasks/thread synchronization several ways to achieve

Insert picture description here

In multi-threaded development, we often encounter this requirement:
Task1, Task2 and other tasks are executed in parallel, and Task3 is executed after all executions are completed.
There are many ways we can achieve this in Kotlin. Here are a list of them:

  1. Thread.join
  2. Synchronized
  3. ReentrantLock
  4. BlockingQueue
  5. CountDownLatch
  6. CyclicBarrier
  7. CASE
  8. Future
  9. CompletableFuture
  10. Rxjava
  11. Coroutine

Tasks Mock


Task3 concatenates strings based on the results returned by Task1 and Task2, and each task simulates time-consuming operations through sleep

val task1: () -> String = {
    
    
    sleep(2000)
    "Hello".also {
    
     println("task1 finished: $it") }
}

val task2: () -> String = {
    
    
    sleep(2000)
    "World".also {
    
     println("task2 finished: $it") }
}

val task3: (String, String) -> String = {
    
     p1, p2 ->
    sleep(2000)
    "$p1 $p2".also {
    
     println("task3 finished: $it") }
}

Thread.join()


Kotlin is compatible with Java, so all Java tools can be used. The simplest way in this case is to use the join method of Thread to achieve synchronization:

@Test
fun test_join() {
    
    
    lateinit var s1: String
    lateinit var s2: String

    val t1 = Thread {
    
     s1 = task1() }
    val t2 = Thread {
    
     s2 = task2() }
    t1.start()
    t2.start()

    t1.join()
    t2.join()
    
    task3(s1, s2)

}

Synchronized


Use synchronizedlocks for synchronization

	@Test
    fun test_synchrnoized() {
    
    
        lateinit var s1: String
        lateinit var s2: String

        Thread {
    
    
            synchronized(Unit) {
    
    
                s1 = task1()
            }
        }.start()
        s2 = task2()

        synchronized(Unit) {
    
    
            task3(s1, s2)
        }

    }

But if there are more than three tasks, using synchrnoized is more awkward. In order to synchronize the results of multiple parallel tasks, you need to declare n locks and nest n synchronized.


ReentrantLock


Lock is a thread lock provided by JUC, which can replace the use of synchronized

	@Test
    fun test_ReentrantLock() {
    
    

        lateinit var s1: String
        lateinit var s2: String

        val lock = ReentrantLock()
        Thread {
    
    
            lock.lock()
            s1 = task1()
            lock.unlock()
        }.start()
        s2 = task2()

        lock.lock()
        task3(s1, s2)
        lock.unlock()

    }

When there are more tasks, there will be no nested synchrnoized problem, but it is still necessary to create multiple locks to manage different tasks.

BlockingQueue


The inside of the blocking queue is also implemented by Lock, so the effect of synchronization lock can also be achieved

	@Test
    fun test_blockingQueue() {
    
    

        lateinit var s1: String
        lateinit var s2: String

        val queue = SynchronousQueue<Unit>()

        Thread {
    
    
            s1 = task1()
            queue.put(Unit)
        }.start()

        s2 = task2()

        queue.take()
        task3(s1, s2)
    }

Blocking queues are more used in the production/consumption task model, in this case just to make up the number


CountDownLatch


Most of the locks in JUC are based on AQSimplementation, including exclusive locks and shared locks. For example, ReentrantLock is an exclusive lock.
In contrast, shared locks are more suitable for this case, such as CountDownLatch, which allows a thread to wait until the operation performed by other threads is completed:

	@Test
    fun test_countdownlatch() {
    
    

        lateinit var s1: String
        lateinit var s2: String
        val cd = CountDownLatch(2)
        Thread() {
    
    
            s1 = task1()
            cd.countDown()
        }.start()

        Thread() {
    
    
            s2 = task2()
            cd.countDown()
        }.start()

        cd.await()
        task3(s1, s2)
    }

The advantage of shared locks is that there is no need to create a separate lock for each task, and it is easy to write even more parallel tasks


CyclicBarrier


CyclicBarrier is another shared lock mechanism provided by JUC. It allows a group of threads to reach a synchronization point and then continue to run together. When any one of the threads does not reach the synchronization point, the other arriving threads will be blocked.
The difference with CountDownLatch is that CountDownLatch is one-time, while CyclicBarrier can be reset and reused, so the name of Cyclic comes from this and can be recycled

	@Test
    fun test_CyclicBarrier() {
    
    

        lateinit var s1: String
        lateinit var s2: String
        val cb = CyclicBarrier(3)

        Thread {
    
    
            s1 = task1()
            cb.await()
        }.start()

        Thread() {
    
    
            s2 = task1()
            cb.await()
        }.start()

        cb.await()
        task3(s1, s2)

    }

CASE


The internal AQS achieves synchronization through a spin lock, which uses CAS to avoid the overhead of thread blocking.
Therefore, we can use CAS-based atomic class counting to ensure thread safety and achieve lock-free operation

 	@Test
    fun test_cas() {
    
    

        lateinit var s1: String
        lateinit var s2: String

        val cas = AtomicInteger(2)

        Thread {
    
    
            s1 = task1()
            cas.getAndDecrement()
        }.start()

        Thread {
    
    
            s2 = task2()
            cas.getAndDecrement()
        }.start()

        while (cas.get() != 0) {
    
    }

        task3(s1, s2)

    }

whileCycling idling may seem wasteful, but the essence of spin locks is this, so it is only used for synchronization of some CPU-intensive short tasks.


volatile


Seeing CAS, in addition, many people may think that using volatile can also be thread-safe to achieve lock-free operation? As follows

 	@Test
    fun test_Volatile() {
    
    
        lateinit var s1: String
        lateinit var s2: String

        Thread {
    
    
            s1 = task1()
            cnt--
        }.start()

        Thread {
    
    
            s2 = task2()
            cnt--
        }.start()

        while (cnt != 0) {
    
    
        }

        task3(s1, s2)

    }

Note that this way of writing is wrong.
Volatile can guarantee visibility, but cannot guarantee atomicity. It cnt--is not thread-safe and requires lock operation.


Future


Regardless of whether there is a lock operation or a lock-free operation above, it is very inconvenient to define two variables s1and s2record the results.
Starting with Java 1.5, Callable and Future are provided, through which the task execution result can be obtained after the task is executed.

	@Test
    fun test_future() {
    
    

        val future1 = FutureTask(Callable(task1))
        val future2 = FutureTask(Callable(task2))

        Executors.newCachedThreadPool().execute(future1)
        Executors.newCachedThreadPool().execute(future2)

        task3(future1.get(), future2.get())

    }

Pass future.get(), you can wait for the result to return synchronously, which is very convenient to write


CompletableFuture


Although future.get() is convenient, it blocks the thread.
In Java 8, the CompletableFuture class is introduced, which implements the Future interface and also implements the CompletionStage interface. CompletableFuture provides a variety of methods that can perform various logical combinations for multiple CompletionStages to achieve complex asynchronous programming. These methods can effectively avoid thread blocking caused by synchronization through callbacks.

   @Test
    fun test_CompletableFuture() {
    
    
        CompletableFuture.supplyAsync(task1)
            .thenCombine(CompletableFuture.supplyAsync(task2)) {
    
     p1, p2 ->
                task3(p1, p2)
            }.join()
    }

RxJava


The various operators and thread switching capabilities provided by RxJava can also help us achieve our needs:
zipoperators can combine the results of two Observables; subscribeOn is used to execute asynchronous threads

@Test
fun test_Rxjava() {
    
    

    Observable.zip(
        Observable.fromCallable(Callable(task1))
            .subscribeOn(Schedulers.newThread()),
        Observable.fromCallable(Callable(task2))
            .subscribeOn(Schedulers.newThread()),
        BiFunction(task3)
    ).test().awaitTerminalEvent()

}

Coroutine


I have talked so much before, but they are all Java tools.
The last Coroutine is finally worthy of the title of this article.

@Test
fun test_coroutine() {
    
    

    runBlocking {
    
    
        val c1 = async(Dispatchers.IO) {
    
    
            task1()
        }

        val c2 = async(Dispatchers.IO) {
    
    
            task2()
        }

        task3(c1.await(), c2.await())
    }
}

It is very comfortable to write, and it can be said that it combines the advantages of the previous tools.


to sum up


This article enumerates all four ways of writing fennel beans, but it does not encourage abuse.
As a conclusion: the most elegant and best parallel task implementation method on Kotlin is the first push coroutine!

Guess you like

Origin blog.csdn.net/vitaviva/article/details/108554692