Синхронизация потокового видеопроигрывателя

Отказ от ответственности: я задал этот вопрос несколько дней назад на codereview, но не получил ответа. Здесь я меняю формат вопроса на запрос проверки на конкретный проблемы.

Я разрабатываю видеоплеер со следующим дизайном:

Основной поток - это поток GUI (Qt SDK).

Второй поток - нить игрока, который принимает команды из потока GUI для воспроизведения, перемотки вперед, назад, остановки и т.д. Теперь этот поток работает в постоянном цикле и использует мьютексы и условия ожидания для синхронизации в реальном времени с командами основного потока.

У меня есть 2 проблемы с этим кодом:

Я не чувствую, что мой дизайн абсолютно прав: я использую как блокировки мьютекса, так и атомные переменные. Интересно, могу ли я оставаться только с атомами и использовать блокировки только для установки условий ожидания.

Я испытываю непоследовательные ошибки (вероятно, из-за гонки состояния, когда команда воспроизведения пытается заблокировать мьютекс, который уже заблокирован потоком во время работы цикла воспроизведения), когда я запускаю команды "play", которые активируют цикл внутри петля потока. Поэтому я полагаю, что он блокирует доступ к общим переменным в основной поток.

Я удалил код из ненужного материала, и он обычно выглядит следующим образом:

  void PlayerThread::drawThread()//thread method passed into new boost::thread
{

   //some init goes here....

      while(true)
      {
          boost::unique_lock<boost::mutex> lock(m_mutex);
          m_event.wait(lock); //wait for event

          if(!m_threadRun){
             break; //exit the tread
          }

           ///if we are in playback mode,play in a loop till interrupted:
          if(m_isPlayMode == true){

              while(m_frameIndex < m_totalFrames && m_isPlayMode){

                       //play
                       m_frameIndex ++;

              }

               m_isPlayMode = false;

          }else{//we are in a single frame play mode:

               if(m_cleanMode){ ///just clear the screen with a color

                       //clear the screen from the last frame
                       //wait for the new movie to get loaded:

                       m_event.wait(lock); 


                       //load new movie......

               }else{ //render a single frame:

                       //play single frame....

               }


          }


      }

}

Вот функции-члены вышеуказанного класса, которые отправляют команды в цикл потока:

void PlayerThread::PlayForwardSlot(){
//   boost::unique_lock<boost::mutex> lock(m_mutex);
    if(m_cleanMode)return;
    m_isPlayMode = false;
    m_frameIndex++;
     m_event.notify_one();
}

 void PlayerThread::PlayBackwardSlot(){
 //  boost::unique_lock<boost::mutex> lock(m_mutex);
  if(m_cleanMode)return;
   m_isPlayMode = false;
   m_frameIndex-- ;
   if(m_frameIndex < 0){
       m_frameIndex = 0;
   }

    m_event.notify_one();

 }


 void PlayerThread::PlaySlot(){
 // boost::unique_lock<boost::mutex> lock(m_mutex);
   if(m_cleanMode)return;
   m_isPlayMode = true;
   m_event.notify_one(); //tell thread to  start playing.

  }

Все члены флага, такие как m_cleanMode, m_isPlayMode и m_frameIndex, являются атомами:

  std::atomic<int32_t>   m_frameIndex;
  std::atomic<bool>      m_isPlayMode; 
  std::atomic<bool>      m_cleanMode;

Сводка вопросов:

  • Нужно ли мне блокировать мьютексы при использовании атомистики?

  • Я устанавливаю ожидание в правильном месте внутри цикла while нить?

  • Любое предложение лучшего дизайна?

UPDATE:

Хотя я получил ответ, который, кажется, находится в правильном направлении, я действительно не понимаю его. Особенно часть псевдокода, которая говорит об обслуживании. Мне совершенно непонятно, как это будет работать. Мне хотелось бы чтобы получить более подробный ответ. Также странно, что я получил только один конструктивный ответ на такую ​​общую проблему. Поэтому я возвращаю награду.

Ответы

Ответ 1

Любое предложение лучшего дизайна?

