第1920讲windows线程同步技术信号量使用_第1页
第1920讲windows线程同步技术信号量使用_第2页
第1920讲windows线程同步技术信号量使用_第3页
第1920讲windows线程同步技术信号量使用_第4页
第1920讲windows线程同步技术信号量使用_第5页
已阅读5页,还剩28页未读 继续免费阅读

付费下载

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

中科研CASoft软件工程师培训讲义Game

Master游戏修改工具第19~20讲Windows线程同步技术信号量的使用中科天地软件人才培训中心Created

by本讲重点提要¤

本讲介绍用于线程同步的另一内核对象:信号量(Semaphore)对象,在介绍了

信号量对象的特性及如何使用之后,利

用信号量对象实现一个很有用处的生产

者消费者模型。线程同步:信号量¤

信号量(Semaphore)内核对象用于对资源进

行计数。它们使线程能够查询可以资源的数目,如果存在一个或多个资源时,线程就得以继续

执行,而可用资源的计数就被减少。即当从信

号量请求一个资源时,操作系统查看资源是否

可用,并减少可用资源的计数而不允许其它线

程干涉,只有在资源计数被减少后,系统才允

许另一个线程从信号量请求资源。线程同步:信号量¤

信号量与所有内核对象一样,包含一个使用数

量,但是它们也包含另外两个带符号的32位值,一个是最大资源数量,一个是当前资源数量。最大资源数量用于标识信号量能够控制的资源的最大数量。当前资源数量则用于标识当前可以使用的资源的数量。¤

不同于临界区和互斥量,信号量不能被认为是属于某个线程的,一个线程可以等待信号量对象(减少它的资源计数),而另一个线程可以释放该对象(增加它的资源计数)。线程同步:信号量¤

信号量的使用规则如下:如果当前资源的数量大于0,则信号量为有信号状态(Signaled)。如果当前资源数量是0,则信号量为无信号状态(Nonsignaled)。系统决不允许当前资源的数量为负值。当前资源数量决不能大于最大资源数量。当使用信号量时,不要将信号量对象的使用数量与它的当前资源数量混为一谈。线程同步:信号量¤

信号量创建时,当前资源数量被初始化为0,因此不发出信号量信号。等待信号量的所有线程均进入等待状态。¤

通过调用等待函数,传递负责保护资源的信号

量的句柄,线程就能够获得对该资源的访问权。从内部来说,该等待函数要检查信号量的当前

资源数量,如果它的值大于0(信号量已经发

出信号),那么计数器递减1,调用线程保持

可调度状态。信号量的出色之处在于它们能够

以原子操作方式来执行测试和设置操作。线程同步:信号量¤

当向信号量申请一个资源时,操作系统就要检查是否有这个资源可供使用,同时将可用资源的数量递减,而不让另一个线程加以干扰。¤

只有当资源数量递减后,系统才允许另一个线程申请对资源的访问权。如果该等待函数确定信号量的当前资源数量是0(信号量没有发出通知信号),那么系统就调用函数进入等待状态。当另一个线程将对信号量的当前资源数量进行递增时,系统会记住该等待线程(或多个线程),并允许它变为可调度状态(相应地递减它的当前资源数量)。线程同步:信号量¤

信号量对象相关的API函数:CreateSemaphore:创建信号量内核对象。OpenSemaphore:打开指定名称的信号量。ReleaseSemaphore:释放信号量对象,对信号量的当前资源数量进行递增。CloseHandle:关闭信号量句柄。信号量对象同样使用WaitForSingleObject或WaitForMultipleObjects来等待对象有信号,等待满足的情况信号量表示的资源计数将被减1。注意,只要资源计数大于0,信号量对象始终是处于有信号状态的,也就是说WaitXxxx函数总是可以等待成功。线程同步:信号量¤

创建一个信号量对象,使用CreateSemaphore函数,该函数原型如下图所示:¤

第一个参数仍然是安全描述,传递NULL使用缺省值。第二个参数lInitialCount表示创建时信号量的初始可用资源数。参数

