在这篇文章中,我们将探索如何使用 Kotlin 协程实现一个有趣的小任务:交替打印奇数和偶数。通过这个例子,我们不仅可以学习协程的基本用法,还能深入理解协程的非阻塞特性。
背景知识
在多线程编程中,常常需要多个线程协同工作。Java 提供了多种并发工具,如 synchronized
、ReentrantLock
和 Condition
,而 Kotlin 则通过协程提供了一种轻量级的并发方式,使得编写并发代码变得更加简单和高效。
任务描述
我们的任务是创建两个线程或协程,一个打印奇数,一个打印偶数,并且它们需要交替打印。例如,输出应该是:1, 2, 3, 4, 5, 6, ...,直到某个最大值。
Java 版本
在进入 Kotlin 版本之前,我们先来看一下如何用 Java 实现这个任务。Java 中,我们可以使用 ReentrantLock
和 Condition
来实现线程之间的同步。
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();
}
}
}
}
代码解释
- 共享变量:
count
是共享的计数器,lock
是用于同步的ReentrantLock
对象,condition
是Condition
对象,用于线程间的通信。 - 奇数线程:
printOdd
方法在count
为奇数时打印,并在打印后递增count
,然后调用condition.signal()
唤醒等待的线程。 - 偶数线程:
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
启动一个协程作用域,并阻塞主线程直到所有协程完成。mutex
是 Mutex
的实例,用于控制对共享变量 count
的访问。condition
是自定义 Condition
类的实例,使用 mutex
进行同步。注意以上是在主线程启动两个协程实现交替打印奇偶数,并没有开启新的线程,这个和Java 是不同的。
运行结果
运行上述代码,你会看到奇数和偶数交替打印,直到达到最大值:
Odd: 1
Even: 2
Odd: 3
Even: 4
...
对比分析
并发模型
- Java:基于线程的并发模型,每个线程都有自己的栈空间,线程切换开销较大。
- Kotlin:基于协程的并发模型,协程是轻量级的线程,切换开销小,适合高并发场景。
非阻塞特性
- Java:线程在等待条件时会阻塞,直到条件满足。这意味着线程会占用系统资源,直到被唤醒。
- Kotlin:协程在等待条件时会挂起,而不是阻塞。这使得协程可以释放资源,允许其他协程继续执行,从而提高系统的整体效率。