std::thread, std::condition_variable и dll
От: Doom100500 Израиль  
Дата: 13.06.21 11:25
Оценка:
Привет эксперты.

Если бы не dll, то это не было бы в прикладных вопросах, но, если нет dll, то и проблема не воспроизводится.

Вобщем есть код в длл:

Woker.dll:

//worker.h

#ifdef WORKER_EXPORTS
#define WORKER_API __declspec(dllexport)
#else
#define WORKER_API __declspec(dllimport)
#endif

#include <string>
#include <memory>
#include <mutex>


struct WORKER_API WorkerBase {
    virtual ~WorkerBase() {};
    virtual void AddVal(const std::string& val) = 0;
};

class WorkerFactory {
public:
    WORKER_API static WorkerBase* instance();
private:
    static std::unique_ptr<WorkerBase> worker_ptr;
    static std::once_flag once;
};

//worker.cpp

#include "pch.h"
#include "framework.h"
#include "Worker.h"
#include <string>
#include <deque>
#include <mutex>
#include <thread>
#include <iostream>
#include <chrono>


class Worker : public WorkerBase {
    friend class WorkerFactory;
private:
public:
    Worker() {
        worker_thread = std::thread([this] {
            std::cout << "started working thread" << std::endl;
            while (is_running) {
                std::unique_lock<std::mutex> lk(queue_lock);
                queue_cv.wait(lk, [this] {return !queue.empty();});
                if (!is_running) {
                    std::cout << "break" << std::endl;
                }
                std::cout << "got val: " << queue.front() << std::endl;
                queue.pop_front();
            }
            std::cout << "exit working thread" << std::endl;
            });
    }

    ~Worker() {
        queue_lock.lock();
        is_running = false;
        queue.push_back("last value");
        queue_lock.unlock();
        queue_cv.notify_one();

        if (worker_thread.joinable())
            worker_thread.join();

        std::cout << "dtor completed" << std::endl;
    }


    void AddVal(const std::string& val) override {
        queue_lock.lock();
        queue.push_back(val);
        queue_lock.unlock();
        queue_cv.notify_one();
    }

private:
    std::deque<std::string> queue;
    std::mutex queue_lock;
    std::thread worker_thread;
    std::condition_variable queue_cv;
    std::atomic<bool> is_running = true;
};

std::unique_ptr<WorkerBase> WorkerFactory::worker_ptr;
std::once_flag WorkerFactory::once;

WorkerBase* WorkerFactory::instance() {
    std::call_once(once, [] {worker_ptr = std::unique_ptr<WorkerBase>(new Worker);});
    return worker_ptr.get();
}


и main. dll подключается через lib:


#include <chrono>
#include <thread>

#include "../Worker/Worker.h"


int main() {
    auto w = WorkerFactory::instance();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    w->AddVal("1");
    std::this_thread::sleep_for(std::chrono::seconds(1));
    w->AddVal("2");
    std::this_thread::sleep_for(std::chrono::seconds(1));
    w->AddVal("3");

    std::this_thread::sleep_for(std::chrono::seconds(1));



    return 0;
}



output:

started working thread
got val: 1
got val: 2
got val: 3
dtor completed

Я всю голову сломал почему я не вижу в output "exit working thread" и "got val: last value", но join отработал без проблем ?
Если закоментировать join в деструкторе, то получаю исключение "abort called", что правильно.
Если закоментировать работу с conditional variable в деструцторе, то ничего не меняется, хотя поток не должен был завершиться.

Если это всё скомпилированно в одном модуле (exe), то всё работает правильно, т.е. получаю last value из десттруктора и вижу вывод "exit working thread".

Как так? Надо ли волноваться?

Если вдруг, кто-нибудь захочет поиграться, то вот solution
Спасибо за внимание
Re: std::thread, std::condition_variable и dll
От: kov_serg Россия  
Дата: 13.06.21 12:06
Оценка:
Здравствуйте, Doom100500, Вы писали:

void delay() { std::this_thread::sleep_for(std::chrono::seconds(1)); }
int main() {
    { 
        auto w = WorkerFactory::instance();        
        w->>AddVal("1"); delay();
        w->>AddVal("2"); delay();
        w->>AddVal("3"); delay();
    }
    delay();

    return 0;
}

А так что пишет?
Re[2]: std::thread, std::condition_variable и dll
От: Doom100500 Израиль  
Дата: 13.06.21 12:16
Оценка:
Здравствуйте, kov_serg, Вы писали:

_>А так что пишет?


