面试题-C++多线程打印的问题

最近在参加一些面试,其中遇到一道多线程打印的问题。当时做的不够好,后来回家自己又调了一下。这里把自己做的简单实现记录一下。
话不多说,题目大概是这样的:

有一个包含了字母,数字,特殊字符的字符串(假设为“~82hda@sf908r3jkrew&t92o435$”),要求设计3个线程,A线程打印其中的字母,B线程打印其中的数字,而C线程打印特殊字符,同时要求依次打印出行号,且行号不能乱。比如,
1.线程C打印出 ~
2.线程B打印出 8
3.线程B打印出 2
4.线程A打印出 h

当时我是这样写的

#include <iostream>
#include <thread>
#include <mutex>

std::string str = "~82hda@sf908r3jkrew&t92o435$";
std::mutex mutex_print;

void printLetter()
{
    for(int i=1; i<=str.size(); ++i)
    {
        if ((str[i] >= 'a' && str[i] <= 'z') ||
            (str[i] >= 'A' && str[i] <= 'Z'))
        {
            mutex_print.lock();
            std::cout << i << ".printLetter " << str[i] << std::endl;
            mutex_print.unlock();
        }
    }
}

void printDigit()
{
    for(int i=1; i<=str.size(); ++i)
    {
        if (str[i] >= '0' && str[i] <= '9')
        {
            mutex_print.lock();
            std::cout << i << ".printDigit " << str[i] << std::endl;
            mutex_print.unlock();
        }
    }
}

void printOther()
{
    for(int i=1; i<=str.size(); ++i)
    {
        if ((!(str[i] >= 'a' && str[i] <= 'z')) &&
            (!(str[i] >= 'A' && str[i] <= 'Z')) &&
            (!(str[i] >= '0' && str[i] <= '9')))
        {
            mutex_print.lock();
            std::cout << i << ".printOther " << str[i] << std::endl;
            mutex_print.unlock();
        }
    }
}

int main()
{
    std::thread thread_A(printLetter);
    std::thread thread_B(printDigit);
    std::thread thread_C(printOther);

    thread_A.join();
    thread_B.join();
    thread_C.join();
}

想法很丰满,现实很骨感!以为这样就能对打印顺序和字符做控制,简直就是异想天开。实际编译后运行,在thread_B.join();就抛异常了。
1.程序并不能保证按顺序打印
2.程序并未对临界区资源做真正的互斥操作

下面是我修改后的代码

#include <iostream>
#include <thread>
#include <queue>
#include <mutex>

const std::string str = "~82hda@sf908r3jkrew&t92o435$";
std::mutex mutex_print;

std::queue<char> queue_ch;
int line_num = 0;

void printLetter()
{
    while (!queue_ch.empty())
    {
        auto str_ch = queue_ch.front();
        if ((str_ch >= 'a' && str_ch <= 'z') ||
            (str_ch >= 'A' && str_ch <= 'Z'))
        {
			mutex_print.lock();
            queue_ch.pop();
            std::cout << ++line_num << ".printLetter " << str_ch << std::endl;
			mutex_print.unlock();
        }
    }
}

void printDigit()
{
    while (!queue_ch.empty())
    {
        auto str_ch = queue_ch.front();
        if (str_ch >= '0' && str_ch <= '9')
        {
            mutex_print.lock();
            queue_ch.pop();
            std::cout << ++line_num << ".printDigit " << str_ch << std::endl;
            mutex_print.unlock();
        }
    }
}

void printOther()
{
    while (!queue_ch.empty())
    {
        auto str_ch = queue_ch.front();
        if ((!(str_ch >= 'a' && str_ch <= 'z')) &&
            (!(str_ch >= 'A' && str_ch <= 'Z')) &&
            (!(str_ch >= '0' && str_ch <= '9')))
        {
            mutex_print.lock();
            queue_ch.pop();
            std::cout << ++line_num << ".printOther " << str_ch << std::endl;
            mutex_print.unlock();
        }
    }
}


int main()
{
    for (const auto& item : str)
    {
        queue_ch.push(item);
    }

    std::thread thread_A(printLetter);
    std::thread thread_B(printDigit);
    std::thread thread_C(printOther);

    thread_A.join();
    thread_B.join();
    thread_C.join();
}

这次主要改动在以下方面:
1.引入了queue,将字符串每个字符放到队列容器中,使其成为临界资源
2.每个线程循环判断队列是否为空,不为空时,获取队头字符
3.判断是否是自己这个线程要打印的字符
4.在pop队列队头字符的前后实现加锁和解锁操作
5.同时引入一个全局int型变量来记录行号
这次就实现了题目的功能。有人会说,如果每个线程加锁的地方放在if条件判断之前,是不是可以?答案是否定的!会出现下面的报错
抛异常了
这是什么原因呢?从异常说明可以看出,在线程调用front()的时候,队列已经为空了。可是明明是先判断的队列是否为空,如果已经为空了,while循环就退出了。
这就是多线程导致的问题。因为之前我们是先判断是否是自己线程需要打印的字符后,再加锁;这样就能保证其他两个线程虽然拿到了队首字符,但是不是自己线程要打印的字符,所以不用加锁。这样,它们就会一直循环判断直到真正处理队列的那个线程处理完,队列有了变化后,它们才有进入加锁处理的可能性。而现在的情况是,不管是不是我要打印的字符,我都先加锁。那这样就存在一种可能,虽然我判断队列不为空,但是另一个线程刚好在处理最后一个字符。然后当你调front时,那个线程刚好pop了。就报了异常。

