C++11 prototype app design multithreading issue

2019-09-05 22:38发布

I have designed a prototype app with the following classes:

  • Ticker class (discussed here) that has an assignable callback that will be executed every tick (specified by _tickInterval) on a separate thread.
  • SongPosition class that basically represents a position in a song.
  • BeatClock class that assigns a callback to _ticker which is supposed to update _songpos and here I have a problem...

Here is what Ticker class implementation looks like:

#include <cstdint>
#include <functional>
#include <chrono>
#include <thread>
#include <future>
#include <mutex>

class Ticker {
public:
/* TYPES: */
    // Tick interval type in nanoseconds
    typedef std::chrono::nanoseconds tick_interval_type;
    // OnTick callback type
    typedef std::function<void()> on_tick_type;

/* CONSTANTS: */
    // Default ticker interval equal to 120 BPM
    static const tick_interval_type kDefaultTickerInterval;

/* INIT: */
    // Constructs object
    explicit Ticker (tick_interval_type tickInterval) 
    : _onTick ()
    , _tickInterval (tickInterval)
    , _running (false) {}
    // Destructs object
    ~Ticker () {}

/* PROPERTIES: */
    // Sets tick interval to a new value
    void setTickInterval (tick_interval_type tickInterval) {
        _tickIntervalMutex.lock();
        _tickInterval = tickInterval;
        _tickIntervalMutex.unlock();
    }
    // Sets on-tick callback to a new value
    void setOnTick (on_tick_type onTick) { _onTick = onTick; }

/* PUBLIC METHODS: */
    // Starts the ticker
    void start () {
        if (_running) return;
        _running = true;
        std::thread run( &Ticker::loop, this );
        run.detach();
    }
    // Stops the ticker
    void stop () { _running = false; }

private:
/* FIELDS: */
    // OnTick callback, called every tick
    on_tick_type            _onTick;
    // Tick interval between every tick
    tick_interval_type      _tickInterval;
    // Ticker running flag
    volatile bool           _running;
    // Tick interval mutex for thread safety
    std::mutex              _tickIntervalMutex;

/* PRIVATE METHODS: */
    // Inner loop executed on a separate thread to produce ticks
    void loop () {
        tick_interval_type ti;
        while (_running) {
            std::thread run (_onTick);
            run.detach();

            _tickIntervalMutex.lock();
            ti = _tickInterval;
            _tickIntervalMutex.unlock();

            std::this_thread::sleep_for( ti );
        }
    }
};

// Default ticker interval equal to 120 BPM
const Ticker::tick_interval_type Ticker::kDefaultTickerInterval (625000000UL / 120UL);

Here is what SongPosition class implementation looks like:

#include <cstdint>

class SongPosition {
public:
/* INIT: */
    // Constructs song position instance: Ticks | Beats | Measures
    SongPosition (uint8_t ticks = 0u, uint8_t beats = 0u, uint16_t measures = 0u) {
        _ticks = ticks;
        _beats = beats;
        _measures = measures;
    }
    ~SongPosition () {}

/* PROPERTIES:: */
    // Counts from 0..95
    uint8_t ticks () const { return _ticks; }
    // Counts the quarter notes 0..3
    uint8_t beats () const { return _beats; }
    // Counts the measures 0..65535
    uint16_t measures () const { return _measures; }
    // Current meter location, sixteens-notes counter
    uint16_t meter () const { return (_beats << 2) | (_measures << 4); }

/* METHODS: */
    // Resets meter counters
    void reset () {
        _ticks = 0u;
        _beats = 0u;
        _measures = 0u;
    }
    // Resets ticks counter
    void resetTicks () { _ticks = 0u; }
    // Resets beats counter
    void resetBeats () { _beats = 0u; }
    // Increments ticks counter
    void incrementTicks () { ++_ticks; }
    // Increments beats counter
    void incrementBeats () { ++_beats; }
    // Increments measures counter
    void incrementMeasures () { ++_measures; }
    // Decrements measures counter
    void decrementMeasures () { --_measures; }

