C++: 处理线程本地对象的销毁

3
我有一个日志系统,基本上使用线程本地缓冲区进行记录。这有助于减少锁定。一堆消息可以写入线程本地缓冲区并在一次刷新中刷新。而且由于它是线程本地的,我们可以避免为每个日志消息分配缓冲区。
无论如何,在进程退出过程中出现问题。我们在访问线程本地缓冲区时看到崩溃。
我拥有的线程本地对象类似于std::vector<Buffer>。[vector因为有多个缓冲区]。代表性代码如下所示。
Buffer* getBuffer (int index)
{
    static thread_local auto buffers =    std::make_unique<std::vector<Buffer>>();
    return buffers ? buffers->at(index) : nullptr;
}

现在当程序退出并调用全局析构函数时,不幸的是其中一些会记录日志。析构函数从主线程调用(否则什么也不做)。因此,当第一个全局对象被销毁并调用记录器时,线程本地缓冲区被创建,但立即被销毁,因为对象按创建顺序的相反顺序销毁,并且这是最后创建的静态对象。当下一个全局对象析构函数调用记录器时,它实际上正在访问已销毁的对象,我认为这就是问题所在。
但是我查看了unique_ptr析构函数,它确实将其内部指针设置为nullptr [或者至少将指针设置为默认构造的指针-我相信这是值初始化为零吗?]。所以我的return buffers ? buffers->at(index) : nullptr; 检查应该已经防止了对已释放对象的访问,不是吗?
我创建了一个玩具程序来尝试这个问题,我发现buffers ?检查确实可以防止访问。但是在真正的代码库中,这种情况并没有发生。在崩溃点处,向量已经被访问并且已经失效。
现在如果有人告诉我一个神奇的解决方案,那将使我的生活变得容易:-)。否则,有没有人知道为什么unique_ptrbool运算符不返回false。这是因为访问已销毁的对象是典型的未定义行为吗?
我在stackoverflow上读到,如果对象具有平凡的析构函数,则在销毁后访问它是可以的。在那种情况下,如果我在包含unique_ptr的包装器类的析构函数中创建一个线程本地的bool,并将其设置为true,那么我的问题是否解决了?

2
欢迎来到静态初始化顺序混乱的世界。https://isocpp.org/wiki/faq/ctors#static-init-order 这里有一些方法可以确保系统能够正常工作,无论编译器/链接器以什么顺序组合代码。嗯,我猜他们从没写过那部分。此外,这里还有更多信息:https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Nifty_Counter#Also_Known_As - xaxxon
@xaxxon:这不是真正的问题(只有在一开始你不知道它时才是问题)。排序问题有一个简单的解决方案。https://dev59.com/anRC5IYBdhLWcg3wUvUS#335746 - Martin York
1
@MartinYork和最令人困惑的解析(most-vexing parse)如果你了解它,就不会感到困惑。但显然,这些事情足够让很多人感到困惑,以至于它们有了自己的花哨名称 :) - xaxxon
虽然有解决这个问题的方法,但是我认为通常最好在退出main之前进行清理,因为你可以使用显式代码更好地控制。 - Phil1970
我会重复 @Phil1970 的评论。问题在于你在全局范围内有静态存储期对象。因此,代码在 main() 退出后执行。最好在 main 函数中完成这个操作。在 main 中创建一个日志对象,然后将其传递给需要记录日志的任何对象。 - Martin York
3个回答

3

但是我看了unique_ptr的析构函数,它确实将其内部指针设置为nullptr。

不过这并不重要。一旦对象的生命结束,在任何方式中访问该对象就是未定义的行为。因此这不会起作用。

从生命周期的角度来看问题

问题

您的全局日志在某些本地缓冲区之前已经超出了范围。

解决方案

全局日志必须比本地缓冲区存在更长的时间。

如何实现

如果全局日志必须比缓冲区存在更长时间,则必须首先创建它。为了强制执行这个操作,确保在本地缓冲区被构造时请求对全局缓冲区的引用。这将强制创建全局日志并在销毁本地缓冲区时保持其有效。

示例解决方案

类似于以下内容:

class Log
{
    public:
        static Log& getLog()
        {
            static Log theOneAndOnlyLog;
            return theOneAndOnlyLog;
        }
    }
};

