在SOUI4中工作线程如果与UI线程交互
在SOUI4中工作线程如果与UI线程交互
很多时候程序的耗时过程需要在工作线程执行,执行过程中可能需要通过UI线程来展示运行状态及结果,这就涉及到工作线程与UI线程交互的问题。
SOUI的UI框架本身不是线程安全的,如果在工作线程直接操作UI元素,运气好就是数据不正常,运气坏一点就是程序崩溃。因此正确的在工作线程操作UI元素是一个非常重要的问题。
早期SOUI提供了一个NotifyCenter对象,用户可以在工作线程包装一个事件发给NotifyCenter,NotifyCenter会在UI线程定时检查事件队列,再把事件传递到UI线程。这种方式使用起来相对复杂。
从4.0开始,SOUI的MsgLoop实现了一个PostTask方法,用户在工作线程拿到主线程的IMessageLoop即可使用IMessageLoop::PostTask方法将一个异步任务交给主线程去执行。
如果没有模式窗口,一个程序的主线程只有一个IMessageLoop对象,用户拿到这个对象后,就可以保证PostTask的任务会被主线程执行,但是如果程序运行过程中有弹出MessageBox等模式窗口,则程序中可能同时存在多个IMessageLoop对象,因此通过IApplication::GetMsgLoop方法拿到主线程的IMessageLoop,并PostTask到这个对象,有可能不能被即时执行。考虑到这个可能的副作用,我也一直没有向大家推荐这个方法,只是我自己偷偷的使用(因为只有我知道使用过程中可能的坑)。
最近通过对IMessageLoop进行重新设计,引用了parent messageloop的概念,这样设计以后,一个IMessageLoop在启动前会获取当前正在运行的IMessageLoop对象,称之为parent msgloop, 在新的msgloop运行的时候,会自动执行parent msgloop中的async task list,从而保证了通过IApplication::GetMsgLoop提交的异步任务,无论当前是哪个msgloop在运行都会被即时执行。
因此在4.4中工作线程要切换到UI线程,最简单的方法,就是获取主线程的msgloop并通过PostTask方法将一个异步任务交给主线程去执行。
例如sliveplayer里的SVodPlayer 用法:
1 class SVodPlayer : public ITransVodListener { 2 public: 3 Value m_cbHandler; 4 Value m_onError; 5 Value m_onDuration; 6 Value m_onPlayPosition; 7 Value m_onStateChanged; 8 9 protected: 10 IMessageLoop* GetMsgLoop() { 11 return m_presenter->GetHostWnd()->GetMsgLoop(); 12 } 13 STDMETHOD_(void, onError)(THIS_ LPCSTR url, transvod::ErrorCode errCode, int statusCode) override { 14 SStringA strUrl(url); 15 STaskHelper::post(GetMsgLoop(), this, &SVodPlayer::_onError, strUrl, errCode, statusCode); 16 } 17 18 STDMETHOD_(void, _onError)(THIS_ LPCSTR url, transvod::ErrorCode errCode, int statusCode) { 19 if (m_onError.IsFunction()) { 20 Context* ctx = m_onError.context(); 21 Value args[2] = {NewValue(*ctx,(int)errCode),NewValue(*ctx,statusCode)}; 22 ctx->Call(m_cbHandler, m_onError, 2, args); 23 } 24 } 25 26 STDMETHOD_(void, onTotalTime)(THIS_ LPCSTR url, uint32_t totalTime) override { 27 SStringA strUrl(url); 28 STaskHelper::post(GetMsgLoop(), this, &SVodPlayer::_onTotalTime, strUrl, totalTime); 29 } 30 STDMETHOD_(void, _onTotalTime)(THIS_ SStringA& url, uint32_t totalTime) { 31 if (m_onDuration.IsFunction()) { 32 Context* ctx = m_onDuration.context(); 33 Value args = NewValue(*ctx, totalTime); 34 ctx->Call(m_cbHandler, m_onDuration, 1, &args); 35 } 36 } 37 38 STDMETHOD_(void, onPlayedTimeChanged)(THIS_ LPCSTR url, uint32_t playedTime) override { 39 SStringA strUrl(url); 40 STaskHelper::post(GetMsgLoop(), this, &SVodPlayer::_onPlayedTimeChanged, strUrl, playedTime); 41 } 42 STDMETHOD_(void, _onPlayedTimeChanged)(THIS_ LPCSTR url, uint32_t playedTime) { 43 if (m_onPlayPosition.IsFunction()) { 44 Context* ctx = m_onPlayPosition.context(); 45 Value args[1] = { NewValue(*ctx,playedTime) }; 46 ctx->Call(m_cbHandler, m_onPlayPosition, 1, args); 47 } 48 } 49 50 STDMETHOD_(void, onStateChanged)(THIS_ LPCSTR url, transvod::PlayerState state, transvod::ErrorReason reason) override { 51 SStringA strUrl(url); 52 STaskHelper::post(GetMsgLoop(), this, &SVodPlayer::_onStateChanged, strUrl, state, reason); 53 } 54 STDMETHOD_(void, _onStateChanged)(THIS_ LPCSTR url, transvod::PlayerState state, transvod::ErrorReason reason) { 55 if (m_onStateChanged.IsFunction()) { 56 Context* ctx = m_onStateChanged.context(); 57 Value args[2] = { NewValue(*ctx,(int)state),NewValue(*ctx,(int)reason) }; 58 ctx->Call(m_cbHandler, m_onStateChanged, 2, args); 59 } 60 } 61 };
播放器的回调函数都是从播放器的线程过来的,要更新UI的状态,我需要将它切换到UI线程来执行,通过GetMsgLoop(),我可以获取到UI线程的IMessageLoop对象,然后通过STaskHelper::post这个方法,可以将回调函数及各种参数打包到一个IRunnable里, 然后就会在UI线程执行这个Runnable。可以看出来使用起来还是非常简单的。
例如SVodPlayer::onStateChanged是播放器线程的回调函数,SVodPlayer::_onStateChanged则是UI线程执行这个回调的地方。参数类型都基本一样。
使用这种方法实现异步任务需要注意2个问题:
1 需要注意的是参数的生命周期。
比如SVodPlayer::onStateChanged的第一个参数是一个LPCSTR类型,如果在STaskHelper::post的时候,直接把这个url参数打包到IRunnable中,这个参数在SVodPlayer::_onStateChanged执行的时候已经失效了,因此在UI线程再访问这个参数的内存空间则会出错。
使用STaskHelper::post打包参数的时候,是使用模板技术,将所用到的参数复制一份。显然复制裸指针是不行的,因此我使用了一个SStringA对象,将这个url复制一份到strUrl中,STaskHelper::post再把这个strUlr复制一份到打包的IRunnable中,从而保证这个url在执行的时候是有效的。
2 另一个需要注意的问题在于挂起的异步任务清理
如果一个对象析构了,而这个对象可能还有挂在MsgLoop的异步任务没有执行完成。IMessageLoop提供了一个方法:IMessageLoop::RemoveTasksForObject(void *pObj), pObj是执行异步任务的对象。当前对象析构前,应该调用这个方法把挂起的异步任务清空,才能安全的释放对象。