间时紧张,先记一笔,后续优化与完善。
简介
本文讨探基本的步同念概,并现实着手帮助新手握掌多线程程编。本文的重点在各种步同巧技。
基本念概
在线程执行过程当中,或多或少都要需彼此互交,种这互交行为有多种形式和类型。例如,一个线程在执行完它被予赋的任务后,通知另一个线程任务经已成完。然后第二个线程做开始剩下的任务。
下述对象是用来支撑步同的:
1)信号量
2)互斥锁
3)症结区域
4)件事
每一个对象都有不同的目标和用处,但基本目标都是支撑步同。当然还有其他可以用来步同的对象,比如进程和线程对象。后两者的应用由程序员决议,比如说判断一个给定进程或线程是不是执行毕完为了应用进程和线程对象来行进步同,我们一般应用Wait*函数,在应用这些函数时,你该应晓得一个念概,任何被作为步同对象的内核对象(症结区域除外)都处于两种态状之一:通知态状和未通知态状。例如,进程和线程对象,当他们开始执行时处于未通知态状,而当他们执行毕完时处于通知态状,
为了判断一个给定进程或线程是不是经已结束,我们必须判断示表其的对象是不是处于通知态状,而要到达这样的目标,我们要需应用Wait*函数。
Wait*函数
面上是最简略的Wait*函数:
DWORD WaitForSingleObject
(
HANDLE hHandle,
DWORD dwMilliseconds
);
数参hHandle示表待查检其态状(通知或者未通知)的对象,dwMilliseconds示表用调线程在被查检对象进入其通知态状前该应等待的间时。若对象处于通知态状或指定间时过去了,这个函数返回制控权给用调线程。若dwMilliseconds置设为INIFINITE(值为-1),则用调线程会直一等待直到对象态状变成通知,这有可能使得用调线程永久等待下去,致导“饿死”。
例如,查检指定线程是不是正在执行, dwMilliseconds置设为0,是为了让用调线程马上返回。
DWORD dw = WaitForSingleObject(hProcess, 0 );
switch (dw)
{
case WAIT_OBJECT_0:
// the process has exited
break;
case WAIT_TIMEOUT:
// the process is still executing
break;
case WAIT_FAILED:
// failure
break;
}
下一个Wait类函数相似面上的,但它带的是一系列句柄,并且等待其中之一或部全进入已通知态状。
DWORD WaitForMultipleObjects
(
DWORD nCount,
CONST HANDLE * lpHandles,
BOOL fWaitAll,
DWORD dwMilliseconds
);
数参nCount示表待查检的句柄个数,lpHandles指向句柄数组,若fWaitAll为TRUE,则等待有所的对象进入已通知态状,若为FALSE,则当任何一个对象进入已通知态状时,函数返回。dwMilliseconds意思同上。
例如,面上代码判断哪个进程会先结束:
HANDLE h[ 3 ];
h[ 0 ] = hThread1;
h[ 1 ] = hThread2;
h[ 2 ] = hThread3;
DWORD dw = WaitForMultipleObjects( 3 , h, FALSE, 5000 ); // 任何一个进入已通知就返回
switch (dw)
{
case WAIT_FAILED:
// failure
break;
case WAIT_TIMEOUT:
// no processes exited during 5000ms
break;
case WAIT_OBJECT_0 + 0:
// a process with h[0] descriptor has exited
break;
case WAIT_OBJECT_0 + 1:
// a process with h[1] descriptor has exited
break;
case WAIT_OBJECT_0 + 2:
// a process with h[2] descriptor has exited
break;
}
句柄数组中索引号为index的对象进入已通知态状时,函数返回WAIT_OBJECT_0 + 索引号。若fWaitAll为TRUE,则当有所对象进入已通知态状时,函数返回WAIT_OBJECT_0。
一个线程若用调一个Wait*函数,则它从户用式模切换为内核式模。这带来的果后有好有坏。好不的是切换进入内核式模大概要需1000个时钟周期,这消费不算小。好的是当进入内核式模后,就不要需应用处理器,而是进入休眠态,不与参处理器的调度了。
当初让我们进入MFC,并看看它能为我们做些什么。这里有两个类装封了对Wait*函数的用调: CSingleLock和CMultiLock。
步同对象 | 等价的C++类 |
| |
| |
| |
| |
每一个类都从一个类--CSyncObject承继来下,此类最用有的成员是重载的HANDLE运算符,它返回指定步同对象的在内句柄。有所这些类都定义在<AfxMt.h>头文件中。
件事
一般来说,件事于用这样的情况下:当指定的作动生发后,一个线程(或多个线程)才开始执行其任务。例如,一个线程可能等待必须的数据收集完后才开始将其存保到硬盘上。有两种件事:手动重置型和主动重置型。通过应用件事,我们可以松轻地通知另一个线程特定的作动经已生发了。对于手动重置型件事,线程应用它通知多个线程特定作动经已生发,而对于主动重置型件事,线程应用它只可以通知一个线程。在MFC中,CEvent类装封了件事对象(若在win32中,它是用一个HANDLE来示表的)。CEvent的构造函数运行我们选择建创手动重置型和主动重置型件事。认默的建创类型是主动重置型件事。为了通知正在等待的线程,我们可以用调CEvent::SetEvent方法,这个方法将会让件事进入已通知态状。若件事是手动重置型,则件事会坚持已通知态状,直到对应的CEvent::ResetEvent被用调,这个方法将使得件事进入未通知态状。这个特性使得一个线程可以通过一个SetEvent用调去通知多个线程。若件事是主动重置型,则有所正在等待的线程中只有一个线程会接收到通知。当那个线程接收到通知后,件事会主动进入未通知态状。
面上两个例子将展示上述特性:
// create an auto-reset event
CEvent g_eventStart;
UINT ThreadProc1(LPVOID pParam)
{
::WaitForSingleObject(g_eventStart, INFINITE);
return 0;
}
UINT ThreadProc2(LPVOID pParam)
{
::WaitForSingleObject(g_eventStart, INFINITE);
return 0;
}
在这个例子中,一个全局的CEvent对象被建创,当然它是主动重置型的。除此以外,有两个任务线程在等待这个件事对象以便开始其任务。只要第三个线程用调那个件事对象的SetEvent方法,则两个线程中之一(当然没人晓得会是哪个)会接收到通知,然后件事会进入未通知态状,这就止防了第二个线程也失失落件事的通知。
面上来看第二个例子:
// create a manual-reset event
CEvent g_eventStart(FALSE, TRUE);
UINT ThreadProc1(LPVOID pParam)
{
::WaitForSingleObject(g_eventStart, INFINITE);
return 0;
}
UINT ThreadProc2(LPVOID pParam)
{
::WaitForSingleObject(g_eventStart, INFINITE);
return 0;
}
这段代码和面上的稍有不同,CEvent对象构造函数的数参不一样了,但意思上就大不同了,这是一个手动重置型件事对象。若第三个线程用调件事对象的SetEvent方法,则可以保确两个任务线程都市同时(几乎是同时)开始任务。这是因为手动重置型件事在进入已通知态状后,会坚持此态状直到对应的ResetEvent被用调。
除此以外件事对象还有一个方法:CEvent::PulseEvent。这个方法首先使得件事对象进入已通知态状,然后使其退回到未通知态状。若件事是手动重置型,件事进入已通知态状会让有所正在等待的线程失失落通知,然后件事进入未通知态状。若件事是主动重置型,件事进入已通知态状时只会让有所等待的线程之一失失落通知。若没有线程在等待,则用调ResetEvent什么也不干。
实例---任务者线程
本文所带的例子中,作者将展示如何建创任务者线程以及如何合理地销毁它们。作者定义了一个被有所线程应用的制控函数。当点击图视区域时,就建创一个线程。有所被建创的线程应用上述制控函数在图视户客区绘制一个动运的圆形。这里作者应用了一个手动重置型件事,它被用来通知有所任务线程其“死讯”。除此以外,我们将看到如何使得主线程等待直到有所任务者线程销毁失落。
作者将线程函数定义为全局的:
struct THREADINFO
{
HWND hWnd;//主图视区
POINT point;//起始点
} ;
UINT ThreadDraw(PVOID pParam);
extern CEvent g_eventEnd;
UINT ThreadDraw(PVOID pParam)
{
static int snCount = 0;//线程计数器
snCount ++;//计数器递增
TRACE(TEXT("- ThreadDraw %d: started\n"), snCount);
//出取传入的数参
THREADINFO *pInfo = reinterpret_cast<THREADINFO *> (pParam);
CWnd *pWnd = CWnd::FromHandle(pInfo->hWnd);//主图视区
CClientDC dc(pWnd);
int x = pInfo->point.x;
int y = pInfo->point.y;
srand((UINT)time(NULL));
CRect rectEllipse(x - 25, y - 25, x + 25, y + 25);
CSize sizeOffset(1, 1);
//刷子颜色随机
CBrush brush(RGB(rand()% 256, rand()% 256, rand()% 256));
CBrush *pOld = dc.SelectObject(&brush);
while (WAIT_TIMEOUT == ::WaitForSingleObject(g_eventEnd, 0))
{//只要主线程还未通知我自残,续继任务!(注意间时置设为)
CRect rectClient;
pWnd->GetClientRect(rectClient);
if (rectEllipse.left < rectClient.left || rectEllipse.right > rectClient.right)
sizeOffset.cx *= -1;
if (rectEllipse.top < rectClient.top || rectEllipse.bottom > rectClient.bottom)
sizeOffset.cy *= -1;
dc.FillRect(rectEllipse, CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));
rectEllipse.OffsetRect(sizeOffset);
dc.Ellipse(rectEllipse);
Sleep(25);//休眠下,给其他绘制子线程运行的会机
}
dc.SelectObject(pOld);
delete pInfo;//删除数参,止防内存泄漏
TRACE(TEXT("- ThreadDraw %d: exiting.\n"), snCount --);//奉还计数器
return 0;
}
注意作者传入的是一个安全句柄,而不是一个CWnd指针,并且在线程函数中通过传入的句柄建创一个临时的C++对象并应用。这样就防止了在多线程程编中多个对象引用单个C++对象的险危。
CArray < CWinThread * , CWinThread *> m_ThreadArray; // 存保CWinThread对象指针
// manual-reset event
CEvent g_eventEnd(FALSE, TRUE);
void CWorkerThreadsView::OnLButtonDown(UINT nFlags, CPoint point)
{
THREADINFO *pInfo = new THREADINFO;//线程数参
pInfo->hWnd = GetSafeHwnd();//图视口窗
pInfo->point = point;//前当点
//将界面作为数参传入线程中,就能够在线程中自己更新主界面,而不用去通知主线程更新界面
CWinThread *pThread = AfxBeginThread(ThreadDraw, (PVOID) pInfo, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);//建创线程,初始态状为挂起
pThread->m_bAutoDelete = FALSE;//线程执行毕完后不主动销毁
pThread->ResumeThread();//线程开始执行
m_ThreadArray.Add(pThread);//存保建创的线程
}
每日一道理
有些冷,有些凉,心中有些无奈,我一个人走在黑夜中,有些颤抖,身体瑟缩着,新也在抖动着,我看不清前方的路,何去何从,感觉迷茫,胸口有些闷,我环视了一下周围,无人的街头显得冷清,感到整个世界都要将我放弃。脚步彷徨之间,泪早已滴下……
为了合理地销毁有所线程,首先使得件事进入已通知态状,这会通知任务线程“死期已至”,然后用调WaitForSingleObject让主线程等待有所的任务者线程完整销毁失落。注意每次迭代时用调WaitForSingleObject会致导从户用式模进入内核式模。例如,10此迭代会费浪失落大约10000次时钟周期。为了防止这个题问,我们可以应用
WaitForMultipleObjects
。这就是第二种方法。
void CWorkerThreadsView::OnDestroy()
{
g_eventEnd.SetEvent();
/**//* // 第一种方式
for (int j = 0; j < m_ThreadArray.GetSize(); j ++)
{
::WaitForSingleObject(m_ThreadArray[j]->m_hThread, INFINITE);
delete m_ThreadArray[j];
}
*/
//第二种方式
int nSize = m_ThreadArray.GetSize();
HANDLE *p = new HANDLE[nSize];
for (int j = 0; j < nSize; j ++)
{
p[j] = m_ThreadArray[j]->m_hThread;
}
::WaitForMultipleObjects(nSize, p, TRUE, INFINITE);
for (int j = 0; j < nSize; j ++)
{
delete m_ThreadArray[j];
}
delete [] p;
TRACE("- CWorkerThreadsView::OnDestroy: finished!\n");
}
症结区域
和其他步同对象不同,除非有要需以外,症结区域任务在户用式模下。若一个线程想运行一个装封在症结区域中的代码,它首先做一个旋转封锁,然后等待特定的间时,它进入内核式模去等待症结区域。现实上,症结区域持有一个旋转计数器和一个信号量,前者于用户用式模的等待,后者于用内核式模的等待(休眠态)。在Win32API中,有一个CRITICAL_SECTION结构体示表症结区域对象。在MFC中,有一个类CCriticalSection。症结区域是这样一段代码,当它被一个线程执行时,必须保确不会被另一个线程中断。
一个简略的例子是多个线程共用一个全局变量:
int g_nVariable = 0 ;
UINT Thread_First(LPVOID pParam)
{
if (g_nVariable < 100)
{
}
return 0;
}
UINT Thread_Second(LPVOID pParam)
{
g_nVariable += 50;
return 0;
}
这段代码不是线程安全的,因为没有线程对变量g_nVariable是独占应用的。为了解决这个题问,可以如下应用:
CCriticalSection g_cs;
int g_nVariable = 0 ;
UINT Thread_First(LPVOID pParam)
{
g_cs.Lock();
if (g_nVariable < 100)
{
}
g_cs.Unlock();
return 0;
}
UINT Thread_Second(LPVOID pParam)
{
g_cs.Lock();
g_nVariable += 20;
g_cs.Unlock();
return 0;
}
这里应用了CCriticalSection类的两个方法,用调Lock函数通知系统面上代码的执行不能被中断,直到相同的线程用调Unlock方法。系统会首先查检被系统症结区域封锁的代码是不是被另一个线程捕获。若是,则线程等待直到捕获线程释放失落症结区域。
若有多个共享资源要需保护,则最好为每一个资源应用一个单独的症结区域。记得要配对应用UnLock和Lock。还有一点是要需止防“死锁”。
class CSomeClass
{
CCriticalSection m_cs;
int m_nData1;
int m_nData2;
public:
void SetData(int nData1, int nData2)
{
m_cs.Lock();
m_nData1 = Function(nData1);
m_nData2 = Function(nData2);
m_cs.Unlock();
}
int GetResult()
{
m_cs.Lock();
int nResult = Function(m_nData1, m_nData2);
m_cs.Unlock();
return nResult;
}
} ;
互斥锁
和症结区域相似,互斥锁设计为对步同访问共享资源行进保护。互斥锁在内核中实现,因此要需进入内核式模操纵它们。互斥锁不仅能在不同线程之间,也可以在不同进程之间进程步同。要跨进程应用,则互斥锁该应是有名的。MFC中应用CMutex类来操纵互斥锁。可以如下方式应用:
CSingleLock singleLock( & m_Mutex);
singleLock.Lock(); // try to capture the shared resource
if (singleLock.IsLocked()) // we did it
{
// use the shared resource
// After we done, let other threads use the resource
singleLock.Unlock();
}
或者通过Win32函数:
// try to capture the shared resource
::WaitForSingleObject(m_Mutex, INFINITE);
// use the shared resource
// After we done, let other threads use the resource
::ReleaseMutex(m_Mutex);
我们可以应用互斥锁来限制应用程序的运行实例为一个。可以将如下代码放置到InitInstance函数(或WinMain)中:
HANDLE h = CreateMutex(NULL, FALSE, " MutexUniqueName " );
if (GetLastError() == ERROR_ALREADY_EXISTS)
{//互斥锁经已存在
AfxMessageBox("An instance is already running.");
return(0);
}
信号量
为了限制应用共享资源的线程数目,我们该应应用信号量。信号量是一个内核对象。它存储了一个计数器变量来跟踪应用共享资源的线程数目。例如,面上代码应用CSemaphore类建创了一个信号量对象,它保确在给定的间时间隔内(由构造函数第一个数参指定)最多只有5个线程能应用共享资源。还假定初始时没有线程获得资源:
CSemaphore g_Sem( 5 , 5 );
一旦线程访问共享资源,信号量的计数器就减1.若变成0,则接来下对资源的访问会被拒绝,直到有一个持有资源的线程离开(也就是说释放了信号量)。我们可以如下应用:
// Try to use the shared resource
::WaitForSingleObject(g_Sem, INFINITE);
// Now the user's counter of the semaphore has decremented by one
// Use the shared resource
// After we done, let other threads use the resource
::ReleaseSemaphore(g_Sem, 1 , NULL);
// Now the user's counter of the semaphore has incremented by one
主从线程之间的通信
若主线程想通知从线程一些作动的生发,应用件事对象是很方便的。但反过来却是低效,不方便的。因为这会让主线程停来下等待件事,进而降低了应用程序的响应速度。作者提出的方法是让从线程发自定义消息给父线程。
#define WM_MYMSG WM_USER + 1
这只能保证口窗类中唯一,但为了保确整个应用程序中唯一,更为安全的方式是:
#define WM_MYMSG WM_APP + 1
afx_msg LRESULT OnMyMessage(WPARAM , LPARAM );
LRESULT CMyWnd::OnMyMessage(WPARAM wParam, LPARAM lParam)
{
// A notification got
// Do something
return 0;
}
BEGIN_MESSAGE_MAP(CMyWnd, CWnd)
ON_MESSAGE(WM_MYMSG, OnMyMessage)
END_MESSAGE_MAP()
UINT ThreadProc(LPVOID pParam)
{
HWND hWnd = (HWND) pParam;
// notify the primary thread's window
::PostMessage(hWnd, WM_MYMSG, 0, 0);
return 0;
}
但这个方法有个很大的缺陷--内存泄漏,作者没有深入研究,可以参考我这篇文章《浅谈一个线程通信代码的内存泄漏及解决方案 》
作者:洞庭散人
本博客遵从 Creative Commons Attribution 3.0 License,若于用非商业目标,您可以自由转载,但请保留原作者信息和文章链接URL。
文章结束给大家分享下程序员的一些笑话语录: IT业众生相
第一级:神人,天资过人而又是技术狂热者同时还拥有过人的商业头脑,高瞻远瞩,技术过人,大器也。如丁磊,求伯君。
第二级:高人,有天赋,技术过人但没有过人的商业头脑,通常此类人不是顶尖黑客就是技术总监之流。
第三级:牛人,技术精湛,熟悉行业知识,敢于创新,有自己的公司和软件产品。
第四级:工头,技术精湛,有领导团队的能力,此类人大公司项目经理居多。
第五级:技术工人,技术精湛,熟悉行业知识但领导能力欠加,此类人大多为系分人员或资深程序员,基本上桀骜不逊,自视清高,不愿于一般技术人员为伍,在论坛上基本以高手面目出现。
第六级:熟练工人,技术有广度无深度,喜欢钻研但浅尝辄止。此类人大多为老程序员,其中一部分喜欢利用工具去查找网上有漏洞的服务器,干点坏事以获取成绩感。如果心情好,在论坛上他们会回答菜鸟的大部分问题。此级别为软件业苦力的重要组成部分。
第七级:工人,某些技术较熟练但缺乏深度和广度,此类人大多为程序员级别,经常在论坛上提问偶尔也回答菜鸟的问题。为软件产业苦力的主要组成部分。
第八级:菜鸟,入门时间不长,在论坛上会反复提问很初级的问题,有一种唐僧的精神。虽然招人烦但基本很可爱。只要认真钻研,一两年后就能升级到上一层。
第九级:大忽悠,利用中国教育的弊病,顶着一顶高学历的帽子,在小公司里混个软件部经理,设计不行,代码不行,只会胡乱支配下属,拍领导马屁,在领导面前胡吹海侃,把自己打扮成技术高手的模样。把勾心斗角的办公室文化引入技术部门,实在龌龊!
第十级:驴或傻X,会写SELECT语句就说自己精通ORALCE,连寄存器有几种都不知道就说自己懂汇编,建议全部送到日本当IT产业工人,挣了日本人的钱还严重打击日本的软件业!