Да! Поскольку вы используете Qt, я бы настоятельно предложил использовать Qt eventloop (помимо данных пользовательского интерфейса это IMO - одна из основных точек продажи этой библиотеки) и асинхронный сигнал/слоты для управления, а не для вашей доморощенной синхронизации, которая - как вы узнали - это очень хрупкое предприятие.

Основное изменение, которое это принесет вашему текущему дизайну, заключается в том, что вам нужно будет сделать свою видео логику как часть цикла событий Qt или, проще говоря, просто выполните QEventLoop::processEvents. Для этого вам понадобится QThread.  Тогда это очень просто: вы создаете класс, который наследует от QObject let say PlayerController, который должен содержать такие сигналы, как play, pause, stop и класс Player, у которых будут слоты onPlay, onPause, onStop (или без включения, ваши предпочтения). Затем создайте объект 'controller' класса PlayerController в потоке GUI и Player в потоке видео (или используйте QObject::moveToThread). Это важно, так как Qt имеет понятие сродства потоков, чтобы определить, в каких потоках SLOT выполняются. Не соединяйте объекты, выполняя QObject::connect(controller, SIGNAL(play()), player, SLOT(onPlay())). Любой вызов теперь PlayerController:play на "контроллер" из потока GUI приведет к тому, что метод onPlay "игрока" будет выполнен в видеопотоке на следующей итерации цикла событий. То, где вы можете затем изменить свои логические переменные статуса или выполнить другое действие без необходимости явной синхронизации, поскольку ваши переменные теперь только изменения из видеопотока.

Итак, что-то в этих строках:

class PlayerController: public QObject {
Q_OBJECT

signals:
    void play();
    void pause();
    void stop();
}

class Player: public QObject {
Q_OBJECT

public slots:
    void play() { m_isPlayMode = true; }
    void pause() { m_isPlayMode = false; }
    void stop() { m_isStop = true; };

private:
    bool m_isPlayMode;
    bool m_isStop;
}

class VideoThread: public QThread {

public:
    VideoThread (PlayerController* controller) {
        m_controller = controller;
    }

protected:
    /* override the run method, normally not adviced but we want our special eventloop */
    void run() {
        QEventLoop loop;
        Player* player = new Player;

        QObject::connect(m_controller, SIGNAL(play()), player, SLOT(play()));
        QObject::connect(m_controller, SIGNAL(pause()), player, SLOT(pause()));
        QObject::connect(m_controller, SIGNAL(stop()), player, SLOT(stop()));

        m_isStop = false;
        m_isPlayMode = false;
        while(!m_isStop) {
            // DO video related stuff
            loop.processEvents();
        }
    }


private:
    PlayerController* m_controller;
}



// somewhere in main thread
PlayerController* controller = new PlayerController();
VideoThread* videoThread = new VideoThread(controller);
videoThread.start();
controller.play();

Ответ 2

Самая большая проблема с вашим кодом заключается в том, что вы ожидаете безоговорочно. boost:: condition:: notify_one only просыпается поток ожидания. Это означает Forward Step\Backward Step then Play, если достаточно быстро проигнорирует команду воспроизведения. Я не получаю clean mode, но вам нужно как минимум

if(!m_isPlayMode)
{
     m_event.wait(lock);
}

В вашем коде остановка и переход к кадру практически то же самое. Вы можете использовать tristate PLAY,STEP, STOP, чтобы иметь возможность использовать рекомендуемый способ ожидания переменной условия

while(state == STOP)
{
    m_event.wait(lock);
}

1. Нужно ли мне блокировать мьютексы при использовании атомистики?

Технически да. В этом конкретном случае я так не думаю. Текущие условия гонки (я заметил):

  • режим воспроизведения, воспроизведение и воспроизведение не приведут к тому же m_frameIndex в зависимости от того, находится или нет drawThread в цикле while(m_frameIndex < m_totalFrames && m_isPlayMode). Действительно, m_frameIndex может увеличиваться один или два раза (playforward).
  • Ввод состояния воспроизведения в PlaySlot может быть проигнорирован, если drawThread выполнить m_isPlayMode = false; до получения следующего события. Сейчас это не проблема, потому что это произойдет, только если m_frameIndex < m_totalFrames является ложным. Если PlaySlot изменял m_frameIndex, тогда у вас будет случай нажатия на игру, и ничего не произойдет.