class BufferFrame
{
    std::vector<Buffer>   buffer;
    BufferFrame()
    {
        Log::getLog();   // Force the log to be created first.
                         // Note: Order of destruction is guranteed
                         //       for static storage duration objects
                         //       to be the exact reverse of the order of
                         //       creation.
                         //
                         // This means if A is created before B
                         // Then B must be destroyed before A
                         //
                         // Here we know that `theOneAndOnlyLog`
                         // has been constructed (fully) thus `this`
                         // object is created after it. Thus this object
                         // will be destroyed before `theOneAndOnlyLog`.
                         //
                         // This means you can safely accesses `theOneAndOnlyLog`
                         // from the destructor of this object.
    }
    ~BufferFrame()
    {
        // We know the log has been created first
        // So we know it is still alive now.
        foreach(Buffer& buf: buffer) {
             Log::getLog() << buf; // Dump Buffer
        }
    }
    Buffer& at(std::size_t index)
    {
        return buffer.at(index);
    }
};
Buffer& getBuffer(int index)
{
    static thread_local BufferFrame buffers;
    return buffers.at(index);  // Note this will throw if index is out of range.
}

class MyObjectThatLogsToBuffer
{
    public:
        MyObjectThatLogsToBuffer()
        {
            getBuffer(0);   // Have created the FramBuffer
                            // It is created first. So it will be
                            // destroyed after me. So it is safe to
                            // access in destructor.
        }
        ~MyObjectThatLogsToBuffer()
        {
            log("I am destroyed");  // assume this calls getBuffer()
        }                           // while logging. Then it will work.
};

@MGH 同样的规则适用。如果记录日志的对象在其析构函数期间需要访问某些内容,则它只需要在其构造函数期间访问该对象即可强制其生命周期与所需时间一致。 - Martin York
@MGH 添加了日志对象概念。 - Martin York
@MartinYork 好的,我明白了。但是由于这是日志记录,所以无法强制执行它。由于日志记录非常通用,一些析构函数可能会调用某些函数,甚至不知道它正在进行某些日志记录。这是使用全局/静态变量的基本问题。但我必须想出一个解决方案,其中创建顺序无法强制执行。 - MGH
1
@MGH:不要泄露日志记录对象。这只会导致你在后面使用时出现问题。当应用程序已经泄漏时,您是否尝试过调试应用程序以查找泄漏? - Martin York
@MGH 您可以使用MyObjectThatLogsToBuffer(或仅使其继承自它)包装任何可能记录日志的静态对象。或者您可以使用“Schwarz”计数器。 - Martin York
显示剩余6条评论

2
Schwarz计数器或Nifty Counter习惯用法可以实现您想要的功能,但它并不是“魔法”。您可能能够想出一个宏来使其使用起来更加方便(请查看非标准的__COUNTER__),但其要点是:
在每个编译单元(.cpp文件)的顶部,放置一个变量的实例,该变量递增/递减静态计数器并指向日志记录器类型的实际对象。
当计数器从0变为1时,将动态创建“goal”对象。当计数器从1变为0时,将销毁“goal”对象。否则,此管理器对象的构造函数/析构函数不执行任何操作。
这保证了在第一次使用之前创建并在最后一次使用之后销毁。

https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Nifty_Counter#Also_Known_As


1
如果对您的用例足够,我建议使用@martinyork的答案,因为它更简单。我正在重现他的评论,解释为什么您可能想要使用我回答的Schwarz / Nifty计数器: “Schwartz计数器适用于无法显式强制创建顺序的情况。请注意,在我的代码中,我将全局变量包装在函数内,因此需要调用该函数才能获取它们,从而强制执行顺序。像std :: cout(另一个全局变量)这样的东西没有这个优势,因此可以通过Schwartz计数器实现。” - xaxxon

-3

你可以使用std::weak_ptr来跟踪其他线程中超出范围的内容。
我没有一个简单的示例。 不容易找到一个:
https://github.com/alexeyneu/bitcoin/commit/bbd5aa3e36cf303779d888764e1ebb3bd2242a4a

关键行:

    std::weak_ptr<int> com_r;
...
   bhr->SetProgressValue(hwnd , com_r.expired() == 0 ? reserve = *com_r.lock() : reserve, 190);

extern  std::weak_ptr<int> com_r;
...
//inside a class
   std::shared_ptr<int> sp_tray;

   com_r = sp_tray;

  *sp_tray = nVerificationProgress*190;

这是测试用例(已更新)

