背景
又翻到了这个经典的面试题,虽然搞客户端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
表达了如果true
则lock
,然后通过别的线程notify_one
来重新激活。
mutex
用于保证同时只有一个线程在操作数字。
Golang
Golang
中也有sync.Mutex
和sync.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等机制,让开发者尽量少的避免纠结细节,把精力聚焦到实际业务中。
不过这个需求确实也比较刁钻,强行把一个更适合单线程执行的任务拆分到多线程里,很容易使得初学者对多线程的意义产生误解。
当学习到一定程度之后,再来思考这个问题,反倒是比较能体会到其中的一些深层次的知识。