在这里插入图片描述

一、问题概述

面试官: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;
}

在这里插入图片描述

上面的这段代码让两个线程交替打印奇数和偶数。下面是代码实现的核心思路:

  1. 初始化同步工具

    • std::mutex mtx;:创建一个互斥锁mtx,用于保护共享资源(在这个例子中是变量xflag)的访问。
    • std::condition_variable cv;:创建一个条件变量cv,用于线程间的同步和通信。
    • bool flag = false;:创建一个标志变量flag,用于控制线程t1t2的执行顺序。
  2. 创建线程

    • 使用std::thread创建两个线程t1t2,它们将共享相同的函数对象,但执行不同的任务。
  3. 线程t1的逻辑

    • t1负责打印奇数。
    • 使用std::unique_lock锁定互斥锁mtx,确保对共享资源的安全访问。
    • 通过while (flag)循环和cv.wait(lock)调用,t1flagtrue时等待,这是为了让t2先执行。
    • flagfalse(即t2执行完毕后),t1打印当前的x值,然后将x加1。
    • flag设置为true,表示t1已经执行完毕,现在轮到t2执行。
    • 调用cv.notify_one()唤醒等待在cv上的一个线程,即t2
  4. 线程t2的逻辑

    • t2负责打印偶数。
    • 类似于t1t2首先锁定互斥锁mtx
    • 通过while(!flag)循环和cv.wait(lock)调用,t2flagfalse时等待,这是为了让t1先执行。
    • flagtrue(即t1执行完毕后),t2打印当前的x值,然后将x加1。
    • flag设置为false,表示t2已经执行完毕,现在轮到t1执行。
    • 调用cv.notify_one()唤醒等待在cv上的一个线程,即t1
  5. 等待线程结束

    • 使用t1.join()t2.join()确保主线程等待t1t2线程完成执行。
  6. 程序退出

    • return 0; 表示程序正常退出。

这种使用互斥锁、条件变量和标志变量的模式是多线程同步中常见的一种方法,它允许多个线程以一种协调的方式交替执行任务。通过这种方式,可以避免竞态条件和数据不一致的问题,确保线程安全。

四、代码优化

代码可以进行一些优化以提高其可读性和效率。

  1. 使用std::atomic
    使用std::atomic<int>代替int类型来声明x,这样可以避免在多线程环境中对x的访问需要互斥锁的保护。

  2. 减少锁的范围
    缩小互斥锁的使用范围,只在必要时锁定和解锁,以减少锁的争用。

  3. 使用std::chrono
    使用std::chrono库中的类型来指定condition_variable的超时时间,以避免长时间等待。

  4. 使用notify_all代替notify_one
    如果只有两个线程在等待同一个条件变量,使用notify_all可以避免唤醒一个线程后再次等待。

  5. 代码重构
    将线程函数提取为独立的函数,以提高代码的可读性和可维护性。

下面是优化后的代码:

#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参数来区分是打印奇数还是偶数。

在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