lMaximunCount表示信号量的最大资源数。线程同步:信号量¤

最后一个参数lpName是赋给信号量的字符串名字,可以通过在其它进程或线程中使用该名字调用CreateSemaphore或者OpenSemaphore来得到信号量的句柄。¤

OpenSemaphore函数原型如下图所示,其参数含义OpenEvent/OpenMutex等完全一致。即访问权限、句柄的子进程继承性和名字。线程同步:信号量¤

释放信号量(增加其可用资源计数)使用函数ReleaseSemaphore,函数原型如下图所示:¤

与ReleaseMutex函数不同的是,由于信号量不属于某

个线程,因此,任意线程可以在任意时刻调用

ReleaseSemaphore函数来增加信号量的可用资源计数。¤

其次,ReleaseSemaphore可以使用大于1的值来增加

其可用资源计数。参数lReleaseCount即表示要增加的

计数值,通过参数lpPreviousCount可以返回之前的值。线程同步:信号量¤

因此,如果某种情况下不得不调用

WaitForSingleObject多次来获得多个可用资源,则释放资源时只需调用一次ReleasSemaphore就可以了。比如下面的代码:WaitForSingleObject(g_hPort,

INFINITE);WaitForSingleObject(g_hPort,

INFINITE);…ReleaseSemaphore(g_hPort,

2,

NULL);¤注意,不可以调用WaitForMultipleObjects去两次获得一个信号量。前面我们提到过,该函数不允许在同一次调用中使用同一句柄超过一次。线程同步:信号量¤

另外,向ReleaseSemaphore的最后一个参数传递一个LONG型变量的指针可以获取调用

ReleaseSemaphore之前信号量的可用资源数,如果对该值不感兴趣,传递NULL是可以的。¤

但不能试图用下面这样的方式,企图在不改变信号量的可用资源计数的情况下获取当前的可用资源计数:ReleaseSemaphore(g_hSem,

0,

&lPreCount);这时返回的将是0值,因此,现在没有办法不改变信号量的计数而得到它。范例:生产者-消费者模型¤生产者-消费者问题,有时也称作有界缓冲区问题。它可以这样描述:两个线程共享一个公共的固定大小的

缓冲区,其中一个线程作为生产者,将数据写入缓冲

区,而另一个线程作为消费者,从缓冲区中读取数据。¤麻烦在于当缓冲区已满,而生产者还想向其中写数据,或者缓冲区已空,消费者还想读取数据的情况。¤解决的办法在于:当缓冲区已满时,让生产者睡眠,等到消费者从中取走一个数据时,再唤醒生产者。同样,当缓冲区已空时,让消费者睡眠,等到生产者又向缓冲区中写入一个数据时,唤醒消费者。范例:生产者-消费者模型¤

生产者-消费者模型实现:考虑生产者-消费者问题的特点,生产者和消费者共享一个固定大小的缓冲区,而且当缓冲区中数据项个数不为0也不是最大值时,生产者和消费者可以同时访问缓冲区,但要保证不是访问同一个数据块。当缓冲区中充满数据时,生产者应该阻塞自己,等待消费者读取数据,至少腾空一个数据块位置时,再向缓冲区中写入数据。消费者的情况正好相反。注意:这里指的是只有一个生产者和一个消费者的情况。范例:生产者-消费者模型¤

基于以上,我们可用考虑使用一个循环队列来作为共享的缓冲区。注意,如果使用链表,将失去队列的随机存取特性,但使用链表也有好处,那就是使得动态改变缓冲区大小更为容易。这里我们考虑使用循环队列,并使用两个游标Head和Tail

来标记数据区的头和尾的位置。¤并且,我们约定生产者向队列尾增加数据项,消费者从队列头读取数据项。¤比如,当队列为空的时候,头和尾都指向位置0,当生产者写入一个数据项,则位置0中包含了数据项,而尾的位置增加变为1,此时,消费者就可以从队列头,也就是位置0读取一个数据项,并将头的位置增加也变为

