摘 要:本文简单介绍了Windows环境下进行多线程编程的意义,重点讨论了C++Builder环境下开发多线程应用程序这一问题,并通过实现生产者-消费者问题,帮我们更好地理解同步概念及其实现方法。
关键词:多线程;同步;生产者-消费者;C++Builder
线程之可行性
在很多情况下,可能需要为程序创建线程。这里给出其中一些可能性:
(1)如果创建的是一个多文档接口(Multiple Document Interface,MDI)程序,那么为每个窗口分配一个线程就显得十分重要了,例如,对于一个通过多个Modem同时连接到多个主机的MDI通信程序而言,如果每个窗口都有它自己的线程来和一个主机通信,那么整个事情就简化很多。
(2)如果使用的是一台有多个处理器的机器,并希望充分利用所有可能获得的CPU资源,那么就需要将应用程序分解成多个线程。Windows2000中CPU的划分单位为线程。因此,如果程序只包含一个线程,那么,默认环境下该程序只能使用其中一个CPU。但是,如果将此程序划分为多个线程,那么Windows2000就可以在不同的CPU上运行各个线程。
(3)在后台运行的某些任务的同时,要求用户还可以继续使用应用程序进行工作。利用线程很容易实现这点。例如:可以将一些冗长的重算、页面格式化操作、文件的读写等活动都放在单独的线程中,使其在后台运行,而不会对用户造成影响。
同步
撰写多线程程序的一个最具挑战性的问题就是:如何让一个线程和另一个线程合作。这引出了一个非常重要的问题:同步。所谓同步是指进程、线程间相互通信时避免破坏各自数据的能力。Windows环境下的同步问题是由Win32系统的CPU时间片分配方式引起的。虽然在某一时刻,只有一个线程占用CPU(单CPU)时间,但是无法知道在什么时候,在什么地方线程被打断,这样如何保证线程之间不破坏彼此的数据就显得格外重要。同步问题是如此重要,也相当有趣,因而吸引了不少学者对他进行研究,由此产成了一系列经典的进程同步问题,其中较有代表性的是"生产者-消费者问题"、"读者-写者问题""哲学家进餐问题"等。在此,本文简要讨论了C++Builder平台下如何利用多线程编程技术实现"生产者-消费者"问题,帮助我们更好得理解同步概念及其实现方法。
生产者-消费者问题
生产者-消费者问题是一个著名的进程同步问题。它描述的是:有一群生产者进程在生产消息,并将此消息提供给消费者进程去消费。为使生产者进程和消费者进程能并发进行,在他们之间设置了一个具有N个缓冲区的缓冲池,生产者进程可以将它所生产的消息放入一个缓冲区中,消费者进程可以从一个缓冲区中取得一个消息消费。尽管所有的生产者进程和消费者进程都是以异步方式进行的,但他们之间必须保持同步,即不允许消费者进程到一个空的缓冲区中去取消息,也不允许生产者进程向一个已装满消息且尚未被取走消息的缓冲区中投放消息。
C++Builder多线程应用程序编程基础
1、使用C++Builder提供的TThread类
VCL类库提供了用于线程编程的TThread类。在TThread类中封装了Windows中关于线程机制的WindowSAPI。对于大多数的应用程序来说,可在应用程序中使用线程对象来表示执行线程。线程对象通过封装使用线程所需的内容,简化了多线程应用程序的编写。注意,线程对象不允许控制线程堆栈的大小或其安全属性。若需要控制这些,必须使用WindowsAPI的Create Thread()或Begin Thread()函数。
TThread类有以下一些属性和方法:
1) 属性:
·Priority:优先级属性。可以设置线程的优先级。
·Return Value:返回值属性。当线程介绍时返回给其他线程一个数值。
·Suspended:挂起属性。可以判断线程是否被挂起。
·Terminated:结束属性。用来标志是否应该结束线程。
·ThreadID:标识号属性。在整个系统中线程的标识号。使用Windows API函数时该属性非常有用。
2) 方法:
·Do Terminate:产生一个On Terminate事件,但是不结束线程的执行。
·Resume:唤醒一个线程继续执行。
·Suspend:挂起一个线程,要与Resume过程成对使用。
·Synchronize:由主VCL线程调用的一个同步过程。
·Terminate:将Terminate属性设置为True,中止线程的执行。
·Wait For:等待线程的中止并返回Return Value属性的数值。
2、协调线程
在编写线程执行时运行的代码时,必须考虑到可能同步执行的其他线程的行为。特别注意,避免两个线程试图同时使用相同的全局对象或变量。另外,一个线程中的代码会依赖其他线程执行任务的结果。
1) 避免同时访问
为避免在访问全局对象或变量时与其他线程发生冲突,可能需要暂停其他线程的执行,直到该线程代码完成操作。
(1)锁定对象。一些对象内置了锁定功能,以防止其他线程使用该对象的实例。例如,画布对象(TCanvas及其派生类)有一种Lock()函数可以防止其他线程访问画布,直到调用Unlock()函数。显然,这种方法只对部分类有效。
(2)使用重要区段。若对象没有提供内置的锁定功能,可使用重要区段。重要区段像门一样,每次只允许一个线程进入,要使用重要区段,需创建TCriticalSection的全局实例。TCriticalSection有两个函数:Acquire()(阻止其他线程执行该区域)及Release()(取消对其他线程的阻止)。
(3)使用多重读、独占写的同步器。当使用重要区段来保护全局内存时,每次只有一个线程可以使用该内存。这种保护可能会超出了需要,特别是有一个经常读但很少写的对象或变量时更是如此。多个线程同时读相同内存但没有线程写内存是没有危险的。当有一些经常被读,但是很少写的全局变量时,可用TMultiReadExclusiveWriteSynchronizer对象保护它。这个对象和重要区段一样,但它允许多个线程同时读,只要没有线程写即可。每个需要读内存的线程首先要调用Begin Read()函数(确保当前无其他线程写内存),线程完成对保护内存读操作后,要调用End Read()函数。任何线程需要写保护内存必须调用Begin Write()函数(确保当前无其他线程读或写内存),完成对保护内存写操作后,调用End Write()函数。
(4)使用Synchronize函数:Void __fast call Synchronize (TThreadMethod &Method);
其中参数Method为一个不带参数的过程名。在这个不带参数的过程中是一些访问VCL的代码。我们可以在Execute过程中调用Synchronize过程来避免对VCL的并发访问。程序运行期间的具体过程实际上是由Synchronize过程来通知主线程,然后主线程在适当的时机来执行Synchronize过程的参数列表中的那个不带参数的过程。在多个线程的情况下,主线程将Synchronize过程发过来的通知放到消息队列中,然后逐个地响应这些消息。通过这种机制Synchronize实现了线程之间地同步。
2) 等待其他线程
若线程必须等待另一线程完成某项任务,可让线程临时中断执行。然后,要么等待另一线程完全执行结束,要么等待另一线程通知完成了该任务。
(1)等待线程执行结束
要等待另一线程执行结束,使用它地Wait For()函数。Wait For函数直到那个线程终止才返回,终止的方式要么完成了其Execute()函数,要么由于一个异常。
(2)等待任务完成。有时,只需要等待线程完成一些操作而不是等待线程执行结束。为此,可使用一个事件对象。事件对象(TEvent)应具有全局范围以便他们能够为所有线程可见。当一个线程完成一个被其他线程依赖的操作时,调用TEvent::Set Event()函数。Set Event发出一个信号,以便其他线程可以检查并得知操作完成。要关掉信号,则使用Reset Event()函数。
例如,当必须等待若干线程完成其执行而不是单个线程时。因为不知道哪个线程最后完成,也就不能对某个线程使用Wait For()函数。此时,可通过调用Set Event以在线程结束时累加计数值并在最后一个线程结束时发出信号以指示所有线程结束。
多线程应用程序编程实例
下面是一个实现"生产者-消费者问题"的多线程应用实例。在此例中,我们按上面介绍的方法构造了两个TThread的子类TProducerThread(生产者线程)和TCustomerThread(消费者线程),生产和消费的商品仅仅是一个整数。在协调生产和消费的过程中,重要区段(TCriticalSection)和事件(TEvent)得到了应用。生产者通过TEvent类的对象Begin Consume来通知消费者开始消费,而消费者通过TEent类的对象Begin Produce通知生产者开始生产。程序中共有两个生产者,一个消费者。在两个生产者之间,通过TCriticalSection类的对象同步。其运行界面如图1所示。
图1 程序运行效果 |
主要源程序如下所示:
生产者线程:
Void __fast call TProducerThread:: Execute () { //---- Place thread code here ---- Int i = 0; Int j; while(i<100) //每个生产者线程生产100个商品 { Sleep(1000);//延迟,为清楚得显示执行效果 if(Form1->buffer_size > 0)//缓冲池不空,通知消费者消费 { Form1->Begin Consumer->Set Event (); } Form1->Produce Guard->Acquire (); i++; StrResult = IntToStr (i); J = Form1->buffer_size; Form1->Product [j] = i; Form1->buffer_size++; Synchronize(Show Result);//刷新界面,显示最新生产-消费状况 Form1->Begin Consumer->Set Event();//通知消费者消费 if(Form1->buffer_size == 5)//缓冲池满,挂起生产者线程,直到通知再生产 { Form1->Begin Produce->Wait For (INFINITE); } Sleep (1000); Form1->Produce Guard->Release (); } While (Form1->buffer_size > 0) { Form1->Begin Consumer->Set Event (); } } |
消费者线程:
Void __fast call TConsumerThread::Execute() { //---- Place thread code here ---- Int j; For (int i = 0;i < 200;i++) { Sleep(100); //延迟,为清楚得显示执行效果 Form1->Begin Consumer->Wait For(INFINITE);//挂起消费者线程,直到通知再消费 J = Form1->buffer_size - 1; StrResult = IntToStr (Form1->Product [j]); Form1->buffer_size--; Synchronize(Show Result); //刷新界面,显示最新生产-消费状况 if(Form1->buffer_size == 4)//缓冲池不再full,唤醒由于缓冲池full而挂起的生产者线程 { Form1->Begin Produce->Set Event (); } Sleep (100); } } |
结论
本文讨论了多线程编程及其可行性,说明了在Windows环境下进行多线程编程的意义,并重点讨论了C++Builder平台下如何开发多线程应用程序这一问题,通过实现"生产者-消费者问题"这一著名的进程同步问题,比较清晰地反映了在Windows环境下进行多线程编程技术及其实现的作用和效果。