阅读导航
- 一、问题概述
- 二、解决思路
- 三、代码实现
- 四、代码优化
一、问题概述
面试官:C++多线程了解吗?你给我写一下,起两个线程交替打印0~100的奇偶数。就是有两个线程,一个线程打印奇数另一个打印偶数,它们交替输出,类似这样。
偶线程:0
奇线程:1
偶线程:2
奇线程:3
……
偶线程:98
奇线程:99
偶线程:100
面对突如其来的面试题,确实可能会让人感到手足无措。即便你已经掌握了多线程的相关知识,面试官突然提出一个问题,短时间内想要构思出一个解决方案可能还是有些困难。实际上,这类问题所涉及的知识点通常并不复杂,但如果在准备面试时没有遇到过类似的题目,想要迅速想出解决方案确实需要一定的技巧,而且面试官往往还要求面试者现场手写代码。
二、解决思路
回到题目本身,我们需要处理的是两个线程的协作问题,并且要求它们能够交替打印数字。这涉及到线程间的通信和同步。在这种情况下,我们可以想到的基本策略是使用锁来控制线程的执行顺序。拿到锁的线程可以执行打印操作,然后释放锁,让另一个线程有机会获取锁。这样,两个线程就可以轮流获得锁,实现交替打印的效果。
创建两个线程并不复杂,实现加锁机制也相对简单。关键在于如何确保这两个线程能够公平地轮流获取锁。我们知道,在加锁之后,线程之间会相互竞争以获取锁。C++标准库中的锁默认并不保证公平性(也就是说,不能保证先请求锁的线程一定会先获得锁),这就可能导致一个线程连续打印多次,而另一个线程则长时间无法打印。
为了解决这个问题,我们可以设计一种机制来确保两个线程能够轮流打印。例如,我们可以定义一个全局变量来指示哪个线程应该先打印,然后每个线程在尝试获取锁之前先检查这个全局变量,确保只有当它应该打印时才去竞争锁。这样,我们就可以避免一个线程长时间占用锁,从而实现两个线程的公平交替打印。
三、代码实现
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
int main()
{
// 创建互斥锁用于同步线程
std::mutex mtx;
// 初始化全局变量x为1,代表要打印的第一个数字
int x = 1;
// 创建条件变量用于线程间同步
std::condition_variable cv;
// 标志变量,用于控制哪个线程应该执行
bool flag = false;
// 创建线程t1,负责打印奇数
std::thread t1([&]() {
for (size_t i = 0; i < 50; i++)
{
// 锁定互斥锁
std::unique_lock<std::mutex> lock(mtx);
// 如果flag为true,则等待cv的通知
while (flag)
cv.wait(lock);
// 打印当前线程ID和x的值
std::cout << "奇线程: " << x << std::endl;
// x加1,准备打印下一个数字
++x;
// 将flag设置为true,允许t2执行
flag = true;
// 通知一个等待cv的线程
cv.notify_one();
}
});
// 创建线程t2,负责打印偶数
std::thread t2([&]() {
for (size_t i = 0; i < 50; i++)
{
// 锁定互斥锁
std::unique_lock<std::mutex> lock(mtx);
// 如果flag为false,则等待cv的通知
while(!flag)
cv.wait(lock);
// 打印当前线程ID和x的值
std::cout << "偶线程: " << x << std::endl;
// x加1,准备打印下一个数字
++x;
// 将flag设置为false,允许t1执行
flag = false;
// 通知一个等待cv的线程
cv.notify_one();
}
});
// 等待线程t1和t2完成
t1.join();
t2.join();
// 程序正常退出
return 0;
}
上面的这段代码让两个线程交替打印奇数和偶数。下面是代码实现的核心思路:
-
初始化同步工具:
std::mutex mtx;
:创建一个互斥锁mtx
,用于保护共享资源(在这个例子中是变量x
和flag
)的访问。std::condition_variable cv;
:创建一个条件变量cv
,用于线程间的同步和通信。bool flag = false;
:创建一个标志变量flag
,用于控制线程t1
和t2
的执行顺序。
-
创建线程:
- 使用
std::thread
创建两个线程t1
和t2
,它们将共享相同的函数对象,但执行不同的任务。
- 使用
-
线程t1的逻辑:
t1
负责打印奇数。- 使用
std::unique_lock
锁定互斥锁mtx
,确保对共享资源的安全访问。 - 通过
while (flag)
循环和cv.wait(lock)
调用,t1
在flag
为true
时等待,这是为了让t2
先执行。 - 当
flag
为false
(即t2
执行完毕后),t1
打印当前的x
值,然后将x
加1。 - 将
flag
设置为true
,表示t1
已经执行完毕,现在轮到t2
执行。 - 调用
cv.notify_one()
唤醒等待在cv
上的一个线程,即t2
。
-
线程t2的逻辑:
t2
负责打印偶数。- 类似于
t1
,t2
首先锁定互斥锁mtx
。 - 通过
while(!flag)
循环和cv.wait(lock)
调用,t2
在flag
为false
时等待,这是为了让t1
先执行。 - 当
flag
为true
(即t1
执行完毕后),t2
打印当前的x
值,然后将x
加1。 - 将
flag
设置为false
,表示t2
已经执行完毕,现在轮到t1
执行。 - 调用
cv.notify_one()
唤醒等待在cv
上的一个线程,即t1
。
-
等待线程结束:
- 使用
t1.join()
和t2.join()
确保主线程等待t1
和t2
线程完成执行。
- 使用
-
程序退出:
return 0;
表示程序正常退出。
这种使用互斥锁、条件变量和标志变量的模式是多线程同步中常见的一种方法,它允许多个线程以一种协调的方式交替执行任务。通过这种方式,可以避免竞态条件和数据不一致的问题,确保线程安全。
四、代码优化
代码可以进行一些优化以提高其可读性和效率。
-
使用
std::atomic
:
使用std::atomic<int>
代替int
类型来声明x
,这样可以避免在多线程环境中对x
的访问需要互斥锁的保护。 -
减少锁的范围:
缩小互斥锁的使用范围,只在必要时锁定和解锁,以减少锁的争用。 -
使用
std::chrono
:
使用std::chrono
库中的类型来指定condition_variable
的超时时间,以避免长时间等待。 -
使用
notify_all
代替notify_one
:
如果只有两个线程在等待同一个条件变量,使用notify_all
可以避免唤醒一个线程后再次等待。 -
代码重构:
将线程函数提取为独立的函数,以提高代码的可读性和可维护性。
下面是优化后的代码:
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include <chrono>
std::mutex mtx;
std::condition_variable cv;
std::atomic<int> x(1); // 使用原子操作来保证线程安全
bool flag = false;
void print_numbers(bool is_odd) {
for (size_t i = 0; i < 50; i++) {
std::unique_lock<std::mutex> lock(mtx);
while (flag != is_odd) {
cv.wait(lock, []{ return flag != is_odd; }); // 使用lambda表达式指定唤醒条件
}
std::cout << std::this_thread::get_id() << ":" << x++ << std::endl;
flag = !is_odd; // 切换flag的值
cv.notify_all(); // 唤醒另一个线程
}
}
int main() {
std::thread t1(print_numbers, true);
std::thread t2(print_numbers, false);
t1.join();
t2.join();
return 0;
}
在这个优化版本中:
x
被声明为std::atomic<int>
类型,因此不需要互斥锁来保护x
的增加操作。- 条件变量的等待条件被封装在lambda表达式中,这样可以更清晰地指定唤醒条件。
- 使用
notify_all()
来唤醒所有等待的线程,因为在这个场景中只有两个线程,所以notify_one()
和notify_all()
效果相同,但notify_all()
是一个更通用的选择。 - 将打印逻辑抽象到
print_numbers
函数中,并使用is_odd
参数来区分是打印奇数还是偶数。