Kotlin 协程如何优雅交替打印奇偶数

189 阅读4分钟

在这篇文章中,我们将探索如何使用 Kotlin 协程实现一个有趣的小任务:交替打印奇数和偶数。通过这个例子,我们不仅可以学习协程的基本用法,还能深入理解协程的非阻塞特性。

背景知识

在多线程编程中,常常需要多个线程协同工作。Java 提供了多种并发工具,如 synchronizedReentrantLockCondition,而 Kotlin 则通过协程提供了一种轻量级的并发方式,使得编写并发代码变得更加简单和高效。

任务描述

我们的任务是创建两个线程或协程,一个打印奇数,一个打印偶数,并且它们需要交替打印。例如,输出应该是:1, 2, 3, 4, 5, 6, ...,直到某个最大值。

Java 版本

在进入 Kotlin 版本之前,我们先来看一下如何用 Java 实现这个任务。Java 中,我们可以使用 ReentrantLockCondition 来实现线程之间的同步。

Java 实现

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class OddEvenPrinter {
    private static final int MAX = 1000;
    private int count = 1;
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public static void main(String[] args) {
        OddEvenPrinter printer = new OddEvenPrinter();
        Thread oddThread = new Thread(printer::printOdd);
        Thread evenThread = new Thread(printer::printEven);

        oddThread.start();
        evenThread.start();
    }

    private void printOdd() {
        while (count <= MAX) {
            lock.lock();
            try {
                while (count % 2 == 0) {
                    condition.await();
                }
                if (count <= MAX) {
                    System.out.println("Odd: " + count);
                    count++;
                    condition.signal();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        }
    }

    private void printEven() {
        while (count <= MAX) {
            lock.lock();
            try {
                while (count % 2 != 0) {
                    condition.await();
                }
                if (count <= MAX) {
                    System.out.println("Even: " + count);
                    count++;
                    condition.signal();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        }
    }
}

代码解释

  1. 共享变量count 是共享的计数器,lock 是用于同步的 ReentrantLock 对象,conditionCondition 对象,用于线程间的通信。
  2. 奇数线程printOdd 方法在 count 为奇数时打印,并在打印后递增 count,然后调用 condition.signal() 唤醒等待的线程。
  3. 偶数线程printEven 方法在 count 为偶数时打印,并在打印后递增 count,然后调用 condition.signal() 唤醒等待的线程。

运行结果

运行上述代码,你会看到奇数和偶数交替打印,直到达到最大值:

Odd: 1
Even: 2
Odd: 3
Even: 4
...

Kotlin 版本

现在,我们来看一下如何使用 Kotlin 协程来实现同样的功能。Kotlin 协程提供了一种轻量级的并发方式,使得编写并发代码变得更加简单和高效。在 Kotlin 中,我们需要自己实现一个类似于 Java Condition 的类。这个类需要提供 await 和 signal 方法,用于挂起和恢复协程。

实现思路

java 的await 和 signal 分别对应等待与唤醒,而Kotlin 对应的是挂起和恢复,那问题来了,怎么挂起 又怎么恢复?当然Kotlin 已经给我们提供了工具,挂起我们可以使用

suspendCancellableCoroutine

在这个方法添加一个等待恢复的列表,等待恰当的时机恢复,

private val waiters = mutableListOf<CancellableContinuation<Unit>>()

恰当的时机恢复

waiters.removeAt(0).resume(Unit)

代码实现

class Condition(private val mutex: Mutex) {
    private val waiters = mutableListOf<CancellableContinuation<Unit>>()

    suspend fun await() {
        mutex.unlock()
        try {
            suspendCancellableCoroutine<Unit> { cont ->
                waiters.add(cont)
       
            }
        } finally {
            mutex.lock()
        }
    }

    fun signal() {
        if (waiters.isNotEmpty()) {
            waiters.removeAt(0).resume(Unit)
        }
    }
}

Condition 类中,await 方法挂起调用的协程并将其添加到 waiters 列表中。signal 方法恢复 waiters 列表中的第一个协程。

接下来,我们定义主函数和协程逻辑:

fun main() = runBlocking {
    val mutex = Mutex()
    val condition = Condition(mutex)
    var count = 1
    val max = 1000

    val evenJob = launch {
        while (count <= max) {
            mutex.withLock {
                while (count % 2 != 0) {
                    condition.await()
                }
                if (count <= max) {
                    println("Even: $count")
                    delay(1000)
                    count++
                    condition.signal()
                }
            }
        }
    }

    val oddJob = launch {
        while (count <= max) {
            mutex.withLock {
                while (count % 2 == 0) {
                    condition.await()
                }
                if (count <= max) {
                    println("Odd: $count")
                    delay(1000)
                    count++
                    condition.signal()
                }
            }
        }
    }

    evenJob.join()
    oddJob.join()
    println("main end")
}

在这个代码中,我们使用 runBlocking 启动一个协程作用域,并阻塞主线程直到所有协程完成。mutexMutex 的实例,用于控制对共享变量 count 的访问。condition 是自定义 Condition 类的实例,使用 mutex 进行同步。注意以上是在主线程启动两个协程实现交替打印奇偶数,并没有开启新的线程,这个和Java 是不同的。

运行结果

运行上述代码,你会看到奇数和偶数交替打印,直到达到最大值:

Odd: 1
Even: 2
Odd: 3
Even: 4
...

对比分析

并发模型

  • Java:基于线程的并发模型,每个线程都有自己的栈空间,线程切换开销较大。
  • Kotlin:基于协程的并发模型,协程是轻量级的线程,切换开销小,适合高并发场景。

非阻塞特性

  • Java:线程在等待条件时会阻塞,直到条件满足。这意味着线程会占用系统资源,直到被唤醒。
  • Kotlin:协程在等待条件时会挂起,而不是阻塞。这使得协程可以释放资源,允许其他协程继续执行,从而提高系统的整体效率。