#include "stdafx.h"
#include "bay.h"
#include <condition_variable>
#include <thread>
#include <atomic>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include <iostream>

#define MAX_LOADSTRING 100

// Global Variables:
HINSTANCE hInst;                                // current instance
wchar_t szTitle[MAX_LOADSTRING];                    // The title bar text
wchar_t szWindowClass[MAX_LOADSTRING];          // the main window class name

// Forward declarations of functions included in this code module:
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPTSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // TODO: Place code here.
    MSG msg;
    HACCEL hAccelTable;

    // Initialize global strings
    LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadString(hInstance, IDC_BAY, szWindowClass, MAX_LOADSTRING);
    MyRegisterClass(hInstance);

    // Perform application initialization:
    if (!InitInstance (hInstance, nCmdShow))
    {
        return FALSE;
    }

    hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_BAY));

    // Main message loop:
    while (GetMessage(&msg, NULL, 0, 0))
    {
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    return (int) msg.wParam;
}

ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEX wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style          = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = WndProc;
    wcex.cbClsExtra     = 0;
    wcex.cbWndExtra     = 0;
    wcex.hInstance      = hInstance;
    wcex.hIcon          = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_BAY));
    wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszMenuName   = MAKEINTRESOURCE(IDC_BAY);
    wcex.lpszClassName  = szWindowClass;
    wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    return RegisterClassEx(&wcex);
}

HWND hWnd;



BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
    hInst = hInstance; // Store instance handle in our global variable

   hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   return TRUE;
}
    std::thread u,u2;
    UINT CALLBACK hammer(VOID *c);
    UINT CALLBACK hammersmith(VOID *c);
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    int wmId, wmEvent;
    HDC hdc;

    switch (message)
    {
    case WM_COMMAND:
        wmId = LOWORD(wParam);
        wmEvent = HIWORD(wParam);
        // Parse the menu selections:
        switch (wmId)
        {
        case IDM_EXIT:
            break;
        case IDM_LETSGO:
            u = std::thread(&hammer,(LPVOID)NULL);
            u2 = std::thread(&hammersmith,(LPVOID)NULL);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
        break;

    case WM_CLOSE:
        DefWindowProc(hWnd, message, wParam, lParam);
        break;          
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
return 0;
}
std::shared_ptr<int> sp_tray;
std::weak_ptr<int> com_r;
std::mutex com_m;   
UINT CALLBACK hammer(VOID *c)
{
    int reserve = 0;
    AllocConsole();
    freopen("CON", "w", stdout);
    while (1)
    {
    std::unique_lock<std::mutex> lb(com_m);
    reserve = com_r.expired() == 0 ? *com_r.lock(): 5;
    lb.unlock();
    std::cout << reserve;   
    }

    return 0;

}
UINT CALLBACK hammersmith(VOID *c)
{   
    while (1)
    {   
        std::unique_lock<std::mutex> lb(com_m);
        sp_tray = std::shared_ptr<int>(new int(7));
        com_r = sp_tray;
        lb.unlock();    
        sp_tray.reset();
    }

    return 0;

}

1
你在过期和锁定调用之间存在竞争条件。 - rubenvb
不,你不会。制作测试用例,你就会发现。当然,为此应该将检查和锁定堆叠在一起。 - Алексей Неудачин
2
一个不会失败的测试用例并不能证明你没有竞争条件。你所说的“堆叠在一起”是什么意思?在C++抽象机的上下文中,我对这个概念不熟悉。明确一点:我的意思是,在你的expired调用和随后的lock调用之间,其他线程可能已经在底层的shared_ptr上调用了clear,使得lock返回一个nullptr,即使在此之前,它本来不会返回nullptrlock的文档说它会原子地执行这个序列,而你的代码却没有这样做。 - rubenvb
所以,如果“expired”返回0,则您有足够的时间获取锁定结果。 - Алексей Неудачин
1
正如@rubenvb指出的那样。我认为com_r.expired() == 0 ? reserve = *com_r.lock() : reserve应该更改为com_r.lock() ? reserve = *com_r.lock() : reserve。在后一种情况下,由第一个锁创建的临时shared_ptr的生存期将延长到语句结束,从而使第二个lock()返回一致的结果。或者基本上在条件之前进行锁定-使用显式的shared_ptr变量。哪种方法更清洁和高效? - MGH
显示剩余10条评论

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