    bool operator== (const SongPosition& other) const {
        if (_ticks == other.ticks() 
            && _beats == other.beats() 
            && _measures == other.measures())   return true;
        else return false;
    }
    bool operator!= (const SongPosition& other) const {
        return !(*this == other);
    }

private:
/* FIELDS: */
    // Counts from 0..95
    uint8_t     _ticks;
    // Counts the quarter notes 0..3
    uint8_t     _beats;
    // Counts the measures 0..65535
    uint16_t    _measures;
};

inline bool operator<  (const SongPosition& lhs, const SongPosition& rhs) {
    if (lhs.measures() < rhs.measures()) return true;
    else if (lhs.measures() == rhs.measures()) {
        if (lhs.beats() < rhs.beats()) return true;
        else if (lhs.beats() == rhs.beats()) {
            if (lhs.ticks() < rhs.ticks()) return true;
            else return false;
        }
        else return false;
    }
    else return false;
}
inline bool operator>  (const SongPosition& lhs, const SongPosition& rhs) {
    return rhs < lhs;
}
inline bool operator<= (const SongPosition& lhs, const SongPosition& rhs) {
    return !(lhs > rhs);
}
inline bool operator>= (const SongPosition& lhs, const SongPosition& rhs) {
    return !(lhs < rhs);
}

And finally here is what BeatClock class implementation looks like (the problem one):

#include <cstdint>
#include "SongPosition.h"
#include "Ticker.h"

class BeatClock {
public:
/* INNER CLASSES: */
    // Inner state of beat clock
    enum class State : uint8_t {
        kStopped,
        kRunning,
        kPaused
    };

/* CONSTANTS: */
    // Clock pre-start delay to give a slave time to prepare (1ms)
    static const std::chrono::milliseconds kPreStartDelay;

/* CONSTRUCTORS/DISTRUCTORS: */
    // Constructs object
    BeatClock () 
    : _bpm (120UL)
    , _ticker (Ticker::kDefaultTickerInterval)
    , _songpos ()
    , _precount () {
        _ticker.setOnTick( [&] {
            _tickerMutex.lock();
            _songpos.incrementTicks();
            if (_songpos.ticks() == 96U) {      // next beat...
                _songpos.resetTicks();
                _songpos.incrementBeats();
                if (_songpos.beats() == 4U) {   // next measure...
                    _songpos.resetBeats();
                    _songpos.incrementMeasures();
                }
            }
            if (_precount == 0U) {
                _precount = 4U;
                printf( "Send - RealTimeClock: %2i | %1i | %05i", 
                    _songpos.ticks(), _songpos.beats(), _songpos.ticks() );
            }
            ++_precount;
            _tickerMutex.unlock();
        } );
        _state = State::kStopped;
    }
    // Destructs object
    ~BeatClock () {}

/* PROPERTIES: */
    // Gets current BPM value 
    uint64_t bpm () const { return _bpm; }
    // Sets current BPM to a new value
    void setBpm (uint64_t bpm) { 
        _bpm = bpm;
        Ticker::tick_interval_type span (625000000UL / _bpm);
        _ticker.setTickInterval( span );
    }

/* PUBLIC METHODS: */
    // Starts if currently stopped / Pauses if currently running / Resumes if currently paused
    void start () {
        switch (_state)
        {
        case State::kStopped:   // start...
            _songpos.reset();
            this->sendMeter();
            printf( "Send - RealTimeStart" );
            std::this_thread::sleep_for( kPreStartDelay );
            _state = State::kRunning;
            _ticker.start();
            break;
        case State::kRunning:   // pause...
            _ticker.stop();
            _state = State::kPaused;
            break;
        case State::kPaused:    // continue...
            printf( "Send - RealTimeContinue" );
            _state = State::kRunning;
            _ticker.start();
            break;
        }
    }
    // Stops if currently running or paused / Resets if currently stopped
    void stop () {
        switch (_state)
        {
        case State::kStopped:   // reset...
            _songpos.reset();
            this->sendMeter();
            printf( "Send - RealTimeStop" );
            break;
        case State::kRunning:
        case State::kPaused:    // stop...
            printf( "Send - RealTimeStop" );
            _ticker.stop();
            _precount = 0U;
            _state = State::kStopped;
            break;
        }
    }
    // Increments measure and resets sub-counters
    void forward () {
        if (_state == State::kRunning) _ticker.stop();
        _songpos.resetTicks();
        _songpos.resetBeats();
        _songpos.incrementMeasures();
        _precount = 0U;
        this->sendMeter();
        if (_state == State::kRunning) _ticker.start();
    }
    // Decrements measure and resets sub-counters
    void rewind () {
        if (_state == State::kRunning) _ticker.stop();
        _songpos.resetTicks();
        _songpos.resetBeats();
        if (_songpos.measures() > 0U) _songpos.decrementMeasures();
        _precount = 0U;
        this->sendMeter();
        if (_state == State::kRunning) _ticker.start();
    }

private:
/* FIELDS: */
    // Current BPM value
    uint64_t        _bpm;
    // Used for generating tick events.
    Ticker          _ticker;
    // Current song position
    SongPosition    _songpos;
    // Clock precount to scale from 96 to 24 ppqn (0..3)
    uint8_t         _precount;
    // Transport state  
    State           _state;
    // Ticker mutex for thread safety
    std::mutex      _tickerMutex;

/* PRIVATE METHODS: */
    // Sends Song-Position MIDI message
    void sendMeter () {
        printf( "Send - SongPosition: %i", _songpos.meter() );
    }
};

