您的位置:首页 > 编程语言

(转帖)使用多线程编程的几个常见问题- -

2008-02-18 14:00 330 查看

使用多线程编程的几个常见问题- -

对于一个多线程编程高手来说,没有两难的选择,因为不论工作线程还是用户界面线程,其本质是完全相同的。所不同的只是用户界面线程为用户增加了消息循环机制,而使用工作线程,你自己也很容易能做到这点。由于增加了消息队列,用户界面线程可以与窗口类协同工作,并方便地使用各类消息处理;但相对工作线程要占用更多的系统资源,对于一个使用大量线程的应用,你在使用线程前还是得考虑一下选择使用哪种线程。

1、工作线程还是用户界面线程?
对于一个多线程编程高手来说,没有两难的选择,因为不论工作线程还是用户界面线程,其本质是完全相同的。所不同的只是用户界面线程为用户增加了消息循环机制,而使用工作线程,你自己也很容易能做到这点。由于增加了消息队列,用户界面线程可以与窗口类协同工作,并方便地使用各类消息处理;但相对工作线程要占用更多的系统资源,对于一个使用大量线程的应用,你在使用线程前还是得考虑一下选择使用哪种线程。
对于没有界面交互的后台工作而言(打印,存盘,收发网络信息),使用工作线程是理所当然。但一些线程工作并非完全后台性质的,例如,收发网络信息,但同时还要将信息发送到用户窗口显示出来。因而与用户界面有交互,此时是继续使用工作线程做这项工作呢,还是使用用户界面线程?这就要看后台工作与前台工作(界面交互)的重要性和比重了。对于一个以收发网络信息为中心的线程而言,用户信息交互只占用了其很少的处理时间,因而没有必要为此多占用系统资源,选择工作线程仍然是明智的。而对于一个大型游戏软件或绘图软件而言,情况有所不同,可能很多窗口都需要在很短时间内处理用户的不同指令,并且窗口之间也有信息交互,这时采用用户界面线程更合适些。
因而归纳一下可以确定以下线程使用原则:
1) 首先区分工作是前台还是后台,后台工作尽量采用工作线程;
2) 前台、后台都有的工作需分析各自所占比例和重要性,后台工作占优的尽量使用工作线程;
3) 以前台工作为主,侧重用户操作响应的工作应考虑使用用户界面线程;
4) 线程与线程之间有较多信息交互,且每个线程都与窗口类相关联,可以考虑使用用户界面线程;
如果遵循以上原则,则不难发现,由于大型的有着复杂窗口及事务处理的应用程序不多,因而使用用户界面线程的机会相对较少;我们开发的大多数多线程应用程序都只是采用工作线程来处理那些耗时的后台工作。

2、跨线程对象访问问题
可能初学者没有意识到,应用程序中创建的对象并非是整个进程共享的。在Windows应用程序体系中,对象是线程"私有"的(也就是调用对象方法不能超出线程边界)!也就是说,A线程创建了B对象,则B对象归A线程所有,A线程负责为B对象处理所有的消息(如果有的话)收发工作并交给B的消息处理函数或发到其他窗口对象去,线程结束时负责释放B对象。由于通常的应用程序创建对象及调用对象方法都是在主线程中进行的,因而不存在什么问题;但如果一个应用程序的多个线程需要操作同一个对象时,情况就会大不一样了。一个常见的多线程示例是:A线程(通常是主线程)创建一个对话框B,创建C线程负责处理后台工作,并将处理进展情况报告给B(例如在B上显示一些文字或图形信息或更新B的显示界面等)。初学者往往会犯这样的错误:将B对象的指针传递给C线程的线程函数,让C线程直接调用B对象的方法添加信息或更新显示界面。这样做对于一个简单的窗口类(如显示处理进度的对话框)而言,有时确实也成功了;但这要冒很大的潜在风险,那就是这种处理方法有时会莫名其妙的出错(通常是访问冲突导致的)!而对于一个复杂的类对象,采用类似的处理方法出现问题的可能性会大大增加,往往根本不能工作。那么,究竟应该怎样处理这类多线程访问同一对象问题呢?

