多线程未按预期运行

Posted

技术标签:

【中文标题】多线程未按预期运行【英文标题】:Multithreads not behaving as expected 【发布时间】:2020-10-27 14:00:01 【问题描述】:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;


std::mutex g_m;
std::string messageGlobal = "";

void threadFunc() // run in the log thread

    while (1)
    
        g_m.lock();
        if (messageGlobal != "")
        
            // logging takes a long time
            sleep(10000)
            cout << messageGlobal << endl;
            messageGlobal = "";
        
        g_m.unlock();
    



// logging api
void log(const string& message)

   g_m.lock();
   messageGlobal = message;
   g_m.unlock();


int main()

    std::thread th(threadFunc);
    
    log("Hello world!");
    log("Hello World2!");
    log("Hello World3!");
    log("Hello World4!");

    // Important work

    th.join();
    return 0;


这里是线程新手,我不明白为什么只打印最后一条消息。

这里的两个线程是主线程和一个额外的线程,它永久运行并在有消息要打印时输出到屏幕。

如果有人告诉我我哪里出错了,我将不胜感激。

编辑:目标是在非常长的日志记录功能发生时执行“重要代码”中的代码。

【问题讨论】:

有几种合法的可能输出,没有任何地方可以优先选择其中一种。在您的情况下,主线程看起来足够快,以至于unlock() 和下一个lock() 之间的时间太短,以至于其他线程没有时间唤醒并显示消息。 启动线程需要时间。这意味着log 甚至可以在th 开始执行之前完成threadFunc 在 MT 编程中您需要意识到的一件事——您看到的顺序并不是可能发生的事情。仅仅因为您看到 std::thread 在 main() 中首先出现并不意味着线程函数将首先运行。这也是 MT 编程不是一个小话题的原因之一。 非常感谢,在thread start语句后面加上sleep语句,保证了主线程去log函数之前线程已经启动运行。 sleep 没有任何问题,如果您打算在这段时间内实际睡眠,即不必尝试让 MT 程序正常工作。如果使用sleep 的目的是让线程同步,那么是的,代码是可疑的。 【参考方案1】:

正如其他人建议的那样,您最好使用队列来保存消息并在线程之间同步消息队列的访问。但是,这里是您的代码的简单修复:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;


std::mutex g_m;
std::string messageGlobal = "";
bool g_all_done = false;

void threadFunc() // run in the log thread

    while (1)
    
        g_m.lock();
        if (messageGlobal != "")
        
            cout << messageGlobal << endl;
            messageGlobal = "";
        
        bool all_done = g_all_done;
        g_m.unlock();
        if (all_done) break;
    



// logging api
void log(const string& message)

  bool logged = false;
  do 
    g_m.lock();
    if (messageGlobal == "") 
      messageGlobal = message;
      logged = true;
    
    g_m.unlock();
   while(!logged);


void all_done() 
  g_m.lock();
  g_all_done = true;
  g_m.unlock();


int main()

    std::thread th(threadFunc);
    
    log("Hello world!");
    log("Hello World2!");
    log("Hello World3!");
    log("Hello World4!");

    all_done();  // this tells the print thread to finish.

    th.join();
    return 0;

【讨论】:

bool g_all_done = false; -- 这可能在 10 年前与编译器一起工作,但在当今处理器和编译器优化器的世界中,用于控制线程的常规 bool 变量并不安全。 这就是我在锁的范围内访问g_all_done的原因。【参考方案2】:

如果有人告诉我我哪里出错了,我将不胜感激。

假设线程会按顺序锁定互斥锁,这是错误的,这是无法保证的。那么,同一个线程(主线程)多次锁定互斥锁并多次修改消息而第二个线程只有机会打印最后一条消息会发生什么。为了让它工作,你应该让主线程等到消息被清空,然后才再次发布,但很可能你应该使用条件变量来做到这一点,否则你会在编写的代码中钉住 CPU 执行此操作。甚至更好地创建一个日志消息队列,并且仅在队列已满时等待。

请注意,您缺少日志线程完成的条件,因此th.join(); 会挂起。

这是一个关于如何处理单个消息的示例:

std::mutex g_m;
std::condition_variable g_notifyLog;
std::condition_variable g_notifyMain;
bool g_done = false;
std::string messageGlobal = "";

void threadFunc() // run in the log thread

    while (1)
    
        std::lock_guard<std::mutex> lk( g_m );
        g_notifyLog.wait( g_m, []()  return !messageGlobal.empty() || g_done;  );
        if( g_done ) break;
        cout << messageGlobal << endl;
        messageGlobal = "";
        g_notifyMain.notify_one();
    



// logging api
void log(const string& message)

   std::lock_guard<std::mutex> lk( g_m );
   g_notifyMain.wait( g_m, []()  return messageGlobal.empty();  );
   messageGlobal = message;
   g_notifyLog.notify_one();


void stop_log()

   std::lock_guard<std::mutex> lk( g_m );
   g_done = true;
   g_notifyLog.notify_one();

【讨论】:

【参考方案3】:

您没有实现任何机制来确保线程交错运行。解锁互斥锁的线程更有可能在下一刻锁定它,因为锁定互斥锁/解锁互斥锁是快速操作,除非触发睡眠/等待。

此外,ThreadFunc 是一个无限循环。所以理论上程序可能只是重复运行循环而不会触发log的任何执行。

当数据可用于记录时,您需要使用std::condition_variable 在线程之间发出信号,并重写log 方法,这样它就不会覆盖现有的要打印的数据。

【讨论】:

以上是关于多线程未按预期运行的主要内容,如果未能解决你的问题,请参考以下文章

Linux 多线程 - 线程不会按预期产生任何输出

Java线程和多线程(十五)——线程的活性

多线程2

多线程之线程安全

运行多线程JMeter websocket插件时消息未分配给特定线程

多线程安全处理