一、Duilib不能开发多线程程序?
记得很久以前就听有人说过Duilib的多线程支持性不好,原因是Duilib里面的控件是用数组管理的全局变量,不能进行多线程访问,加锁很麻烦。其实这个说法是非常不合理的,以至于在开发多线程程序时直接将Duilib拒之门外。当然使用Duilib里面开发多线程是木有任何问题的,不要单纯地认为:其他的界面库就能使用多个线程同时操作一个控件,Duilib就不行。事实证明,这点MFC做不到,WinForm也做不到,连微软自己都搞不定的东西,不能算Duilib的缺陷。
二、UI线程与工作线程
凡是开发过Windows界面的人应该都知道,先要做出稳定并且没有闪烁的界面,所有的绘制代码,必须是顺序执行的,画完一个再画一个,而且绘制代码都要写在一个位置,WM_PAINT里面(当然你写在外面也没问题,但还要处理擦除,一般不这么干)。所以,所有的绘制都是异步执行的,想绘制什么,就得先保存下来,使用Invalidate()去异步触发WM_PAINT,待WM_PAINT消息处理时,再去绘制。就这一点就决定了,绘制UI多线程是行不通的,所以所有的绘制操作都是消息循环所在的主线程完成的。既然多线程不能用于绘制,那么多个线程同时访问同一个控件的属性怎么样?(比如,有一个按钮A,一个线程执行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()
{
}
怎么样,是不是很简单?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),谢谢!