多线程视频播放器同步

16

免责声明:我几天前在codereview上提出了这个问题,但没有得到答案。现在我将问题格式从审核请求更改为具体问题。

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

主线程 - 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. 有没有更好的设计建议?

更新:

虽然我得到了一个看起来正确的答案,但我确实不理解它。特别是伪代码部分,它谈到了服务,它完全不清楚如何工作。我想要一个更详细的答案。对于这样一个常见的问题,我只收到了一个有建设性的回答,这很奇怪。所以我重置了赏金。


迈克尔,如果你不知道Qt有自己的互斥、条件等类,例如QMutexQReadWriteLockQWaitCondition - Super-intelligent Shade
2
几年前,我在Windows上使用C语言做了完全相同的事情(尽管是解码MP3文件,但我自己进行了解码)。我有三个线程:主GUI、处理GUI消息和解码MP3。多线程编程的诀窍是尽可能地将各个线程隔离开来。所有变量都被隔离开来(这是我避免问题的主要目标),我只有一个锁:用于写入和读取消息队列。写入操作来自主GUI,而读取操作来自处理消息的线程。 - γηράσκω δ' αεί πολλά διδασκόμε
2
队列是一个结构体数组。每个结构体都包含有关要执行的操作的信息,例如播放、停止、暂停、下一首歌曲、歌曲标题等。主 GUI 将新消息添加到队列中,处理线程会删除这些消息。它能够正常工作,主要是因为我没有共享变量,只有一个锁。希望这可以帮助你。 - γηράσκω δ' αεί πολλά διδασκόμε
@MichaelIV,+200悬赏金去哪了??? - Super-intelligent Shade
我猜这个奖励会给那个基于Qt的解决方案,它看起来更简单、更自然地融入了环境。UmNyobe的回答很好,但我并没有完全理解,他也不想提供更多细节。但是@UmNyobe绝对值得获得奖励。 - Michael IV
显示剩余2条评论
3个回答

6

您的代码最大的问题是无条件等待。 boost::condition::notify_one 只会唤醒正在等待的线程。这意味着如果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. 当使用原子操作时,我需要使用互斥锁吗?

从技术上讲是需要的。但在这种特定情况下,我认为不需要。 当前存在的竞争条件如下:

  • 在游戏模式下,播放前进和后退取决于drawThread是否在while(m_frameIndex < m_totalFrames && m_isPlayMode)循环内部。实际上m_frameIndex可能会增加一次或两次(播放前进)。
  • 如果PlaySlot修改了m_frameIndex,则在drawThread接收到下一个事件之前执行m_isPlayMode = false;将忽略PlaySlot中的播放状态。现在这不是问题,因为只有在m_frameIndex < m_totalFrames为false时才会发生。否则就会出现按下播放按钮却没有任何反应的情况。

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));
  • 您的游戏方法应该在状态未改变时请求另一个游戏,而不是循环。这将允许更好地用户抢占。
  • 您的步骤方法应在显示帧之前请求停止。

伪代码:

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 对象被销毁或者您显式停止服务之前,线程将一直存在。当您请求停止服务时,所有尚未完成的待处理任务将首先被执行。如果必要,您可以中断线程以快速退出。

当有调用动作时,您会将其发布到由播放器线程运行的事件循环中。

注意:您可能需要共享指针来管理服务和线程。您还需要在播放方法中放置中断点,以允许在回放期间干净地停止线程。您不再需要像以前那样多的原子量。您也不再需要条件变量。


有趣的方法。你能再详细介绍一下中断点吗?因为Boost API被使用了。顺便说一句,cleanMode是当我加载其他素材进行播放时使用的。因此,它是一个将播放器线程当前使用的所有资源清理和重置的点。 - Michael IV
此外,为什么我需要在循环中使用 while(state == STOP) { m_event.wait(lock); } ?在每个渲染帧之后,线程都会进入等待状态,除非它正在内部循环中播放。 - Michael IV
有关中断点,请参见此处。例如,您可以明确使用boost::this_thread::interruption_point();作为中断点。 - UmNyobe
我建议使用与“step”不同的“stop”状态,以使代码更易于编写。这样可以使用更简单的算法,并减少调用线程和接收线程之间共享变量的数量。在这里只有一个等待线程,因此使用“while”并没有什么好处(但也不会有坏处)。 - UmNyobe
我真的很希望能将你的问题标记为正确。我相信它是正确的。但我不完全明白你的意思。我要求你详细说明,但你没有回复。我将重新打开这个问题。 - Michael IV
显示剩余3条评论

1

有更好的设计建议吗?

有!因为您正在使用Qt,我强烈建议使用Qt的事件循环(除了UI之外,这是该库的主要卖点之一)和异步信号/插槽来进行控制,而不是使用手工同步,正如您发现的那样,它是一个非常脆弱的任务。

这将会对您当前的设计带来主要的变化,即您需要将视频逻辑作为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();

0
有更好的设计建议吗? 使用 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
}

我使用单独的线程的原因是因为有很多同步操作。一些处理可能会变得很重,因为我不仅要解码视频,还要进行一些图像处理。这就是为什么我必须将所有操作与 GUI 线程并发,以使 UI 保持响应。 - Michael IV

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接