Тоже самое.
Но если в фабрику добавить метод release такой:


void WorkerFactory::release() {
    return worker_ptr.reset();
}


И дернуть его из main:


auto w = WorkerFactory::instance();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    w->AddVal("1");
    std::this_thread::sleep_for(std::chrono::seconds(1));
    w->AddVal("2");
    std::this_thread::sleep_for(std::chrono::seconds(1));
    w->AddVal("3");

    std::this_thread::sleep_for(std::chrono::seconds(1));

    //delay();

    WorkerFactory::release();


То всё работает правильно.:

started working thread
got val: 1
got val: 2
got val: 3
break
got val: last value
exit working thread
dtor completed


Я понимаю, что это связано как-то с убиванием dll рантаймом, но по логике не могу понять как такое происходит. Логика подсказывает, что нужно либо упасть, либо висеть вечно.
Спасибо за внимание
Re: std::thread, std::condition_variable и dll
От: ononim  
Дата: 13.06.21 14:06
Оценка: 15 (2) +3
Вникать лень но что сказать имею.
В винде _НИЗЗЯ_ ждать завершения потока из DllMain — это вызовет дедлок по неочевидным для простого писателя кода причинам. Это означает в том числе что нельзя ждать завершения потока в деструкторе статик объектов, которые объявлены в длл (так как эти деструкторы исполняются из DllMain).
Как много веселых ребят, и все делают велосипед...
Re: std::thread, std::condition_variable и dll
От: ArtDenis Россия  
Дата: 13.06.21 16:04
Оценка:
Здравствуйте, Doom100500, Вы писали:

D>Привет эксперты.

D> ...

