【问题标题】:Threaded Video Player sync线程视频播放器同步
【发布时间】:2015-02-27 13:58:57
【问题描述】:

免责声明:我前几天问过这个问题on codereview,但没有得到答案。这里我将问题格式从review request改为具体问题。

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

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

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

这段代码有两个问题:

我不觉得我的设计是完全正确的:我同时使用互斥锁和原子变量。我想知道我是否可以只使用原子并且只使用锁来设置等待条件。

当我运行“播放”命令时,我遇到了不一致的错误(可能是由于在播放循环工作时播放命令尝试锁定已被线程锁定的互斥锁时的条件竞争)线程循环。所以我想它会阻止对主线程的共享变量的访问。

我已经从不需要的东西中剥离了代码,它通常是这样的:

  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_cleanModem_isPlayModem_frameIndex 等所有标志成员都是原子的:

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

问题总结::

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

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

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

更新:

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

【问题讨论】:

  • Michael,如果你不知道 qt 有自己的互斥量、条件等类,例如QMutexQReadWriteLockQWaitCondition
  • 另外,在 qt 中,您可以跨线程发送信号,这在某些情况下可以帮助完全避免使用锁
  • @InnocentBystander 是的,我知道,但我已经在使用 Boost 线程 API,并且没有时间正确学习 Qt 的相关 API。
  • 几年前我在windows中使用c做了同样的事情(播放mp3文件,我自己解码)。我有三个线程:主 gui,处理来自 gui 的消息,解码 mp3。多线程的诀窍是尽可能多地隔离线程。隔离的所有变量(这是我避免问题的主要目标)并且我只有一个:从消息队列写入和读取。写入是从主 gui 执行的,读取是从线程处理消息中执行的。
  • 队列是一个结构数组。每个结构都有关于要执行什么的信息,例如播放、停止、暂停、下一首歌曲、歌曲标题等。主 gui 向队列中添加了一条新消息,处理线程删除了这些消息。它没有任何问题,主要是因为我没有共享变量。只有一把锁。希望对您有所帮助。

标签: c++ multithreading qt


【解决方案1】:

您的代码最大的问题是您无条件地等待。 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 &lt; m_totalFrames &amp;&amp; m_isPlayMode) 循环内。事实上,m_frameIndex 可以增加一到两次(前向)。
  • 如果drawThread在接收下一个事件之前执行m_isPlayMode = false;,则可以忽略PlaySlot进入播放状态。现在这不是问题,因为它只会在m_frameIndex &lt; m_totalFrames 为假的情况下发生。如果PlaySlot 正在修改m_frameIndex,那么您将有推演的情况,但没有任何反应。

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

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

 PLAY, STOP, LOADMOVIE, STEP

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

使用显式事件队列。您可以使用基于 Qt(需要 Qthreads)或基于 boost 的一种。基于 boost 的使用 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));
  • 鉴于状态没有改变,您的播放方法应该请求另一个播放,而不是循环播放。它允许更好的用户抢占。
  • 您的 step 方法应在显示框架之前请求停止。

伪代码:

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

编辑: 只有一个玩家线程,它被创建一次并且只执行一个事件循环。 Is 等价于 QThread::start()。只要循环不返回,线程就会一直存在,直到workobject is destroyed OR when you explicitly stop the service。当您请求停止服务时,所有仍待处理的已发布任务将首先执行。如有必要,您可以中断线程以快速退出。

当你在播放器线程运行的事件循环中发布一个动作的调用时。

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

【讨论】:

  • 有趣的方法。如果使用了 Boost API,您能否在中断点上扩展一点?顺便说一句,cleanMode 是当我加载另一个素材来播放时。所以这是一个点,所有的播放器线程当前使用的资源被清理和重置。
  • 另外,为什么我需要 while(state == STOP) { m_event.wait(lock); } ?在每个渲染帧之后的循环中,线程进入等待状态,除非它正在内部循环中播放。
  • 中断点见here。例如,您可以显式使用boost::this_thread::interruption_point(); 作为中断点。
  • 我推荐一个不同于stepstop 状态,只是为了使代码更容易。它允许更简单的算法。它允许您在调用者和接收线程之间使用更少的共享变量。您在这里只有一个等待线程,因此使用 while 不会带来好处(但也不会造成伤害)。
  • 我是否正确地说您建议在每次有行动呼吁时创建新的播放器线程?在什么上下文中调用 drawFrame?请注意,我在播放器线程中使用 OpenGL 上下文必须是在玩家的整个生命周期中都存在。因此在每次玩家调用时重新创建它是不切实际的。
【解决方案2】:

有更好的设计建议吗?

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

这将为您当前的设计带来的主要变化是您必须将视频逻辑作为 Qt 事件循环的一部分,或者更简单,只需执行 QEventLoop::processEvents。为此,您需要QThread。 然后它非常简单:您创建一些继承自QObject 的类,假设PlayerController 应包含playpausestop 之类的信号和一个类Player,它将有插槽onPlayonPauseonStop(或不带 on,您的偏好)。然后在 GUI 线程中创建PlayerController 类的“控制器”对象,在“视频”线程中创建Player 对象(或使用QObject::moveToThread)。这很重要,因为 Qt 具有线程亲和性的概念来确定在哪个线程 SLOT 中执行。不通过QObject::connect(controller, SIGNAL(play()), player, SLOT(onPlay())) 连接对象。现在从 GUI 线程对“控制器”上的 PlayerController:play 的任何调用都将导致在下一次事件循环迭代中在 视频线程 中执行“播放器”的 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();

【讨论】:

    【解决方案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
    }
    

    【讨论】:

    • 我使用单独线程的理由是因为那里有很多同步的东西。一些处理可能会变得很繁重,因为我不仅解码视频而且还进行一些图像处理.这就是为什么我必须让它与我希望 UI 保持响应的 GUI 线程并发。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-04
    相关资源
    最近更新 更多