C++中使用静态成员的std::thread出现卡顿

4
我正在尝试创建一个日志记录类,其中调用写日志的操作是静态的。现在,由于性能要求,我希望在单独的线程中执行实际的日志记录。由于写入日志的函数是静态的,所以我认为线程也需要是静态的,这也与另一个静态成员函数绑定,该函数执行实际的日志写入操作。我尝试编写代码,但某种方式会导致在静态线程初始化期间挂起。以下是复制行为的代码示例:

"Logger.h"

#ifndef LOGGER_H
#define LOGGER_H

#include <condition_variable>
#include <mutex>
#include <queue>
#include <string>
#include <thread>
#include <vector>

#define LIBRARY_EXPORTS

#ifdef LIBRARY_EXPORTS // inside DLL
#define LIBRARY_API __declspec(dllexport) 
#else // outside DLL
#define LIBRARY_API __declspec(dllimport)
#endif 

using namespace std;

namespace Company { namespace Logging {

class LIBRARY_API Logger
{
public:
    ~Logger();

    void static Write(string message, vector<string> categories = vector<string>());

private:
    Logger();
    Logger(Logger const&) {}
    void operator=(Logger const&) {}

    static thread processLogEntriesThread;

    static void ProcessLogEntries();
};

}}

#endif

"Logger.cpp"

#include "Logger.h"

#include <iostream>

using namespace std;

namespace Company { namespace Logging {

thread Logger::processLogEntriesThread = thread(&Logger::ProcessLogEntries);

Logger::Logger()
{
}

Logger::~Logger()
{
    Logger::processLogEntriesThread.join();
}

void Logger::Write(string message, vector<string> categories)
{
    cout << message << endl;
}

void Logger::ProcessLogEntries()
{
}

}}

我发现的一个奇怪的问题是只有在将类打包成DLL时才会出现挂起的情况。如果我直接将类文件用于控制台EXE项目中,似乎一切正常。
所以,我的问题基本上是挂起的部分和我是否正确地做了事情。
提前感谢您...

1
我没有看到任何“Logger”实例,因此析构函数没有被调用,也没有“join”。 - Jarod42
Write调用中,你需要一个互斥锁。此外,你实际上正在创建一个单例...一个静态类。当你这样做时,你必须小心初始化和对象永远不会真正被销毁。 - Gort the Robot
@Steven。如果我没有其他选择,我会考虑使用单例模式。但是我不确定在使用单例模式时是否能够保持“Logger :: Write(...)”的调用约定。它可能会变成类似于“Logger :: GetInstance()。Write(...)”,当然我更喜欢前者。我可能需要尝试实现它才能确定。 - Boggs
只有当类打包在DLL中时,才会出现挂起的情况。这可能是原因。全局变量的初始化发生在CRT DLL入口例程中(请参见底部),这意味着对DLL入口例程内部调用所施加的限制也适用于这些全局变量的初始化。线程可以通过CreateThread在DLL启动期间创建(请参见底部),但不能保证使用std::thread - dyp
1
你使用的是MSVC吗?可能与这个有关:https://dev59.com/1Wgv5IYBdhLWcg3wBMEK - beerboy
显示剩余4条评论
3个回答

1

为提供链接添加一些上下文支持。这也会给出该链接提供的内容的概念。 - Harshith Rai

1
当类被打包在DLL中时,挂起部分只会发生在这种情况下。请参见Dynamic-Link Library Best Practices以获取完整详情,为什么会挂起:
您不应从DllMain中执行以下任务:
  • 调用CreateThread。如果您不与其他线程同步,则创建线程可以正常工作,但是存在风险。
解决方案是为您的记录器库提供一个初始化函数/对象,用户必须在main中显式调用它,而不是在进入main之前初始化全局线程对象。此函数应创建线程。
或使用std::call_once在第一次记录调用时创建线程。但是,这涉及对每个记录调用进行额外的条件检查。此检查可能很便宜,但并非免费。

0

我看不到记录器线程的任何用法。在类中作为成员有一个线程并不意味着所有成员函数都将在创建的线程中运行。如果没有记录器实例,析构函数将永远不会被调用。iostream 不是线程安全的!

你需要做的:

创建某种存储来收集日志信息。这个实例必须是线程安全的!从外部世界推送消息到这个实例中。该实例本身必须有一个自己的线程,它从存储中读取数据并将数据放入输出中。这也必须以线程安全的方式完成,因为读取和写入来自不同的线程!


我承认代码并不完整,也不是很有意义,因为我更关注演示悬挂问题。我的初始计划是使用静态声明的队列来包含日志(可能还要在队列周围放置锁)。"Write()"函数只会将日志推送到队列中。"processLogEntriesThread"线程将实际记录日志并清空队列。 "cout"只是为了测试在调用"Write()"之前是否发生了悬挂。无论如何,现在我只是在研究使用Boost.Log 2.0。 - Boggs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接