// Clock pre-start delay to give a slave time to prepare (1ms)
const std::chrono::milliseconds BeatClock::kPreStartDelay (1);

_onTick callback is not working as expected, updating _songpos randomly (because of data-races?) Can you please advice how to update _songpos in controllable way from a callback (which is running on a separate thread)?

Here is what I'm expecting to be logged (that is 24 ticks per beat and 4 beats per measure):

Send - SongPosition: 0
Send - RealTimeStart            // begin...
Send - RealTimeClock:  2 | 0 | 00000
Send - RealTimeClock:  5 | 0 | 00000
Send - RealTimeClock:  9 | 0 | 00000
Send - RealTimeClock: 13 | 0 | 00000
Send - RealTimeClock: 17 | 0 | 00000
Send - RealTimeClock: 21 | 0 | 00000
Send - RealTimeClock: 25 | 0 | 00000
Send - RealTimeClock: 29 | 0 | 00000
Send - RealTimeClock: 33 | 0 | 00000
Send - RealTimeClock: 37 | 0 | 00000
Send - RealTimeClock: 41 | 0 | 00000
Send - RealTimeClock: 45 | 0 | 00000
Send - RealTimeClock: 49 | 0 | 00000
Send - RealTimeClock: 53 | 0 | 00000
Send - RealTimeClock: 57 | 0 | 00000
Send - RealTimeClock: 61 | 0 | 00000
Send - RealTimeClock: 65 | 0 | 00000
Send - RealTimeClock: 69 | 0 | 00000
Send - RealTimeClock: 73 | 0 | 00000
Send - RealTimeClock: 77 | 0 | 00000
Send - RealTimeClock: 81 | 0 | 00000
Send - RealTimeClock: 85 | 0 | 00000
Send - RealTimeClock: 89 | 0 | 00000
Send - RealTimeClock: 93 | 0 | 00000
Send - RealTimeClock:  1 | 1 | 00000    // next beat..
Send - RealTimeClock:  5 | 1 | 00000
Send - RealTimeClock:  9 | 1 | 00000
Send - RealTimeClock: 13 | 1 | 00000
Send - RealTimeClock: 17 | 1 | 00000
Send - RealTimeClock: 21 | 1 | 00000
Send - RealTimeClock: 25 | 1 | 00000
Send - RealTimeClock: 29 | 1 | 00000
Send - RealTimeClock: 33 | 1 | 00000
Send - RealTimeClock: 37 | 1 | 00000
Send - RealTimeClock: 41 | 1 | 00000
Send - RealTimeClock: 45 | 1 | 00000
Send - RealTimeClock: 49 | 1 | 00000
Send - RealTimeClock: 53 | 1 | 00000
Send - RealTimeClock: 57 | 1 | 00000
Send - RealTimeClock: 61 | 1 | 00000
Send - RealTimeClock: 65 | 1 | 00000
Send - RealTimeClock: 69 | 1 | 00000
Send - RealTimeClock: 73 | 1 | 00000
Send - RealTimeClock: 77 | 1 | 00000
Send - RealTimeClock: 81 | 1 | 00000
Send - RealTimeClock: 85 | 1 | 00000
Send - RealTimeClock: 89 | 1 | 00000
Send - RealTimeClock: 93 | 1 | 00000
Send - RealTimeClock:  1 | 2 | 00000    // next beat..
Send - RealTimeClock:  5 | 2 | 00000
Send - RealTimeClock:  9 | 2 | 00000
Send - RealTimeClock: 13 | 2 | 00000
Send - RealTimeClock: 17 | 2 | 00000
Send - RealTimeClock: 21 | 2 | 00000
Send - RealTimeClock: 25 | 2 | 00000
Send - RealTimeClock: 29 | 2 | 00000
Send - RealTimeClock: 33 | 2 | 00000
Send - RealTimeClock: 37 | 2 | 00000
Send - RealTimeClock: 41 | 2 | 00000
Send - RealTimeClock: 45 | 2 | 00000
Send - RealTimeClock: 49 | 2 | 00000
Send - RealTimeClock: 53 | 2 | 00000
Send - RealTimeClock: 57 | 2 | 00000
Send - RealTimeClock: 61 | 2 | 00000
Send - RealTimeClock: 65 | 2 | 00000
Send - RealTimeClock: 69 | 2 | 00000
Send - RealTimeClock: 73 | 2 | 00000
Send - RealTimeClock: 77 | 2 | 00000
Send - RealTimeClock: 81 | 2 | 00000
Send - RealTimeClock: 85 | 2 | 00000
Send - RealTimeClock: 89 | 2 | 00000
Send - RealTimeClock: 93 | 2 | 00000
Send - RealTimeClock:  1 | 3 | 00000    // next beat..
Send - RealTimeClock:  5 | 3 | 00000
Send - RealTimeClock:  9 | 3 | 00000
Send - RealTimeClock: 13 | 3 | 00000
Send - RealTimeClock: 17 | 3 | 00000
Send - RealTimeClock: 21 | 3 | 00000
Send - RealTimeClock: 25 | 3 | 00000
Send - RealTimeClock: 29 | 3 | 00000
Send - RealTimeClock: 33 | 3 | 00000
Send - RealTimeClock: 37 | 3 | 00000
Send - RealTimeClock: 41 | 3 | 00000
Send - RealTimeClock: 45 | 3 | 00000
Send - RealTimeClock: 49 | 3 | 00000
Send - RealTimeClock: 53 | 3 | 00000
Send - RealTimeClock: 57 | 3 | 00000
Send - RealTimeClock: 61 | 3 | 00000
Send - RealTimeClock: 65 | 3 | 00000
Send - RealTimeClock: 69 | 3 | 00000
Send - RealTimeClock: 73 | 3 | 00000
Send - RealTimeClock: 77 | 3 | 00000
Send - RealTimeClock: 81 | 3 | 00000
Send - RealTimeClock: 85 | 3 | 00000
Send - RealTimeClock: 89 | 3 | 00000
Send - RealTimeClock: 93 | 3 | 00000
Send - RealTimeClock:  1 | 0 | 00001    // next measure and once again...
Send - RealTimeStop         // enough. :)   

Thank you very much!

1条回答
女痞
2楼-- · 2019-09-05 23:03

I have tested your code and it works fine and I haven't noticed any race condition. Consider this string log( "Send - RealTimeClock: %2i | %1i | %05i", _songpos.ticks(), _songpos.beats(), _songpos.ticks() ); It writes ticks | beats | ticks about every one second. Perhaps, you mean measures instead one of ticks? Proper output looks like the following (the last value is a measure)

Send - SongPosition: 0,
Send - RealTimeStart
Send - RealTimeClock: 1 | 0 | 0
Send - RealTimeClock: 61 | 2 | 0
Send - RealTimeClock: 25 | 1 | 1
Send - RealTimeClock: 85 | 3 | 1
Send - RealTimeClock: 49 | 2 | 2
Send - RealTimeClock: 13 | 1 | 3

And it is ok. You can check if it is race condition just changing thread start to simple function invocation.

And one remark. _tickerMutex appears to be unsafe in your case. If you use mutex to protect some data from simultaneous change then use it in every case when the data is changed. So _songpos is protected in your ticker lambda function but is unsafe in all functions like start, stop etc.

查看更多
登录 后发表回答