简体   繁体   中英

C++ : Handle thread-local object destruction

I have a logging system, which basically uses a thread-local buffer to log. This helps in reducing locking. A bunch of message can be written into the thread-local buffer and flushed in one shot. And also since it is thread-local, we can avoid allocating buffer per log message.

Anyway the issue is during the process exit. We are seeing crash while accessing the thread-local buffer.

The thread-local object I have is something like std::vector<Buffer> . [ vector because there are multiple buffers]. A representative code is something like this.

Buffer* getBuffer (int index)
{
    static thread_local auto buffers =    std::make_unique<std::vector<Buffer>>();
    return buffers ? buffers->at(index) : nullptr;
}

Now when the program exits and the global destructors are called and unfortunately some of them logs. The destructors are called from main thread (which otherwise does nothing). So when the first global object is destroyed and it calls the logger, the thread_local buffer is created, but it is immediately destroyed, because the objects are destroyed in reverse order of creation and this is the last static object created. When the next global object destructor calls logger, it is effectively accessing destroyed object, which I think is the problem.

But I looked at the unique_ptr destructor and it does set the pointer inside it to nullptr [or atleast it sets the pointer to default constructed pointer - which I believe is value initialized to zero??]. So my return buffers ? buffers->at(index) : nullptr; return buffers ? buffers->at(index) : nullptr; check should have prevented the access to freed object isn't it?

I created a toy program to try this out and I see that the buffers ? check does prevent the access. But in the real code base that is not happening. At the point of crash the vector is accessed at it is already toast.

Now if someone can tell me a magic solution, it will make my life easy :-). Otherwise any idea why the bool operator of unique_ptr does not return false . Is it because it is classic undefined behavior accessing a destroyed object.

I read in stack-overflow that if the object has a trivial destructor, it is okay to access it after destruction. In that case would my problem is solved if I create a thread-local bool just above the unique_ptr , and set it to true in the destructor of a wrapper class containing unique_ptr ?

But I looked at the unique_ptr destructor and it does set the pointer inside it to nullptr

Does not matter. Once the object's lifespan is over accessing the object in any way is UB. So this will not work.

Looking at the issue in terms of lifespan

The Issue.

Your global log is going out of scope before some of your local buffers.

The solution

The global log must live longer than your local buffers.

How to achieve it

If the global log must live longer than the buffers it must be created first. To force this make sure your local buffer ask for a reference to the global buffer while they are being constructed. This will force the global log to be created first and thus be alive when the local buffer is destroyed.

Example Solution

Something like this:

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.
};

The Schwarz counter or Nifty Counter Idiom can do what you want, but it's not "magic". You may be able to come up with a macro to make it less cumbersome to use (check out non-standard __COUNTER__), but the gist of it is:

In each compilation unit (.cpp file) at the very top, you put an instance of a variable that increments/decrements a static counter and a pointer to a real object of the logger type.

When the counter goes from 0 to 1, the "goal" object is dynamically created. When the counter goes from 1 to 0, the "goal" object is destroyed. Otherwise the constructor/destructor of this manager object don't do anything.

This guarantees creation before first use and destruction after last use.

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

you could use std::weak_ptr there to track stuff gone out of scope in other thread.
i dont have an easy sample on that. not easy one is there:
https://github.com/alexeyneu/bitcoin/commit/bbd5aa3e36cf303779d888764e1ebb3bd2242a4a

key lines on that:

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

and

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

   com_r = sp_tray;

  *sp_tray = nVerificationProgress*190;

and this is testcase(updated)

#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;

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM