Умные указатели Тестовое приложение пула объектов завершается неудачно при завершении / выходе - PullRequest
0 голосов
/ 20 июня 2019

Я написал многопоточное приложение на Qt / C ++ 11, Windows.
Идея состояла в том, чтобы иметь и перерабатывать некоторые строки из пула, используя умные указатели.
Вот stringpool.cpp:

#include "stringpool.h"

QMutex StringPool::m_mutex;
int StringPool::m_counter;
std::stack<StringPool::pointer_type<QString>> StringPool::m_pool;

StringPool::pointer_type<QString> StringPool::getString()
{
    QMutexLocker lock(&m_mutex);

    if (m_pool.empty())
    {
        add();
    }
    auto inst = std::move(m_pool.top());

    m_pool.pop();
    return inst;
}

void StringPool::add(bool useLock, QString * ptr)
{
    if(useLock)
        m_mutex.lock();

    if (ptr == nullptr)
    {
        ptr = new QString();
        ptr->append(QString("pomo_hacs_%1").arg(++m_counter));
    }

    StringPool::pointer_type<QString> inst(ptr, [this](QString * ptr) { add(true, ptr); });
    m_pool.push(std::move(inst));

    if(useLock)
        m_mutex.unlock();
}

А вот stringpool.h:

#pragma once

#include <QMutex>
#include <QString>
#include <functional>
#include <memory>
#include <stack>

class StringPool
{
public:
    template <typename T> using pointer_type = std::unique_ptr<T, std::function<void(T*)>>;
    //
    StringPool() = default;
    pointer_type<QString> getString();

private:
    void add(bool useLock = false, QString * ptr = nullptr);
    //
    static QMutex m_mutex;
    static int m_counter;
    static std::stack<pointer_type<QString>> m_pool;
};

А вот и тестовое приложение:

#include <QtCore>
#include "stringpool.h"

static StringPool Pool;


class Tester : public QThread
{
public:
    void run() override
    {
        for(int i = 0; i < 20; i++)
        {
            {
                auto str = Pool.getString();
                fprintf(stderr, "Thread %p : %s \n", QThread::currentThreadId(), str->toUtf8().data());
                msleep(rand() % 500);
            }
        }
        fprintf(stderr, "Thread %p : FINITA! \n", QThread::currentThreadId());
    }
};

#define MAX_TASKS_NBR       3

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    Tester tester[MAX_TASKS_NBR];

    for(auto i = 0; i < MAX_TASKS_NBR; i++)
        tester[i].start();

    for(auto i = 0; i < MAX_TASKS_NBR; i++)
        tester[i].wait();
    //
    return 0;
}

Компилируется нормально, запускается и выдаетследующий результат:

program output

Ну, идея состоит в том, что приложение работает (очевидно) ОК.
Но сразу после его завершения у меня появляется эта ошибка:
popup with abort has been called message
У кого-нибудь есть идеи, как это исправить?

1 Ответ

0 голосов
/ 22 июня 2019

Причина этой ошибки связана с умным указателем, а не с многопоточностью.

Вы определяете pointer_type как псевдоним для unique_ptr с пользовательским средством удаления

template <typename T> using pointer_type = std::unique_ptr<T, std::function<void(T*)>>;

Вы создаете строки с пользовательскими удалителями

void StringPool::add(bool useLock, QString * ptr)
{    
    if (ptr == nullptr)
    {
        ptr = new QString();
        ptr->append(QString("pomo_hacs_%1").arg(++m_counter));
    }

    StringPool::pointer_type<QString> inst(ptr, [this](QString * ptr) { add(true, ptr); }); // here
    m_pool.push(std::move(inst));
}

В конце программы m_pool выходит из области видимости и запускает деструктор.

Рассмотрим путь казни ... m_pool попытается уничтожить всех его участников. Для каждого члена настраиваемый удалитель. Пользовательский удалитель вызывает add. add толкает указатель на стек.

Логически это бесконечный цикл. Но более вероятно создать какое-то неопределенное поведение, нарушив согласованность структуры данных. (т.е. стек не должен выдвигать новых членов, пока он разрушается). Исключение может произойти из-за переполнения стека функций или буквального переполнения стека (хех), когда недостаточно памяти для добавления в структуру данных стека. Поскольку исключение возникает в деструкторе необработанным, оно немедленно завершает программу. Но это также может быть ошибкой сегмента из-за толчка при разрушении.

Исправления:

Мне уже не понравилась ваша add функция.

StringPool::pointer_type<QString> StringPool::getString()
{
    QMutexLocker lock(&m_mutex);

    if (m_pool.empty())
    {
        auto ptr = new QString(QString("pomo_hacs_%1").arg(++m_counter));
        return pointer_type<QString>(ptr, [this](QString* ptr) { reclaim(ptr); });
    }

    auto inst = std::move(m_pool.top());    
    m_pool.pop();
    return inst;
}

void StringPool::reclaim(QString* ptr)
{
    QMutexLocker lock(&m_mutex);

    if (m_teardown)
        delete ptr;
    else
        m_pool.emplace(ptr, [this](QString* ptr) { reclaim(ptr); });
}

StringPool::~StringPool()
{
    QMutexLocker lock(&m_mutex);
    m_teardown = true;
}

StringPool был статическим классом, но с этим исправлением теперь он должен быть одноэлементным классом.

Может быть заманчиво вытащить m_teardown из критической секции, но это общие данные, поэтому это откроет дверь для условий гонки. В качестве преждевременной оптимизации вы можете сделать m_teardown std::atomic<bool> и выполнить проверку на чтение перед входом в критическую секцию (можете пропустить критическую секцию, если false), но для этого требуется 1) вы проверяете значение снова в критической секции и 2 ) вы переходите от истинного к ложному ровно один раз.

...