Всё целиком лень смотреть, но как wait сможет завершится если queue.empty() даже если is_running = false ?
std::cout << "started working thread" << std::endl;
while (is_running) {
    std::unique_lock<std::mutex> lk(queue_lock);
    queue_cv.wait(lk, [this] {return !queue.empty();});
...


Я в последнее время предпочитаю wait без условия:

std::unique_lock<std::mutex> lk(queue_lock);
while (queue.empty() && is_running) queue_cv.wait(lk);
[ 🎯 Дартс-лига Уфы | 🌙 Программа для сложения астрофото ]
Re: std::thread, std::condition_variable и dll
От: Videoman Россия https://hts.tv/
Дата: 13.06.21 21:45
Оценка: 4 (1)
Здравствуйте, Doom100500, Вы писали:

D>Привет эксперты.

D>Если бы не dll, то это не было бы в прикладных вопросах, но, если нет dll, то и проблема не воспроизводится.

Можно конечно разбираться что конкретно происходит при выходе (скорее всего deadlock), но в общем случае в DLL Main нельзя использовать функции из других системных DLL.
Вот что написано в документации!

Because DLL notifications are serialized, entry-point functions should not attempt to communicate with other threads or processes. Deadlocks may occur as a result.

The entry-point function should perform only simple initialization or termination tasks. It must not call the LoadLibrary or LoadLibraryEx function (or a function that calls these functions), because this may create dependency loops in the DLL load order. This can result in a DLL being used before the system has executed its initialization code. Similarly, the entry-point function must not call the FreeLibrary function (or a function that calls FreeLibrary) during process termination, because this can result in a DLL being used after the system has executed its termination code.

А все эти вещи могут возникнуть в вашем коде.
Re[2]: std::thread, std::condition_variable и dll
От: Doom100500 Израиль  
Дата: 14.06.21 04:46
Оценка:
Здравствуйте, ArtDenis, Вы писали:


AD>Всё целиком лень смотреть, но как wait сможет завершится если queue.empty() даже если is_running = false ?


Там в деструкторе пихается значение в очередь.

AD>Я в последнее время предпочитаю wait без условия:


AD>
AD>std::unique_lock<std::mutex> lk(queue_lock);
AD>while (queue.empty() && is_running) queue_cv.wait(lk);
AD>


Похоже, что те-же яйца, только сбоку. Мне кажется, что wait с условием семантически более понятно.
Спасибо за внимание
Re[2]: std::thread, std::condition_variable и dll
От: Doom100500 Израиль  
Дата: 14.06.21 04:50
Оценка:
Здравствуйте, Videoman, Вы писали:

V>Здравствуйте, Doom100500, Вы писали:


V>Можно конечно разбираться что конкретно происходит при выходе (скорее всего deadlock), но в общем случае в DLL Main нельзя использовать функции из других системных DLL.

V>Вот что написано в документации!
V>

V>Because DLL notifications are serialized, entry-point functions should not attempt to communicate with other threads or processes. Deadlocks may occur as a result.

V>The entry-point function should perform only simple initialization or termination tasks. It must not call the LoadLibrary or LoadLibraryEx function (or a function that calls these functions), because this may create dependency loops in the DLL load order. This can result in a DLL being used before the system has executed its initialization code. Similarly, the entry-point function must not call the FreeLibrary function (or a function that calls FreeLibrary) during process termination, because this can result in a DLL being used after the system has executed its termination code.

V>А все эти вещи могут возникнуть в вашем коде.

Был бы deadlock, было бы понятно, но поток (std::thread) завершается молча, и создаётся впечатление, что всё работает правильно. Только после добавления диагностических логов возник вопрос.
Спасибо за внимание
Re[3]: std::thread, std::condition_variable и dll
От: ArtDenis Россия  
Дата: 14.06.21 05:51
Оценка:
Здравствуйте, Doom100500, Вы писали:

AD>>Всё целиком лень смотреть, но как wait сможет завершится если queue.empty() даже если is_running = false ?


D>Там в деструкторе пихается значение в очередь.


Ну значит не в этом дело.
[ 🎯 Дартс-лига Уфы | 🌙 Программа для сложения астрофото ]
Re[2]: std::thread, std::condition_variable и dll
От: Коваленко Дмитрий Россия http://www.ibprovider.com
Дата: 14.06.21 06:12
Оценка: 11 (1) +2
Здравствуйте, ononim, Вы писали:

O>Вникать лень но что сказать имею.

O>В винде _НИЗЗЯ_ ждать завершения потока из DllMain — это вызовет дедлок по неочевидным для простого писателя кода причинам. Это означает в том числе что нельзя ждать завершения потока в деструкторе статик объектов, которые объявлены в длл (так как эти деструкторы исполняются из DllMain).

Ага.

Общее правило, которое я выработал для себя:

У DLL должны быть функции инициализации и деинициализации. Можно (лучше) с глобальным счетчиком вызовов.

При обнулении этого счетчика, DLL должна прекратить все свои фоновые потоки.

---
Можно обойтись без этих функций.

Тогда DLL должна предоставлять объекты, которые (автоматически) рулят этим глобальным счетчиком использования DLL.

---
Частный пример — DLL сервер COM объектов.

При освобождении последнего COM-объекта, DLL должна прекращать все свои фоновые потоки. Это делается вне DllMain

---
Кстати, если я все правильно помню, у Рихтера еще в третьем издании (это ~98 год) было описана эта проблема, которую он отважно победил
-- Пользователи не приняли программу. Всех пришлось уничтожить. --
Отредактировано 15.06.2021 5:12 DDDX . Предыдущая версия .
Re[3]: std::thread, std::condition_variable и dll
От: Videoman Россия https://hts.tv/
Дата: 14.06.21 09:43
Оценка:
Здравствуйте, Doom100500, Вы писали:

D>Был бы deadlock, было бы понятно, но поток (std::thread) завершается молча, и создаётся впечатление, что всё работает правильно. Только после добавления диагностических логов возник вопрос.


Ну я подебажил ваш код. Так и есть, после выхода из main процесса начинает разрушаться CRT и там уже зовется DllMain из него. Т.е. ресурсы уже начинают отлетать потихоньку, а вы ими продолжаете пользоваться в DLL, тем же std::out и т.д. Все что нужно сделать это вызвать release для w перед выходом из main и все начинает работать как вы хотите.
Re[4]: std::thread, std::condition_variable и dll
От: Doom100500 Израиль  
Дата: 14.06.21 11:39
Оценка:
Здравствуйте, Videoman, Вы писали:

V>Ну я подебажил ваш код. Так и есть, после выхода из main процесса начинает разрушаться CRT и там уже зовется DllMain из него. Т.е. ресурсы уже начинают отлетать потихоньку, а вы ими продолжаете пользоваться в DLL, тем же std::out и т.д. Все что нужно сделать это вызвать release для w перед выходом из main и все начинает работать как вы хотите.


Спасибо.

Это именно тот вывод, который я сделал после Вашего и ononim-ного ответа. Решается ручным управлением времени жизни из вызывающей стороны (тогда unque_ptr тоже не имеет смысла).
Я тоже когда-то Рихтера читал, только забыл уже.

PS. В реальном коде нету std::cout
Спасибо за внимание
Отредактировано 14.06.2021 11:45 Doom100500 . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.