免责声明:几天前我问过这个问题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;
问题摘要::
使用atomics时是否需要互斥锁?
我是否在while循环内的正确位置设置等待 线?
有关更好设计的建议吗?
更新
虽然我得到了一个似乎正确方向的答案但我并不理解它。特别是伪代码部分正在谈论服务。我完全不清楚它是如何工作的。我想得到一个更详细的答案。同样奇怪的是,我只收到了这个常见问题的一个建设性答案。所以我重置了赏金。
答案 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_service
和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)));
然后使用
从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
应该包含play
,pause
,stop
和类{{}}等信号{1}}其中包含广告符Player
,onPlay
,onPause
(或者没有点击,您的偏好)。然后在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)
- 有关更好设计的任何建议吗?
醇>
使用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
}