让Duilib多线程编程更easy

         一、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),谢谢!




郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。