线程视频播放器同步

时间:2014-12-31 13:24:23

标签: c++ multithreading qt

免责声明:几天前我问过这个问题on codereview,但没有回答。我将问题格式从审核请求更改为特定问题。

我正在开发一个具有以下设计的视频播放器:

主线程 - 是GUI线程(Qt SDK)。

第二个线程 - 玩家线程接受来自GUI线程的命令来播放,前进,后退,停止等等。现在,该线程以恒定循环运行并使用互斥锁和等待条件与主线程命令同步

我对此代码有两个问题:

我觉得我的设计不完全正确:我正在使用互斥锁和原子变量。我想知道我是否只能保留原子并仅使用锁来设置等待条件。

我遇到了不一致的错误(可能是由于播放命令试图锁定互斥锁时的条件竞争,当播放循环正在工作时已经被线程锁定)当我运行“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;

问题摘要::

  1. 使用atomics时是否需要互斥锁?

  2. 我是否在while循环内的正确位置设置等待 线?

  3. 有关更好设计的建议吗?

  4. 更新

    虽然我得到了一个似乎正确方向的答案但我并不理解它。特别是伪代码部分正在谈论服务。我完全不清楚它是如何工作的。我想得到一个更详细的答案。同样奇怪的是,我只收到了这个常见问题的一个建设性答案。所以我重置了赏金。

3 个答案:

答案 0 :(得分:6)

您的代码最大的问题是您无条件地等待。 boost::condition::notify_one only wake up a thread which is waiting。这意味着Forward Step\Backward Step然后Play如果足够快将忽略播放命令。我没有clean mode,但你至少需要

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

在你的代码中停止并踩到一个框架几乎是一样的。你可能想使用三态PLAY,STEP, STOP来使用推荐的等待条件变量的方式

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

<强> 1。使用原子时我需要互斥锁吗?

技术上是的。在这个特定的情况下,我不这么认为。 目前的比赛条件(我注意到):

  • 播放模式,前播和播放不会产生相同的m_frameIndex,具体取决于drawThread是否在while(m_frameIndex < m_totalFrames && m_isPlayMode)循环内。实际上m_frameIndex可以增加一次或两次(前进)。
  • 如果PlaySlot在收到下一个事件之前执行drawThread,则可以忽略在m_isPlayMode = false;中输入播放状态。现在这是一个非问题,因为它只会在m_frameIndex < m_totalFrames为假时发生。如果PlaySlot正在修改m_frameIndex,那么您将有推动游戏的情况,并且没有任何事情发生。
  

<强> 2。我是否在线程的while循环内的正确位置设置等待?

为简单起见,我建议您的代码只有一次等待。并明确说明使用特定命令要做的下一步:

 PLAY, STOP, LOADMOVIE, STEP
  

<强> 3。有关更好设计的任何建议吗?

使用显式事件队列。您可以使用基于Qt(需要Qthreads)或基于增强的功能。基于提升的提示使用boost::asio::io_serviceboost::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)));

然后使用

从GUI发送命令
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);
}

修改 只有一个玩家线程 ,只创建一次并仅执行 一个事件循环 。相当于QThread :: start()。只要循环没有返回,线程就会存在,直到work object is destroyed OR when you explicitly stop the service。当您请求停止服务时,所有尚未处理的已发布任务将首先执行。如有必要,您可以中断线程以便快速退出。

当有一个动作要求时,你在玩家线程运行的事件循环中发帖。

注意:您可能需要服务和线程的共享指针。您还需要在播放方法中放置中断点,以便在播放期间干净地停止线程。你不需要像以前那么多的原子。你不再需要一个条件变量了。

答案 1 :(得分:1)

  

有关更好设计的建议吗?

是的!由于你使用的是Qt,我强烈建议使用Qt的eventloop(除了UI这个IMO是该库的主要卖点之一)和异步信号/插槽来进行控制而不是你自己开发的同步,你发现了 - 这是一项非常脆弱的事业。

这将为您当前的设计带来的主要变化是您必须将视频逻辑作为Qt事件循环的一部分,或者更简单,只需执行QEventLoop::processEvents。为此,您需要QThread。  然后它非常简单:你创建了一个继承自QObject的类,让我们说PlayerController应该包含playpausestop和类{{}}等信号{1}}其中包含广告符PlayeronPlayonPause(或者没有点击,您的偏好)。然后在GUI线程中创建onStop类的'controller'对象,在'video'线程中创建PlayerController对象(或使用Player)。这很重要,因为Qt具有线程亲和性的概念来确定执行哪些线程SLOT。不通过QObject::moveToThread连接对象。现在从GUI线程向“控制器”上的QObject::connect(controller, SIGNAL(play()), player, SLOT(onPlay()))调用将导致在下一个事件中视频线程中执行'player'的PlayerController:play方法循环迭代。然后你可以在这里更改你的布尔状态变量或者做其他类型的操作,而不需要显式同步,因为你的变量现在只是从视频线程的变化。

这样的话:

onPlay

答案 2 :(得分:0)

  
      
  1. 有关更好设计的任何建议吗?
  2.   

使用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
}