一、Duilib不能开发多线程程序?
记得非常久曾经就听有人说过Duilib的多线程支持性不好,原因是Duilib里面的控件是用数组管理的全局变量,不能进行多线程訪问,加锁非常麻烦。事实上这个说法是非常不合理的,以至于在开发多线程程序时直接将Duilib拒之门外。当然使用Duilib里面开发多线程是木有不论什么问题的,不要单纯地觉得:其它的界面库就能使用多个线程同一时候操作一个控件,Duilib就不行。事实证明,这点MFC做不到,WinForm也做不到,连微软自己都搞不定的东西,不能算Duilib的缺陷。
二、UI线程与工作线程
凡是开发过Windows界面的人应该都知道,先要做出稳定并且没有闪烁的界面,全部的绘制代码,必须是顺序运行的,画完一个再画一个,并且绘制代码都要写在一个位置,WM_PAINT里面(当然你写在外面也没问题,但还要处理擦除,一般不这么干)。所以,全部的绘制都是异步运行的,想绘制什么,就得先保存下来,使用Invalidate()去异步触发WM_PAINT,待WM_PAINT消息处理时,再去绘制。就这一点就决定了,绘制UI多线程是行不通的,所以全部的绘制操作都是消息循环所在的主线程完毕的。既然多线程不能用于绘制,那么多个线程同一时候訪问同一个控件的属性怎么样?(比方,有一个buttonA,一个线程运行A->SetWith(600),还有一个线程运行A->SetWith(800);)这相同是一个坏主意,控件的width属性是由一个int值来标志的,SetWith这一类的操作,既没有使用原子锁,又没有不论什么相互排斥锁,同一时候訪问是有可能出问题的。或许你以前这样弄过,并没有发现问题,那是由于两个线程“碰上”的几率不大,是你没有碰上而已,但不要存在这样的侥幸心理,几率不大不代表没有,一旦出现势必会出错。要想控件属性支持多线程訪问,要么每个操作都使用原子操作符,要么把每个方法都加上相互排斥锁。然而这样弄是根本划不来的,效率太低了。那么当工作线程须要去改动控件属性怎么办呢?相同是採用异步的方法,在线程里面post自己定义消息,然后再窗体函数里面处理其它线程发过来的消息,去回调应该处理的操作。为什么称之为异步?就是由于我们利用了Windows的“系统消息队列”来充当了我们的异步事件处理队列,不管是“并行“事件还是”串行“事件,经过消息队列编排之后都会串行处理,化多线程为单线程。
比方,下载完毕的通知,是下载线程调用的,这时我们想去改动控件属性时,我们要post一个自己定义消息
void CMainFrame::OnFinished(int percent,TASK*) { PostMessage(WM_UPDATEAD,0,0); }
在窗体过程处理函数里面去操作控件
LRESULT CMainFrame::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { if (uMsg == WM_UPDATEAD) { CDuiString htmlPath = L"file:///"; CDuiString currentPath = m_PaintManager.GetCurrentPath(); htmlPath.Append(currentPath); htmlPath.Append(L"/html/Advertisement/ad.html"); m_pWkeWeb->LoadUrl(L"/html/Advertisement/ad.html"); }
可是这样写是一件十分令人头疼的事情,要为每个函数都弄一个自己定义消息,你不嫌烦编译器都嫌烦了。
三、Skilla为Duilib编写的应用程序框架skillcore,能够完美胜任多线程
Duilib尽管在控件绘制上,花的心思不少。可是在应用程序管理方面却还在“刀耕火种”的时代,还在使用最原始的WinMain。这样做尽管对使用者透明,可是对于多线程程序,不多点花点心思还真是处理不好。就单纯一个析构顺序颠倒的问题,就能导致程序崩溃。所以Skilla仿照Juce的应用程序框架为Duilib写了一套,来解决问题(当然没有Juce里面那么复杂了),以下我们来看怎样使用。
class TestApp : public DuiApplicationBase { public: void OnInit(); void RequestQuit(); void OnQuit(); ScopedPtr<CMainFrame> m_pFrame; }; START_DUI_APPLICATION(TestApp) void TestApp::OnInit() { wkeInit(); CPaintManagerUI::SetInstance(*this); CPaintManagerUI::SetResourcePath(CPaintManagerUI::GetInstancePath()); m_pFrame = new CMainFrame; if (m_pFrame == NULL) return ; m_pFrame->Create(NULL, _T("DuilibDemo"), UI_WNDSTYLE_FRAME, WS_EX_STATICEDGE | WS_EX_APPWINDOW, 0, 0, 800, 600); m_pFrame->CenterWindow(); ::ShowWindow(*m_pFrame, SW_SHOW); } void TestApp::RequestQuit() { wkeShutdown(); Quit(); } void TestApp::OnQuit() { }
怎么样,是不是非常easy?DuiApplicationBase之所以把消息循环封装在里面就是为了处理多线程的事件用的。当然就这么一个玩意并不能保证Duilib能在多线程编程中游刃有余。skillcore相同还为线程做了封装,对消息循环也做了封装,同一时候为了简化多线程编程时复杂的内存管理,对内存,指针相同做了封装,源代码主要借鉴于boost库和Juce库,同一时候还有文件,输入输出流,原子锁,相互排斥锁等一堆辅助的东西。
刚才说道异步调用函数,弄自己定义消息灰常麻烦。为了解决问题,skillcore提供了一个MsgPump的类,用这个对象能够直接在其它线程中使用主线程调用函数。以下我们看一下详细是如何使用的。以下的代码是一个窗体关闭前的一个“收敛”动作的线程动画,类似于酷狗关闭时的效果。
class WindowDestroyThread : public Thread { public: WindowDestroyThread() :Thread(L"DestroyThread") { } ~WindowDestroyThread() { Stop(4000); } void run() { while (!IsShouldExit()) { if (MsgPump::GetInstance()->IsShouldStop()) break; B_Function<int(WindowDestroyThread*)> f = B_Bind(WindowDestroyAnimation,_1); if (!MsgPump::GetInstance()->CallFun(f,this)) { static_cast<TestApp*>(DuiApplicationBase::GetInstance())->RequestQuit(); } Sleep(10); } } LEAKED_THE_CLASS(WindowDestroyThread) }; int WindowDestroyAnimation(WindowDestroyThread* t) { TestApp * app = static_cast<TestApp*>(DuiApplicationBase::GetInstance()); CMainFrame* p = app->m_pFrame; RECT rect; ::GetWindowRect(*p,&rect); CDuiRect r(rect); if (r.GetHeight()>100) { MoveWindow(*p,rect.left,rect.top+15,r.GetWidth(),r.GetHeight()-30,false); return 1; } else return 0; } void CMainFrame::Notify(TNotifyUI& msg) { if (_tcsicmp(msg.sType,_T("windowinit"))==0) { MoveWindow(*this,0,0,900,100,false); CenterWindow(); initThread = new WindowInitThread; initThread->Start(); CWkeBrowserUI* pBrowser = (CWkeBrowserUI*)m_PaintManager.FindControl(L"wke"); pBrowser->LoadUrl(L"www.baidu.com"); }else if (_tcsicmp(msg.sType,_T("click")) == 0) { if (_tcsicmp(msg.pSender->GetName(),_T("btn_close")) == 0) { destroyThread = new WindowDestroyThread(); destroyThread->Start(); } } }
使用时,先把须要调用的函数,绑定成B_Funtion(boost::function)对象,然后使用MsgPump的单例对象的CallFun函数,去调用就可以,第一个參数传递的是绑定好的B_Function对象,后面依次是被调用函数的參数。眼下最多支持4个參数,假设须要很多其它的话,能够自己去扩展,由于每加一个參数都须要写一段反复性代码,Skilla已经写了4个,实在是不愿再写了。
还有里面封装了一堆智能指针,相信这些应该不用讲大家也应该知道怎么用。有ScopePtr自生自灭类型,SharedPtr无根强引用型,WeakPtr无根弱引用型,SharedObjectPtr有根强引用型,ComSmartPtr调用Com接口用的智能指针。
Threads里面还封装了多线程中经常使用到的临界区,自旋锁,事件等加锁机制,使用变得更简单。
讲了这么多,相信大家都已经手痒了,有一种跃跃欲试的感觉。不要急,Skilla已经把源代码贡献出来了,附带一个窗体动画的Demo,链接地址在以下。炫酷的线程动画你也能够轻松实现。
skillcore和測试Demo下载链接(里面的CWkeBrowser控件也是用线程来刷新的哦,比曾经使用定时器要流畅)
因为时间关系,代码构建的比較仓促,可能库本身还存在bug以及不足,假设发现请联系Skilla(QQ:848861075),谢谢!