图1 两个线程同时访问某对象引起访问冲突
有两种不同的方法可以解决这个问题:一是做好线程间的同步工作,让不同的线程访问同一对象时有先有后,避免冲突;另一个方法是使用消息机制来访问对象,由于通过同一消息队列来访问对象,也就不存在冲突问题了。同步方法是在对象中添加同步对象,如CriticalSection,Mutex,Semaphore等对象,某个线程要访问对象,首先必须获取该同步对象。由于同步对象在某一时刻只能允许一个线程(Semaphore允许几个线程同时运行)运行,其他没有获得同步对象的线程只能等待直到那些正在运行的线程释放该同步对象。这就避免了多线程同时修改对象数据造成的混乱。

图2 使用同步对象使两线程依次访问对象(蓝色先访问)
使用消息机制同步对象访问对Windows应用程序而言更合理,因为消息是无连接的(直接方法调用是有连接处理行为),它可以有效避免某个线程运行过程中产生的错误影响其他线程的运行。

图2 使用消息队列使两线程依次访问对象(蓝色先访问)
3、如何结束线程运行
在一些应用程序中,除非用户去结束应用程序的运行,否则其中的子线程会一直处于运行状态。如果应用程序在结束时不主动通知子线程退出,有可能导致主线程结束后,子线程的系统资源得不到释放。如何通知子线程结束运行呢?这需要在程序设计阶段就考虑到,通常可以用事件、消息或全局变量来通知子线程退出运行循环或消息循环。消息通知方式适合用来通知有消息循环的用户界面线程退出,但理论上讲消息机制是不可靠的(消息可能会丢失,尽管可能性很小);事件通知的方式及时并且可靠,在应用程序准备结束运行前,设置一个通知事件;子线程的运行循环中不断检查通知事件,一旦发现事件信号设置,就可以退出循环,从而结束子线程的运行。主线程在设置通知事件后,不能马上退出,而应该等待子线程先结束后再退出。等待子线程结束方法很多,例如使用循环(while或WM_CLOSE消息循环)检测线程状态等待,WaitForSingleObject函数等待,还有Sleep函数等待等,但有些方法在使用时可能导致线程死锁,因而必须小心。
举例来说,一个子线程不断将自己的处理进度刷新到对话框的进度条控件,这里刷新对话框事实上有赖于主线程的消息循环来处理刷新消息;如果用户不等处理结束就点击关闭对话框按钮,而主线程设置通知事件后如果使用WaitForSingleObject函数等待子线程结束的话,极可能导致线程死锁。因为WaitForSingleObject函数会将主线程挂起(任何消息都得不到处理)直到子线程结束事件发生;而子线程可能正在设置进度条,一直在等待主线程将刷新消息处理完毕返回才会检测通知事件。于是,死锁发生了,应用程序"死"在那里无法自动结束,必须靠任务管理器将其强制结束(可能造成该程序的资源和内存得不到释放)。
采用循环检测的方法比较安全些,但while循环本身可能会导致主线程乃至子线程阻塞,子线程也因得不到及时调度而迟迟难以结束,应用程序界面可能半天都没有响应。当子程序结束需要较长时间时,这是很令人厌恶的。不过,可以通过在while循环中加入Sleep(10~50)主动释放CPU占用来促使子线程得到调度,也可以通过调高子线程的优先级来促使其尽快结束。而通过在WM_CLOSE消息处理函数中检测子线程状态的方法最值得推荐,该方法既不会挂起主线程,也不会阻塞子线程。检测如果发现子线程仍然在活动状态,就再此向主窗口发送一个WM_CLOSE消息,以便再次进入该消息处理函数检查子线程状态,直到子线程结束为止。
下面是等待线程结束的示例代码:

void CProgressDlg::OnClose()
{
DWORD dwStatus;

if (m_pWorkerThread != NULL)
{
VERIFY(::GetExitCodeThread(m_pWorkerThread ->m_hThread, &dwStatus));
if (dwStatus == STILL_ACTIVE)
{
::SetEvent(g_hEventKill); // 通知子线程结束运行
// 调高子线程优先级
m_pWorkerThread->SetThreadPriority(THREAD_PRIORITY_ABOVE_NORMAL);
PostMessage(WM_CLOSE); // 给本窗口再发一个结束消息
Return;
}
else
{
delete m_pWorkerThread;
m_pWorkerThread = NULL;
}
}

CDialog::OnClose();
}

CDialog::OnClose();
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: