Многопоточное приложение, скомпилированное с помощью MSVC, дает сбой во время выполнения

Я реализовал класс, который циклически запускает предоставленную функцию.

//Timer.h
#include <chrono>
#include <mutex>
#include <thread>

class Timer {
public:
    Timer(const std::chrono::milliseconds period, const std::function<void()>& handler);
    ~Timer();
    void Start();
    void Stop();
    bool IsRunning() const;

private:
    const std::function<void()>& handler;
    const std::chrono::milliseconds period;
    bool isRunning = false;
    mutable std::recursive_mutex lock;
    int counter = 0;

    void DoLoop(int id);
};

//Timer.cpp
#include "Timer.h"

Timer::Timer(const std::chrono::milliseconds period, const std::function<void()>& handler) :handler(handler), period(period), lock(){}

Timer::~Timer() {
    Stop();
}

void Timer::Stop() {
    lock.lock();
    isRunning = false;  
    lock.unlock();
}

void Timer::Start() {
    lock.lock();
    if (!isRunning) {
        isRunning = true;
        counter++;
        std::thread(&Timer::DoLoop, this, counter).detach();
    }
    lock.unlock();
}

void Timer::DoLoop(int id) {
    while (true){
        std::this_thread::sleep_for(period);
        lock.lock();
        bool goOn = isRunning && counter==id;
        if (goOn) std::thread(handler).detach();
        lock.unlock();

        if (!goOn)
            break;
    }
}

bool Timer::IsRunning() const {
    lock.lock();
    bool isRunning = this->isRunning;
    lock.unlock();
    return isRunning;
}

А вот простая программа, чтобы проверить, работает ли она:

void Tick(){ cout << "TICK" << endl; }

int main() {
    Timer timer(milliseconds(1000), Tick);
    timer.Start();
    cin.get();
}

Когда я создаю приложение с помощью g++, программа собирается и запускается без каких-либо проблем. Однако, когда я использую компилятор Microsoft (v18), программа также компилируется, но происходит сбой во время выполнения.

Когда я использую конфигурацию выпуска, я получаю следующее исключение из одного из потоков:

Необработанное исключение по адресу 0x000007F8D8E14A30 (msvcr120.dll) в Program.exe: запрошен фатальный выход из программы.

Когда я использую конфигурацию отладки, каждую секунду появляется ошибка библиотеки времени выполнения Microsoft Visual C++:

Ошибка отладки!

Программа: ...\путь\Program.exe

R6010 - был вызван abort()

В обеих конфигурациях:

  • Исключение выдается/ошибки начинают появляться на второй итерации цикла таймера.

  • Программа ни разу не заходит в функцию Tick, хотя вызывается thread(handler).

  • Хотя трассировки стека на момент ошибки различаются в этих двух конфигурациях, ни одна из них не содержит ничего из моего кода. Оба начинаются с ntdll.dll!UserThreadStart(); отладочная заканчивается на msvcr123d.dll!_NMSG_WRITE(), а релизная — на msvcr120.dll!abort().

Почему возникают проблемы и почему только тогда, когда приложение скомпилировано с помощью MSVC? Это какая-то ошибка MSVC? Или, может быть, я должен что-то изменить в конфигурации компилятора?


person tearvisus    schedule 25.02.2015    source источник
comment
Вам нужен один сон в основном, и вы должны присоединиться к Stop(). Это только одна итерация!   -  person amchacon    schedule 25.02.2015
comment
Я не понимаю, что ты говоришь. Идет бесконечный цикл. На каждой итерации создается новый поток. К какой теме присоединиться и почему?   -  person tearvisus    schedule 25.02.2015
comment
Когда вы используете stop(); бесконечный цикл заканчивается. Вы можете подождать, чтобы присоединиться к нему :)   -  person amchacon    schedule 25.02.2015
comment
Я знаю об этом, но это не входило в мои намерения, когда я писал этот класс. Stop() должен останавливать таймер от новых вызовов, а не прекращать все действия, начатые таймером. Кроме того, если бы я хотел дождаться завершения вызова, я должен был бы накапливать ссылки на все запущенные потоки, потому что те, которые были запущены ранее, могли еще работать.   -  person tearvisus    schedule 25.02.2015
comment
пожалуйста, не вызывайте lock() и unlock() вручную, вместо этого используйте std::lock_guard. Прямо сейчас, если что-то вызовет исключение внутри ваших замков, вы зайдете в тупик.   -  person Mgetz    schedule 25.02.2015
comment
@Mgetz Спасибо за подсказку. Я знал о потенциальной проблеме исключения. lock_guard кажется идеальным решением.   -  person tearvisus    schedule 25.02.2015
comment
Причина, по которой ваш код не отображается в стеке, по-видимому, заключается в этой ошибке msvc. Я не проверял, но написано, что это было исправлено в VS2015.   -  person Paulo Alves    schedule 11.09.2015


Ответы (1)


Ваш поток генерирует std::bad_function_call, исключение не обрабатывается, поэтому время выполнения вызывает abort().

Изменение:

const std::function<void()>& handler;

To

const std::function<void()> handler;

Устраняет проблему. Я думаю, это потому, что вы делитесь им между потоками?

Также работает, если вы создаете локальный объект и передаете ссылку на него:

  const std::function<void()> f = Tick;
  Timer timer(std::chrono::milliseconds(1000), f);

Значит, это как-то вышло за рамки.

Изменить: действительно, объект функции уничтожается после вызова ctor. Не уверен, почему это так.

person paulm    schedule 25.02.2015
comment
Действительно, это решает проблему. Я думаю, что есть какое-то неявное преобразование, при котором указатель на функцию преобразуется в объект function. Затем объект используется в ctor и впоследствии уничтожается, потому что он больше не нужен. Но почему этот код отлично работает при компиляции с помощью g++? - person tearvisus; 25.02.2015
comment
Должно быть, объект функции все еще находится в области видимости в g++. Хотя я думал, что если временный объект привязан к ссылке, то его время жизни должно быть продлено? - person paulm; 25.02.2015
comment
Похоже, что поведение компилятора в таких случаях не определено. Так что мне просто повезло, что g++ ничего не поставил на место function. - person tearvisus; 25.02.2015
comment
Вероятно, это просто ванильное неопределенное поведение в худшем виде, где оно работает правильно. Временное, несмотря на то, что оно больше не входит в область действия, просто еще не было перезаписано. Если какое-то другое значение затем повторно использует это место в памяти и в него записывается значение, предоставленное «пользователем», у вас есть уязвимость в системе безопасности. - person MaHuJa; 25.02.2015
comment
@tearvisus Нет, вам не повезло, что он не указывает на вашу ошибку до того, как серьезно что-то сломает, когда вы меньше всего можете себе это позволить. См. также отказ быстро. - person MaHuJa; 25.02.2015
comment
Но почему это должно быть вне поля зрения? Вот чего я не понимаю, может быть, я начну новый вопрос об этом - person paulm; 25.02.2015
comment
@MaHuJa Мне, наверное, стоило взять удачу в кавычки. Я имел в виду, что это было просто совпадение, что он работал правильно... по крайней мере, на данный момент. Я согласен, что такие ошибки являются одними из худших. - person tearvisus; 26.02.2015
comment
@paulm Наличие ссылки на объект не предотвращает его уничтожение. Это похоже на указатели — если у вас есть указатель, указывающий на объект, созданный с помощью RAII, и объекты выходят за рамки — пуф! В итоге вы получите висячий указатель. - person tearvisus; 26.02.2015
comment
А, понятно, потому что это ссылка на константу: stackoverflow.com/questions/2784262/ - person paulm; 26.02.2015