1,此时头尾值再次相等,也表明队列中已经没有数据项了。范例:生产者-消费者模型¤

如下图所示,假设循环队列中有D1~D6六个数据项,

Head和Tail分别指向当前数据项的头和尾,则当生产者向Tail指向的位置写入一个数据项后,新的数据尾变成了Tail_2指向的位置,而消费者从Head位置读取一个数据项后,新的数据头将变为Head_2指向的位置。D5D1D2D3D4D6HeadTailTail_2Head_2范例:生产者-消费者模型¤

为了同步生产者和消费者的行为,我们创建两个信号量:一个可以叫做Semaphore_Empty用来表示循环队列中可用数据槽(空闲位置)的个数,它最大可以取到循环队列的大小,同时也初始化为最大值,即循环队列的大小,表示循环队列中还没有表项,所有的位置都可以写入数据。另一个可以叫做Semaphore_Full,用来表示循环队列中数据项的个数,它最大值可以取到循环队列的大小,初始化为0,表示队列中没有数据项。范例:生产者-消费者模型¤

生产者的行为:首先查看Semaphore_Empty信号量是否有信号,如果有,表示可以写入数据,则从队列的尾,即游标

Tail处写入数据,之后将Semaphore_Full信号量值加1,表示增加了一个数据项。并且将游标Tail的值更新。注意Tail总是指向一个空位置,即生产者将从Tail处写入数据。如果Semaphore_Empty无信号,则说明队列中数据已满,无法再写入,此时生产者阻塞,等待消费者取走数据,即等待Semaphore_Empty有信号。范例:生产者-消费者模型¤

消费者的行为:与生产者的行为类似,消费者首先查看

Semaphore_Full信号量是否有信号,如果有,表示队列中有数据项,则从队列的头,即游标Head处取走数据,之后将Semaphore_Empty信号量值加1,表示腾空了一个数据位置。并且将游标Head的值更新。注意Head总是指向一个存在的数据项,消费者将从Head处读取数据。如果Semaphore_Full无信号,则说明队列已空,已经没有数据可读,此时消费者阻塞,等待生产者写入数据,即等待Semaphore_Full有信号。范例:生产者-消费者模型#defineMAX_MSG_QUEUE640//队列长度#definePACKET_SIZE128//数据单元大小¤根据上面的讨论,我们现在可以写出表示生产者和消费者行为的伪代码了,首先我们将队列和使用到的信号量、游标初始化,如下:>>BYTE

MsgQueue[MAX_MSG_QUEUE][PACKET_SIZE];UINT

Head

=

0,

Tail=0;HANDLE

hSemaphoreEmpty

=CreateSemaphore(NULL, MAX_MSG_QUEUE,

MAX_MSG_QUEUE,

NULL);If(!hSemaphoreEmpty)

error;HANDLE

hSemaphoreFull

=CreateSemaphore(NULL, 0, MAX_MSG_QUEUE,

NULL);If(!

hSemaphoreFull

)

error;范例:生产者-消费者模型¤

生产者的行为可以用下面的伪代码描述:>

>>DWORD

dwRet;dwRet

=

WaitForSingleObject(

hSemaphoreEmpty,

waittime);if(dwRet

==

Wait_Failed

||

dwRet

==

Wait_Timeout)

{Error;}memcpy(MsgQueue[Tail],

in_buffer,

PACKET_SIZE);Tail=(Tail+1)%MAX_MSG_QUEUE;//循环队列ReleaseSemaphore(hSemaphoreFull,

1,

NULL);范例:生产者-消费者模型¤

消费者的行为可以用下面的伪代码描述:>

>>DWORD

dwRet;dwRet

=

WaitForSingleObject(hSemaphoreFull,

waittime);if(dwRet

==

Wait_Failed

||

dwRet

==

Wait_Timeout)

{Error;}memcpy(out_buffer,

MsgQueue[Head],

PACKET_SIZE);Head=(Head+1)%MAX_MSG_QUEUE;//循环队列ReleaseSemaphore(hSemaphoreEmpty,