继续思考。有时候我们在加锁后,会忘记释放锁导致抛异常。C++为我们提供了一个lock_grand的模板,它利用了RAII,可以自动释放锁。
于是代码可以变为下面这样

#include <iostream>
#include <thread>
#include <queue>
#include <mutex>

const std::string str = "~82hda@sf908r3jkrew&t92o435$";

std::mutex mutex_print;

std::queue<char> queue_ch;
int line_num = 0;

void printLetter()
{
    while (!queue_ch.empty())
    {
        auto str_ch = queue_ch.front();
        if ((str_ch >= 'a' && str_ch <= 'z') ||
            (str_ch >= 'A' && str_ch <= 'Z'))
        {
            std::lock_guard<std::mutex> lg(mutex_print);
            queue_ch.pop();
            std::cout << ++line_num << ".printLetter " << str_ch << std::endl;
        }
    }
}

void printDigit()
{
    while (!queue_ch.empty())
    {
        auto str_ch = queue_ch.front();
        if (str_ch >= '0' && str_ch <= '9')
        {
            std::lock_guard<std::mutex> lg(mutex_print);
            queue_ch.pop();
            std::cout << ++line_num << ".printDigit " << str_ch << std::endl;
        }
    }
}

void printOther()
{
    while (!queue_ch.empty())
    {
        auto str_ch = queue_ch.front();
        if ((!(str_ch >= 'a' && str_ch <= 'z')) &&
            (!(str_ch >= 'A' && str_ch <= 'Z')) &&
            (!(str_ch >= '0' && str_ch <= '9')))
        {
            std::lock_guard<std::mutex> lg(mutex_print);
            queue_ch.pop();
            std::cout << ++line_num << ".printOther " << str_ch << std::endl;
        }
    }
}


int main()
{
    for (const auto& item : str)
    {
        queue_ch.push(item);
    }

    std::thread thread_A(printLetter);
    std::thread thread_B(printDigit);
    std::thread thread_C(printOther);

    thread_A.join();
    thread_B.join();
    thread_C.join();
}

好了,继续思考!其实std::string本身就是一种特殊的list。那么能不能不要借助queue呢?答案是肯定的。我又接着想了下直接处理string的办法,如下

#include <iostream>
#include <thread>
#include <queue>
#include <mutex>

std::string orgin_str = "~82hda@sf908r3jkrew&t92o435$";
std::string str = std::reverse(orgin_str.begin(),orgin_str.end());
std::mutex mutex_print;

int line_num = 0;

void printLetter1()
{
    while (!str.empty())
    {
        auto str_ch = str.back();
        if ((str_ch >= 'a' && str_ch <= 'z') ||
            (str_ch >= 'A' && str_ch <= 'Z'))
        {
            std::lock_guard<std::mutex> lg(mutex_print);
            str.pop_back();
            std::cout << ++line_num << ".printLetter " << str_ch << std::endl;
        }
    }
}

void printDigit1()
{
    while (!str.empty())
    {
        auto str_ch = str.back();
        if (str_ch >= '0' && str_ch <= '9')
        {
            std::lock_guard<std::mutex> lg(mutex_print);
            str.pop_back();
            std::cout << ++line_num << ".printDigit " << str_ch << std::endl;
        }
    }
}

void printOther1()
{
    while (!str.empty())
    {
        auto str_ch = str.back();
        if ((!(str_ch >= 'a' && str_ch <= 'z')) &&
            (!(str_ch >= 'A' && str_ch <= 'Z')) &&
            (!(str_ch >= '0' && str_ch <= '9')))
        {
            std::lock_guard<std::mutex> lg(mutex_print);
            str.pop_back();
            std::cout << ++line_num << ".printOther " << str_ch << std::endl;
        }
    }
}


int main()
{
    std::thread thread_A(printLetter1);
    std::thread thread_B(printDigit1);
    std::thread thread_C(printOther1);

    thread_A.join();
    thread_B.join();
    thread_C.join();
}

这次的改进是
1.不借助queue了,直接对源字符串处理
2.因为发现string只有pop_back()成员函数,而没有pop_front(),所以先对源字符串反转,然后每次获取back()尾部字符处理
这样也是可以正确打印的

最后说几点
1.我也尝试不反转字符串,也还是每次front()首字符,然后把pop_back的地方,换成str = str.substr(1, str.size()),但是发现,在打印完第一个字符后,线程间死锁了,不再打印了,程序也不崩溃。我实在想不出原因,如果看到文章的大神希望能指点迷津
2.后来我又试了一种办法,就是再换成str.erase(0, 1),这样也能起到类似pop_front的效果。这次没有死锁了,但是程序会随机的抛异常,5次可能正常执行3到4次,抛异常1到2次。又是我想不通的地方,还是请大佬指点迷津
3.多线程的问题,还包括了很多,这里只针对这道面试题展开说说。不足之处,还请指出,欢迎技术交流。谢谢!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值