2. Устанавливаю ли я ожидание в правильном месте внутри цикла while?

Я хотел бы предложить только одно ожидание в вашем коде для простоты. И расскажите о следующем, что нужно сделать, используя определенные команды:

 PLAY, STOP, LOADMOVIE, STEP

3. Любое предложение лучшего дизайна?

Использовать явную очередь событий. Вы можете использовать тот, который основан на Qt (требуется Qthreads) или на основе boost. Один из основанных на boost использует a boost::asio::io_service и a boost::thread.

Вы запускаете цикл событий, используя:

boost::asio::io_service service;
//permanent work so io_service::exec doesnt terminate immediately.
boost::asio::io_service::work work(service); 
boost::thread thread(boost::bind(&boost::asio::io_service::exec, boost::ref(service)));

Затем вы отправляете свои команды из графического интерфейса, используя

MYSTATE state;
service.post(boost::bind(&MyObject::changeState,this, state));
  • Ваш метод воспроизведения должен запросить другую игру, учитывая, что состояние не изменилось, а не циклическое. Это позволяет лучше избавиться от пользователя.
  • Ваш метод шагов должен запросить остановку перед отображением фрейма.

псевдокод:

play()
{
 if(state != PLAYING)
   return;
 drawframe(index);
 index++;
 service.post(boost::bind(&MyObject::play, this));
}

stepforward()
{
 stop();
 index++;
 drawframe(index);
}

stepbackward()
{
 stop();
 index--;
 drawframe(index);
}

Edit: Существует только один нить игрока, который создается один раз и выполняется только один цикл событий. Is эквивалентно QThread:: start(). Поток будет работать до тех пор, пока цикл не вернется, что будет до тех пор, пока объект work не будет уничтожен ИЛИ когда вы явно остановите службу. Когда вы запрашиваете остановку службы, все опубликованные задачи, которые все еще ожидаются, будут выполнены в первую очередь. Вы можете прервать поток для быстрого выхода, если это необходимо.

Когда есть призыв к действию, которое вы публикуете в цикле событий, выполняемом потоком игрока.

Примечание. Вам, вероятно, понадобятся указатели на общие ресурсы для службы и потока. Вам также нужно будет поставить точки прерывания в методе воспроизведения, чтобы обеспечить чистоту потока во время воспроизведения. Тебе не нужно столько атомов, сколько раньше. Вам больше не нужна переменная условия.

Ответ 3

  1. Любое предложение лучшего дизайна?

Вместо использования отдельного потока используйте QTimer и играйте в основном потоке. Никаких атомных или мьютексов не требуется. Я не совсем отслеживаю с помощью m_cleanMode, поэтому я в основном извлек его из кода. Если вы подробнее расскажете о том, что он делает, я купирую его в код.

class Player
{
    int32_t m_frameIndex;
    bool m_cleanMode;

    QTimer m_timer;

    void init();
    void drawFrame();

slots:
    void play();
    void pause();
    void playForward();
    void playBackward();

private slots:
    void drawFrameAndAdvance();
}

void Player::init()
{
    // some init goes here ...

    m_timer.setInterval(333); // 30fps
    connect(&m_timer, SIGNAL(timeout()), this, SLOT(drawFrameAndAdvance()));
}

void Player::drawFrame()
{
    // play 1 frame
}

void Player::drawFrameAndAdvance()
{
    if(m_frameIndex < m_totalFrames - 1) {
        drawFrame();
        m_frameIndex++;
    }
    else m_timer.stop();
}

void PlayerThread::playForward()
{
    if(m_cleanMode) return;

    m_timer.stop(); // stop playback
    if(m_frameIndex < m_totalFrames - 1) {
        m_frameIndex++;
        drawFrame();
    }
}

void PlayerThread::playBackward()
{
    if(m_cleanMode)return;

    m_timer.stop(); // stop playback
    if(m_frameIndex > 0) {
        m_frameIndex--;
        drawFrame();
    }
}

void PlayerThread::play()
{
    if(m_cleanMode) return;
    m_timer.start(); // start playback
}

void PlayerThread::pause()
{
    if(m_cleanMode) return;
    m_timer.stop(); // stop playback
}