操作系统创建线程利用互斥实现线程共享变量通信.docx
《操作系统创建线程利用互斥实现线程共享变量通信.docx》由会员分享,可在线阅读,更多相关《操作系统创建线程利用互斥实现线程共享变量通信.docx(33页珍藏版)》请在冰豆网上搜索。
操作系统创建线程利用互斥实现线程共享变量通信
创建线程,利用互斥实现线程共享变量通信
一.概述
1.1课题目的和意义
掌握线程创建和终止,加深对线程和进程概念的理解,会用同步与互斥方法实现线程之间的通信。
1.2内容和要求
软件界面上点“创建线程”按钮,创建三个生产者线程(P1,P2,P3)和两个消费者线程(C1,C2),生产者和消费者线程共享一个长度为2KB的环型公共缓冲区,生产者向其中投放消息,消费者从中取走消息。
只要缓冲区未满,生产者可将消息送入缓冲区;只要缓冲区未空,消费者可从缓冲区取走一个消息。
每个消息具下列结构格式:
消息头(1B,固定为0xaa),消息长度(1B),消息内容(nB),校验和(1B),检验和计算方式为消息长度和消息内容所有字节异或结果。
每个生产者每隔n毫秒(n用随机数产生,1到100毫秒之间,间隔不固定)生产一个消息加入缓冲区,并把消息产生时间和内容记录在一个文本文件中(或显示在列表框中)。
P1每次生产的数据为26个大写字母,P2每次生产的数据为26个小写字母,P3每次生产的数据为10个数字。
每个消费者每隔n秒(n用随机数产生,1到5秒之间,间隔不固定)从缓冲区取走一个消息。
每消费一个消息需要将消费时间和消息内容记录在一个文本文件中(或显示在列表框中)。
当用户按结束按钮时结束5个线程,并将5个文件内容显示出来进行对照。
这期实是一个经典的生产者—消费者(Producer_consumer)进程(线程)同步的问题。
它描述的是:
有一群生产者进程在生产产品,并将此产品提供给消费者进程(线程)去消费。
为使生产者进程和消费者进程(线程)能并发执行,在它们之间设置有个缓冲区的缓冲池,生产者进程(线程)可将它所生产的产品放入一个缓冲区中,消费者进程(线程)可从一个缓冲区取得一个产品消费。
尽管所有的生产者进程和消费者进程(线程)都是以异步的方式运行的,但它们之间必须保持同步,即不允许消费者进程(线程)到一个空缓冲区去取产品,也不允许生产者进程(线程)向一个已装有消息尚未被取走产品的缓冲区投放产品。
如下图所示:
1.3线程所采用的同步方法
同步是多线程中的重要概念.同步的使用可以保证在多线程运行的环境中,程充不会产生设计之外的结果.同步的实现方式有两种,同步方法和同步块.
线程在执行同步方法是具有排它性的.当任意一个线和进入到一个对象的任意一个同步方法时,这个对象所有同步方法都被锁定,在些期间,期他任何线程都不能访问这个对象的任意一个同步方法,直到这个线程执行完它所调用的同步方法并从中退出,从而导至它释放了该对象的同步锁这后.在一个对象被某个线程锁定之后,其他线程是可以访问.
同步的有几种实现方法,分别是:
wait():
使一个线程处于等待状态,并且释放所有持有的对象lock.
sleep():
使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():
唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
Allnotity():
唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
1.4开发工具平台
开发平台:
windowXP开发工具:
VC++
二.数据定义和详细说明
1数据定义
设计PV操作算法,用信号量机制实现生产者与消费者同步与互斥问题,并与无PV情况下进行对比。
定义20个缓冲区,将其初始化为0。
调用随机函数rand()生成随机数,把随机数通过生产者放入缓冲区。
若缓冲区满则其值非0。
当消费者从缓冲区中去数后缓冲区值变为0。
程序可显示缓冲区中的全部内容,方便观察生产者与消费者的行为。
程序可通过设置sleep(time)中time的值来控制生产者与消费者的执行顺序。
2详细说明
为了实现生产者与消费者同步与互斥的问题,该程序用记录型信号量机制来实现。
假定在生产者与消费者之间,利用一个公共的缓冲池来进行通信,生产者将所生产的信息放入其中,消费者cognitive缓冲池中取得消息来消费,该缓冲池具有n个缓冲区,其编号为0,1,2,3···,n-1;设置一个互斥信号量mutex,用于实现诸进程对缓冲池的互斥使用,其初值为1,利用资源信号量empty,表示缓冲池中空缓冲区的数目,其初值为n;full分别表示缓冲池中满缓冲区的数目,其初值为0.又假定这些生产者和消费者相互等效,只要缓冲池未满,生产者便可将消息送入由指针in所指的缓冲区;只要缓冲池未空,消费者变可以从由指针out所指示的缓冲区中,取走一个消息。
对生产者消费者的问题可以描述如下:
Varmetux,empty,full:
semaphore=1,n,0;
Buffer:
array[0,···n-1]ofitem;
In,out:
integer:
=0;
Begin:
Pabegin
Producer:
begin
Repeat
Produceranitemnextp;
P(empty);
P(mutex);
buffer(in):
=nextp;
in:
=(in+1)modn;
V(mutex);
V(full);
Untilfalse;
End
Consumer:
begin
Repeat
P(full);
P(mutex);
nextc:
=buffer(out);
out:
=(out+1)modn;
V(mutex);
V(empty);
Untilfalse;
End
Parend
End
三.实现思想和设计流程
1实现思想
我们把系统中使用某一类资源的进程(线程)称为该资源的消费者,而把释放同类资源的进程称为该资源的生产者。
例如在计算进程(线程)与打印进程(线程)公用一个缓冲区时,计算进程(线程)把数据送入缓冲区,打印进程(线程)从缓冲区中取数据打印输出,因此,计算进程相当于数据资源的生产者,而打印进程相当于消费者,二者之间必须保持同步。
基于这一问题,我们将使用生产者和消费者这一同步机制算法来处理该问题.
2设计流程
首先,我们知道,生产者—消费者问题是一个同步问题。
即生产者和消费者之间应满足如下条件:
2.1消费者想接收数据时,有界缓冲区中至少有一个单元是满的。
2.2生产者想发送数据时,有界缓冲区中至少有一个单元是空的。
另外,由于有界缓冲区是临界资源,因此,各生产者进程和各消费者进程之间必须互斥。
其次,我们还必须考虑面临的问题是属于进程互斥还是进程同步,或是互斥与同步的混合问题。
然后根据共享资源的数量以及使用共享资源的规则正确的定义信号量及其初值。
最后,还要对结果进行分析处理。
若结果中生产和消费进程都已处理完时,但还可能出现以下两种情况:
一是还有生产进程,但没有空缓冲,且消费进程暂时已完,所以此时,只能结束等待新的消费进程产生空缓冲。
二是还有消费进程,但没有了满缓冲,且生产进程暂时已完,此时,只能结束等待新的生产进程来输入数据,产生新的满缓冲等。
在程序中应能作出相应的判断和处理。
本程序的执行是在C++的环境中通过手动输入生产者和消费者线程的运行速度来控制程序的运行的。
为了实现生产者进程能把生产出来的产品正确的存入缓冲区,和消费者进程能够从缓冲区中取产品进行消费,防止因等待资源而出现死锁的现象,首先设置两个时间:
生产者生产一个产品后等待的时间t1,和消费者消费一个产品后等待的时间t2,来控制生产者和消费者进程执行的速度。
其函
数原形是sleep(t1)和sleep(t2)其中t1、t2指定义挂起执行线程的时间,以毫秒为
单位,取值为0时,该线程将余下的时间片交给处于就绪状态的同一优先级的其他线程。
若没有处于就绪状态的同一优先级的其他线程,则函数立即返回。
3程序流程图
图3-1程序流程图
4关键代码分析
本程序采用了MFC可视化界面来完成,现在给出关键代码来分析.
4.1开始创建线程
原代码:
voidCMultiThreadDlg:
:
OnStart()//开始创建线程
{
hMutex=CreateMutex(NULL,FALSE,NULL);//创建互斥对象
threadController=1;
check=TRUE;//检测标识
HWNDhWnd=GetSafeHwnd();//得到控制权
AfxBeginThread(ThreadProc,hWnd,THREAD_PRIORITY_NORMAL);//启用生产者线程1(P1)
AfxBeginThread(ThreadProc2,hWnd,THREAD_PRIORITY_NORMAL);//启用生产者线程2(P2)
AfxBeginThread(ThreadProc3,hWnd,THREAD_PRIORITY_NORMAL);//启用生产者线程3(P3)
AfxBeginThread(Thread_consumer,hWnd,THREAD_PRIORITY_NORMAL);//启用消费者线程1(S1)
AfxBeginThread(Thread_consumer2,hWnd,THREAD_PRIORITY_NORMAL);//启用消费者线程2(S2)
}
原码功能:
主要创建生产者和消费者线程,创建互斥对象,创建窗体对象.
主要函数功能:
4.1.2CreateMutex()
函数功能:
该函数是创建有名或者无名的互斥对象。
函数原型:
HANDLECreateMutex(LPSECURITY_ATTRIBUTESlpMutexAttributes,
BOOLbInitialOwner,LPCTSTRlpName);
参数:
lpMutexAttributes:
指向SECURITY_ATTRIBUTES结构的指针,该结构决定子进程是否能继承返回句柄。
如果lpMutexAttributes为NULL,那么句柄不能被继承。
在WindowsNT中该结构的lpSecurityDescriptor成员指定新互斥对象的安全描述符。
如果lpMutexAttributes为NULL,那么互斥对象获得缺省的安全描述符。
bInitialOwner:
指定互斥对象的初始所属身份。
如果该值为TRUE并且调用者创建互斥对象,那么调用线程获得互斥对象所属身份。
否则,调用线程不能获得互斥对象所属身份。
判断调用者是否创建互斥对象请参阅返回值部分。
lpName:
指向以NULL结尾的字符串,该字符串指定了互斥对象名。
该名字的长度小于MAX_PATH且可以包含除反斜线路径分隔符(\)以外的任何字符。
名字是区分大小写的。
如果与已存在的有名互斥对象名相匹配,那么该函数要求用__权限访问已存在的对象。
在这种情况下,由于参数己被创建进程所设置,该参数被忽略。
如果参数不为,它决定句柄是否解除继承,但是其安全描述符成员被忽略。
如果lpName为NULL,那么创建的互斥对象无名。
如果lpName与已存在的事件、信号量、可等待定时器、作业、或者文件映射对象的名字相匹配,那么函数调用失败,并且GetLastError函数返回ERPOR_INVALID_HANDLE。
其原因是这些对象共享相同的名字空间。
返回值:
如果函数调用成功,返回值是互斥对象句柄;如果函数调用之前,有名互斥对象已存在,那么函数给已存在的对象返回一个句柄,并且函数GetLastError返回ERROR_ALREADY_EXISTS,否则,调用者创建互斥对象。
如果函数调用失败,则返回值为NULL。
若想获得更多错误信息,请调用GetLastError函数。
如果CreateMutex中的lpMutexAttributes参数允许继承,由CreateProcess函数创建的子进程可以继承父进程的互斥对象句柄。
一个进程可以在调用DuplicateHandle函数时指定互斥对象句柄来创建一个可以被其他进程使用的双重句柄。
一个进程在调用OpenMutex或CreateMutex函数时能指定互斥对象名。
使用CloseHandle函数关闭句柄,进程结束时系统自动关闭句柄。
当最后一
个句柄被关闭时,互斥对象被销毁。
4.1.3GetSafeHwnd()
功能:
到一个窗口对象(CWnd的派生对象)指针的句柄(HWND)
函数原型:
HWNDhwnd=pwnd->GetSafeHwnd();
函数用法:
CWnd*pwnd=FindWindow(“ExploreWClass”,NULL);//希望找到资源管理器
HWNDhwnd=pwnd->m_hwnd;//得到它的HWND
这样的代码当开始得到的pwnd为空的时候就会出现一个“Generalprotectionerror”,并关闭应用程序,因为一般不能对一个NULL指针访问其成员,如果用下面的代码:
CWnd*pwnd=FindWindow(“ExploreWClass”,NULL);//希望找到资源管理器
HWNDhwnd=pwnd->GetSafeHwnd();//得到它的HWND
就不会出现问题,因为尽管当pwnd是NULL时,GetSafeHwnd仍然可以用,只是返回NULL,通过GetSafeHwnd()的实现代码就更清楚了:
_AFXWIN_INLINEHWNDCWnd:
:
GetSafeHwnd()const
{
returnthis==NULL?
NULL:
m_hWnd;
}
4.1.4AfxBeginThread()
功能:
用于创建工作者线程
函数原型:
CWinThread*AfxBeginThread(AFX_THREADPROCpfnThreadProc,LPVOIDpParam,intnPriority=THREAD_PRIORITY_NORMAL,UINTnStackSize=0,DWORDdwCreateFlags=0,LPSECURITY_ATTRIBUTESlpSecurityAttrs=NULL);
返回值:
一个指向新线程的线程对象.
pfnThreadProc:
线程的入口函数,声明一定要如下:
UINTMyThreadFunction(LPVOIDpParam);
pParam:
传递入线程的参数,注意它的类型为:
LPVOID,所以我们可以传递一个结构体入线程.
nPriority:
线程的优先级,一般设置为0.让它和主线程具有共同的优先级.
nStackSize:
指定新创建的线程的栈的大小.如果为0,新创建的线程具有和主线程一样的大小的栈
dwCreateFlags:
指定创建线程以后,线程有怎么样的标志.可以指定两个值:
CREATE_SUSPENDED:
线程创建以后,会处于挂起状态,直到调用:
ResumeThread0:
创建线程后就开始运行.
lpSecurityAttrs:
指向一个SECURITY_ATTRIBUTES的结构体,用它来标志新创建线程的安全性.如果为NULL,那么新创建的线程就具有和主线程一样的安全性.如果要在线程内结束线程,可以在线程内调用AfxEndThread.
4.2生产者线程
原码:
UINTCMultiThreadDlg:
:
ThreadProc(LPVOIDparam)//生产者1的生产过程
{
while(threadController)
{
WaitForSingleObject(hMutex,INFINITE);//等待一个同步事件的到来
if(number<20)
{
srand((unsigned)time(NULL));//返回一个随机数
intn;
n=rand()%9+1;//随机显示字数
Sleep(n*100);
CStringstr;
CStringstr_;
for(inti=0;i{
str_.Format("%c",65+rand()%9+1);//显示大写字母
str.Insert(i,str_);//插入成一列
}
list.AddTail(CString(str));
CStringstring;
string.Format("%s%d%s%d","0xaa",n,list.GetTail(),sizeof(list.GetTail())^sizeof(n));//按格式消息头(1B,固定为0xaa),消息长度(1B),消息内容(nB),校验和(1B)
pbox1->AddString(string);//打印
number++;
}
ReleaseMutex(hMutex);//释放互斥
}
}
原码功能:
当生产者线程1得到控制权,等待同步事件,产生随机个数(n),按产生的随机数显示n个大写字母,利用sleep函数来隔时存储到缓冲区,最后按格式消息头(1B,固定为0xaa),消息长度(1B),消息内容(nB),校验和(1B)的方式输出.
主要函数功能:
4.2.1WaitForSingleObject()
函数功能:
当如下情况之一发生时该函数返回:
指定对象处于信号态;超时。
函数原型:
DWORDWaitForSingleObject(HANDLEhHandle,DWORDdwMilliseconds);
参数:
hHandle:
等待对象句柄。
若想了解指定句柄的对象类型列表,参阅下面说明部分。
在WndowsNT中,句柄必须有SYNCHRONIZE访问权限。
若想获得更多的信息,请查看StandardAccessRights。
dwMilliseconds:
指定以毫秒为单位的超时间隔。
如果超时,即使对象的状态是非信号态的并且没有完成,函数也返回。
如果dwMilliseconds是0,函数测试对象的状态并立刻返回:
如果dwMillseconds是INFINlTE,函数从不超时。
返回值:
如果函数调用成功,返回值表明引起函数返回的事件。
可能值如下:
WAIT_ABANDONED:
指定对象是互斥对象,在线程被终止前,线程没有释放互斥对象。
互斥对象的所属关系被授予调用线程,并且该互斥对象被置为非信号态。
WAIT_OBJECT_0:
指定对象的状态被置为信号态。
WAlT_TIMEOUT:
超时,并且对象的状态为非信号态。
如果函数调用失败,返回值是WAIT_FAILED。
若想获得更多错误信息,请调用GetLastError函数。
4.2.2ReleaseMutex()
函数功能:
该函数放弃指定互斥对象的拥有权。
函数原型:
BOOLReleaseMutex(HANDLEhMutex);
参数:
hMutex:
互斥对象句柄。
为CreateMutex或OpenMutex函数的返回值。
返回值:
如果函数调用成功,那么返回值是非零值;如果函数调用失败,那么返回值是零值。
若想获得更多错误信息,请调用GetLastError函数。
备注:
如果调用线程不拥有互斥对象,ReleaseMutex函数失败。
一个线程通过调用等待函数拥有互斥对象。
创建该互斥对象的线程也拥有互斥对象,而不需要调用等待函数。
当互斥对象的所有者线程不再需要互斥对象时,它可以调用ReleaseMutex函数。
当一个线程拥有一个互斥对象后,它可以用该互斥对象多次调用等待函数而不会阻塞。
这防止一个线程等待一个它已拥有的互斥对象时出现死锁。
不过,为了释放所有权,该线程必须为每一个等待操作调用一次ReleaseMutex函数。
4.2.3Sleep()
函数功能:
该函数对于指定的时间间隔挂起当前的执行线程。
函数原型:
voidSleep(DWORDdwMilliseconds);
参数:
dwMilliseconds:
定义挂起执行线程的时间,以毫秒为单位。
取值为0时,该线程将余下的时间片交给处于就绪状态的同一优先级的其他线程。
若没有处于就绪状态的同一优先级的其他线程,则函数立即返回,该线程继续执行。
若取值为INFINITE则造成无限延迟。
返回值:
该函数没有返回值。
备注:
一个线程可以在调用该函数时将睡眠时间设为0毫秒,以将剩余的时间片交出。
使用Sleep函数和直接或间接创建窗口的代码时必须非常小心。
若线程创建了窗口,它就必须处理消息。
消息广播被发送给系统中的所有窗口。
若有一个线程调用Sleep函数时使用了无限延迟,则系统会死锁。
两个直接创建窗口的代码的例子是DDE和COMCoInitialize。
因此,若有一个创建窗口的线程,则使用MsgWaitForMutipleObjects和MsgWaitForMutipleObjectsEx函数,而不使用Sleep()函数。
4.2.4GetTail()
函数功能:
获取此列表中的头元素
函数原型:
CTypedPtrList:
:
GetTail
参数:
指定保存在列表中的元素类型的模板参数
返回值:
如果是通过一个指向constCTypedPtrList的指针访问此列表,则GetTail返回一个类型由模板参数TYPE指定的指针。
这使此函数只能被使用在赋值语句的右边,这样就保护了列表不被修改。
如果列表被直接访问,或通过一个指向CTypedPtrList的指针访问,则GetTail返回对一个类型由模板参数TYPE指定的指针的引用。
这使得此函数可以使用在赋值语句的任何一边,从而允许该列表可以被修改
备注:
在调用GetTail之前,你必须保证该列表不是空的。
如果列表是空的,则Microsoft基础类库的调试版将给出断言。
使用IsEmpty来检验列表是否包含元素。
4.3消费者线程
原码:
UINTCMultiThreadDlg:
:
Thread_consumer(LPVOIDparam)//消费者
{
while(threadController)
{
WaitForSingleObject(hMutex,INFINITE);//等待同步事件
if(number>0)//缓冲区有内容
{
srand((unsigned)time(NULL));
intn;
n=rand()%4+1;
Sleep(n*1000);//延时1-5秒
CStringstring;
string.Format("%s%d%s%d","0xaa",strlen(list.GetHead()),list.GetHead(),sizeof(list.GetTail())^sizeof(strlen(list.GetHead())));//得到缓冲区内容
pbox4->AddString(string);
list.RemoveHead();//删除头内容
number--;
ReleaseMutex(hMutex);//释放互斥
}
else//否则解除互斥
{
ReleaseMutex(hMutex);
}
}
return0;
}
原码功能:
执行消费费线程,当缓冲区有数据时取出内容,并且按随机时间(1-5秒的范围内)取数,按固定的格式消息头(1B,固定为0xa