多线程下交替输出奇偶数的方法

文章字数:2041

背景

又翻到了这个经典的面试题,虽然搞客户端Gameplay导致日常工作里基本上只和主线程打交道,而且这种题目实际应用中也很难遇到,不过作为一个考察的题目也算合适。

另外也可以用这个题目来反映一下为什么说Golang对于多线程(协程)具有很大的优势。

实现

Java

基础思路是通过synchronized加互斥锁,保证同时只有一个线程在处理某一逻辑,这样就比较容易写出来如下逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class AlternatePrint {
    private static final Object lock = new Object();
    private static int currentNumber = 0;
    private static final int MAX_NUMBER = 100;

    public static void main(String[] args) {
        Thread evenThread = new Thread(() -> {
            synchronized (lock) {
                while (currentNumber <= MAX_NUMBER) {
                    if (currentNumber % 2 == 0) {
                        System.out.println("偶数线程: " + currentNumber);
                        currentNumber++;
                        lock.notify(); // 唤醒等待的奇数线程
                    } else {
                        try {
                            lock.wait(); // 释放锁并等待
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            return;
                        }
                    }
                }
            }
        }, "EvenThread");

        Thread oddThread = new Thread(() -> {
            synchronized (lock) {
                while (currentNumber <= MAX_NUMBER) {
                    if (currentNumber % 2 == 1) {
                        System.out.println("奇数线程: " + currentNumber);
                        currentNumber++;
                        lock.notify(); // 唤醒等待的偶数线程
                    } else {
                        try {
                            lock.wait(); // 释放锁并等待
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            return;
                        }
                    }
                }
            }
        }, "OddThread");

        // 启动线程
        evenThread.start();
        oddThread.start();

        // 等待线程结束
        try {
            evenThread.join();
            oddThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("打印完成!");
    }
}

也即让两个线程交替利用lock变量来表达自己的任务处理完了该交由其他线程处理了。

也可以使用可重入锁ReentrantLock,搭配Condition可以指定唤醒某一线程,这样更为灵活一些。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Thread evenThread = new Thread(() -> {
        lock.lock();
        try {
        while (count <= MAX) {
                if (count % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ": " + count++);
                oddCondition.signal(); // 唤醒奇数线程
                } else {
                evenCondition.await(); // 等待
                }
        }
        } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        } finally {
        lock.unlock();
        }
}, "EvenThread");

还有一种思路是使用信号量Semaphore,也即把输出数字作为一种资源,两个检测交替去获取这个资源。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.concurrent.Semaphore;

public class PrintOddEvenWithSemaphore {
    private static Semaphore evenSemaphore = new Semaphore(1); // 偶数线程先运行
    private static Semaphore oddSemaphore = new Semaphore(0);
    private static int count = 0;
    private static final int MAX = 100;

    public static void main(String[] args) {
        Thread evenThread = new Thread(() -> {
            try {
                while (count <= MAX) {
                    evenSemaphore.acquire();
                    if (count <= MAX) {
                        System.out.println(Thread.currentThread().getName() + ": " + count++);
                    }
                    oddSemaphore.release();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "EvenThread");

        Thread oddThread = new Thread(() -> {
            try {
                while (count <= MAX) {
                    oddSemaphore.acquire();
                    if (count <= MAX) {
                        System.out.println(Thread.currentThread().getName() + ": " + count++);
                    }
                    evenSemaphore.release();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "OddThread");

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

通过new Semaphore(1)的声明,让同时只有一个线程去执行,执行完就把资源耗完然后等另一个线程release释放。

C++

对于C++来说,常用std::mutex来表示一个互斥锁,可以有以下逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

class AlternatePrint {
private:
    std::mutex mtx;
    std::condition_variable cv;
    int currentNumber;
    static const int MAX_NUMBER = 100;

public:
    AlternatePrint() : currentNumber(0) {}

    void printEven() {
        std::unique_lock<std::mutex> lock(mtx);
        while (currentNumber <= MAX_NUMBER) {
            // 等待直到 currentNumber 是偶数
            cv.wait(lock, [this] { return (currentNumber % 2 == 0) || (currentNumber > MAX_NUMBER); });
            
            // 检查是否已经完成
            if (currentNumber > MAX_NUMBER) {
                break;
            }
            
            // 打印偶数
            std::cout << "偶数线程: " << currentNumber << std::endl;
            currentNumber++;
            
            // 通知另一个线程
            cv.notify_one();
        }
    }

    void printOdd() {
        std::unique_lock<std::mutex> lock(mtx);
        while (currentNumber <= MAX_NUMBER) {
            // 等待直到 currentNumber 是奇数
            cv.wait(lock, [this] { return (currentNumber % 2 == 1) || (currentNumber > MAX_NUMBER); });
            
            // 检查是否已经完成
            if (currentNumber > MAX_NUMBER) {
                break;
            }
            
            // 打印奇数
            std::cout << "奇数线程: " << currentNumber << std::endl;
            currentNumber++;
            
            // 通知另一个线程
            cv.notify_one();
        }
    }

    void run() {
        std::thread evenThread(&AlternatePrint::printEven, this);
        std::thread oddThread(&AlternatePrint::printOdd, this);

        // 等待线程结束
        evenThread.join();
        oddThread.join();

        std::cout << "打印完成!" << std::endl;
    }
};

int main() {
    AlternatePrint printer;
    printer.run();
    return 0;
}

使用condition_variable表达了如果truelock,然后通过别的线程notify_one来重新激活。

mutex用于保证同时只有一个线程在操作数字。

Golang

Golang中也有sync.Mutexsync.Cond,所以可以轻松的实现上述的算法。不过对于Golang来说,可以利用Channel机制更便捷的实现这个需求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
    "fmt"
)

func main() {
    const MAX_NUMBER = 100
    
    // 创建两个 channel 用于协调
    evenChan := make(chan bool)
    oddChan := make(chan bool)
    
    // 启动偶数 goroutine
    go func() {
        for i := 0; i <= MAX_NUMBER; i += 2 {
            // 等待接收信号(除了第一次)
            if i > 0 {
                <-oddChan
            }
            fmt.Printf("偶数: %d\n", i)
            // 通知奇数 goroutine
            evenChan <- true
        }
        // 关闭 channel 表示完成
        close(evenChan)
    }()
    
    // 启动奇数 goroutine
    go func() {
        for i := 1; i <= MAX_NUMBER; i += 2 {
            // 等待接收信号
            <-evenChan
            fmt.Printf("奇数: %d\n", i)
            // 通知偶数 goroutine
            oddChan <- true
        }
        close(oddChan)
    }()
    
    // 等待所有输出完成(这里简单等待,实际可通过 sync.WaitGroup 更精确)
    // 由于奇数最大为 99,偶数会打印到 100,所以等待偶数 goroutine 结束
    <-evenChan // 这会阻塞直到 evenChan 被关闭
    
    fmt.Println("打印完成!")
}

思路上,通过chan,可以更直观的表达通知另一个线程执行某个逻辑,可以把数据“发送”到目标,通过语法也更为简洁。

总结

可以看出,Golang事实上改变了一些常规多线程编程的思维,通过goroutine和channel等机制,让开发者尽量少的避免纠结细节,把精力聚焦到实际业务中。

不过这个需求确实也比较刁钻,强行把一个更适合单线程执行的任务拆分到多线程里,很容易使得初学者对多线程的意义产生误解。

当学习到一定程度之后,再来思考这个问题,反倒是比较能体会到其中的一些深层次的知识。

该内容采用 CC BY-NC-SA 4.0许可协议。

如果对您有帮助或存在意见建议,欢迎在下方评论交流。

加载中...