1,

NULL);范例:生产者-消费者模型¤

练习:为了提供一个实用的模型,可以考虑将刚刚讨论的生产者-消费者模型封装为一个消息队列类:CMsgQueue,该类提供以下成员函数:Initialize:对信号量等进行初始化,对于队列大小,可以使用内存分配函数根据传入的参数指定大小,也可以使用配置文件或者简单的宏定义等方式。PushMessage:向队列写入数据,即生产者的行为。PopMessage:从队列读取数据,即消费者的行为。注意在析构函数中进行必要的清理工作。附带的FMsgQueue.cpp和FMsgQueue.h为该类的一个实现。范例:生产者-消费者模型¤

多生产者-多消费者的情况。注意,前面讲述的模型适用于只有一个生产者和一个消费者的情况。如果存在多个生产者线程或多个消费者线程,则需要对循环队列进行保护,包括对队列的Head、Tail游标进行保护。正确的做法是将对队列的访问放在一个临界

区里,或者使用互斥量对象保护对队列的访

问。对于单个进程的情况,可以使用临界区,而对于多个进程的情况则必须使用互斥量对

象。范例:生产者-消费者模型–比如,可能有这样的情况,两个socket连接用于接收数据,而使用同一个工作线程对收到的数据进行解析,即是一个多生产者单消费者的情况。¤

练习:将前面实现的CMsgQueue类中加入临界区或互斥量对生产者即

PushMessage的保护,实现一个可用于多生产者-单消费者的消息队列类。范例:使用生产者-消费者模型¤

练习:上一讲中的范例DocStats演示了自动重设事件对象的使用,现在我们用刚刚完成的

CMsgQueue类所描述的生产者消费者模型来完成DocStats的功能。在上一讲中,我们在DocStats范例中创建了三个线程进行不同项目的统计(字母数、单词数和行数),事实上,观察DocStats的代码,这几项统计项目完全可以在一个线程中完成,我们可以按照下面的描述来重新设计该程序的流程。范例:使用生产者-消费者模型¤

仍然使用原先的界面,首先我们将MsgQueue.cpp和MsgQueue.h加入到工程中去,并在*Dlg类中加入一个

CMsgQueue类的成员,并在初始化对话框的时候将其初始化。¤

之后,当用户点击Brows按钮选择了文件,再点击

Calculate

DocumentsStatistics的时候,创建一个统计线程,该线程将完成所有三项内容的统计。创建后主线程调用CMsgQueue的类成员PushMessage向队列中写入数据块。¤

此时统计线程调用CMsgQueue类的成员函数

PopMessage从队列中读取数据进行处理。并累计处理数据的长度,当长度达到文件的总长度时,线程结束,统计完成。范例:使用生产者-消费者模型¤

我们可以用类似下面的代码这样来定义循环队列的大小和队列的数据单元类型。>

>>#define#define#defineMAX_MSG_QUEUEDATA_BLOCK_SIZEPACKET_SIZE6410241024

+sizeof(ULONG)typedef

struct

Packet

{ULONGlength;BYTE

data[DATA_BLOCK_SIZE];}

PACKET;也就是说,队列中的每一个数据块均为PACKET结构类型,该结构的第一个字段记录了数据长度,第一个字段之后是该长度的数据。范例:使用生产者-消费者模型¤

点击CalculateDocumentsStatistics后的流程可以如下面的伪代码所示:>

>>HANDLE

hFile

=

CreateFile(m_strFileName,…);m_nFileLen

=

GetFileSize(hFile,

NULL);m_dwNumLetters

=

0;m_dwNumWords

=

0;m_dwNumLines

=

0;HANDLE

hThread

=

CreateThread(NULL,

0,StatThreadFunc,

(PVOID)

this,

0,

&dwThreadID);//创建了统计线程。范例:使用生产者-消费者模型下面的代码循环读取文件数据,并填充PACKET结构,将其写入的循环队列中去。PushMessage函数已经对

线程间的同步做了很好的处理

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论