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


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

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

    当时我是这样写的

    #include 
    #include 
    #include 
    
    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();
    }
    
    • 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

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

    下面是我修改后的代码

    #include 
    #include 
    #include 
    #include 
    
    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
    • 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
    • 73
    • 74
    • 75
    • 76

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

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

    #include 
    #include 
    #include 
    #include 
    
    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();
    }
    
    
    • 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
    • 73
    • 74

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

    #include 
    #include 
    #include 
    #include 
    
    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
    • 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

    这次的改进是
    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.多线程的问题,还包括了很多,这里只针对这道面试题展开说说。不足之处,还请指出,欢迎技术交流。谢谢!

  • 相关阅读:
    git和github的使用
    【深度学习相关知识】
    美团后端开发一面(40min)
    Nginx负载均衡和备份和故障转移
    【Rust 笔记】15-字符串与文本(下)
    Python字符串-切片、查找、修改、判断
    GBase 8c 函数/存储过程参数(一)
    使用CSS渲染不同形状
    金融壹账通拟7月4日香港上市:2年亏近30亿 市值蒸发超90%
    ubuntu安装最新版本的go基于官网二进制
  • 原文地址:https://blog.csdn.net/HyaZz_/article/details/126503687