4

I've implemented a class that cyclically runs a supplied function.

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

And here'a s simple program to see if it works:

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

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

When I build the app with g++, the program builds and runs without any problems. However, when I use the Microsoft's compiler (v18) the program compiles as well, but it fails at runtime.

When I use the release configuration I get the following exception from one of the threads:

Unhandled exception at 0x000007F8D8E14A30 (msvcr120.dll) in Program.exe: Fatal program exit requested.

When I use the debug configuration, a Microsoft Visual C++ Runtime Library error pops up every second:

Debug Error!

Program: ...\path\Program.exe

R6010 - abort() has been called

In both configurations:

  • The exception is thrown/the errors start popping up in the second iteration of the timer's loop.

  • The program does not enter the Tick function even once, even though thread(handler) gets invoked.

  • Although the stack traces at the moment of error differ in those two configurations, neither of them contains anything from my code. Both start with ntdll.dll!UserThreadStart(); the debug one ends with msvcr123d.dll!_NMSG_WRITE() and the release one ends with msvcr120.dll!abort().

Why do the problems appear and why only when the app is compiled with MSVC? Is it some kind of MSVC's bug? Or maybe should I change something in the compiler's configuration?

tearvisus
  • 2,013
  • 2
  • 15
  • 32
  • 2
    You need one sleep in the main, and you should do join in Stop(). It's only one iteration! – amchacon Feb 25 '15 at 12:18
  • I don't understand what you're saying. There's an infinite loop going on. In each iteration a new thread is spawned. Which thread should I join and why? – tearvisus Feb 25 '15 at 13:11
  • When you use stop(); the "infinite loop" ends. You could wait for join him :) – amchacon Feb 25 '15 at 13:21
  • I'm aware of that, but that wasn't my my intention when I was writing this class. `Stop()` is supposed to stop the timer from making new calls, not to cease all activity started by the timer. Besides, if I wanted to wait for the call to end, I should accumulate references to all invoked threads, because the previously invoked ones might still be working. – tearvisus Feb 25 '15 at 13:30
  • 2
    please do not call `lock()` and `unlock()` manually, use an [`std::lock_guard`](http://en.cppreference.com/w/cpp/thread/lock_guard) instead. Right now if anything throws an exception inside your locks you'll deadlock. – Mgetz Feb 25 '15 at 13:35
  • @Mgetz Thanks for the hint. I was aware of the potential exception problem. `lock_guard` seems to be a perfect fix. – tearvisus Feb 25 '15 at 13:38
  • The reason why your code does not appear on the stack seems to be [this msvc bug](https://connect.microsoft.com/VisualStudio/feedback/details/845184). I have not tested, but it says it was fixed in VS2015. – Paulo Alves Sep 11 '15 at 13:49

1 Answers1

3

Your thread is throwing std::bad_function_call, the exception is not handled so the run time is calling abort().

Changing:

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

To

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

Fixes the problem. I guess this is because you are sharing it between threads?

Also works if you create a local and pass a reference to that:

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

So it must have somehow gone out of scope.

Edit: Indeed the function object is destructed at after the ctor call. Not sure why this is.

paulm
  • 5,629
  • 7
  • 47
  • 70
  • Indeed, this solves the problem. I think that there's some implicit conversion in which pointer to the function is converted to the `function` object. Then the object is used in the ctor and destroyed afterwards, because it's not needed anymore. But why does this code work fine when compiled with g++? – tearvisus Feb 25 '15 at 13:46
  • It must be that the function object is still on scope in g++. Although I thought if a temporary object is bound to a reference then its life time should be extended? – paulm Feb 25 '15 at 14:31
  • [It seems](http://stackoverflow.com/questions/9941502/c-reference-to-out-of-scope-object) that the compiler's behavior in such cases is undefined. So I was just lucky that g++ didn't put anything in the `function`'s place. – tearvisus Feb 25 '15 at 14:48
  • 1
    It's probably just plain vanilla undefined behavior of the next-to-worst kind - where it appears to be working correctly. The temporary, despite not being in scope any longer, just hasn't been overwritten yet. If some other value then reuses that place in memory, and a 'user' supplied value is written into it, you have a security vulnerability. – MaHuJa Feb 25 '15 at 14:50
  • @tearvisus No, you're unlucky it doesn't point out your error before it seriously breaks something when you can least afford it. See also "fail fast". – MaHuJa Feb 25 '15 at 15:09
  • But why would it be out of scope though? Thats what I don't understand, maybe I'll start a new question about it – paulm Feb 25 '15 at 19:52
  • @MaHuJa I probably should have put lucky in quotes. I meant that it was just a coincidence that it worked correctly... at least for the time being. I agree that errors like that are among the worst. – tearvisus Feb 25 '15 at 22:33
  • @paulm Having a reference to an object does not stop the object from being destroyed. It's similar to pointers - if you have a pointer pointing at an objected created via RAII and the objects goes out of scope - poof! You end up with a dandling pointer. – tearvisus Feb 25 '15 at 22:37
  • Ah I see, its because its a const reference: http://stackoverflow.com/questions/2784262/does-a-const-reference-prolong-the-life-of-a-temporary – paulm Feb 26 '15 at 00:20