Windows下线程同步互斥

来源:互联网 发布:怎么代理淘宝商品 编辑:程序博客网 时间:2024/04/29 09:02

Windows下线程同步互斥

近期老师安排了个操作系统的作业,涉及到一些同步和互斥的问题。网上查找了一些资料,整理了一下加强记忆。(一些资料未注明出处,如需添加说明私信即可)

一、有关线程的几个重要的函数:
1._beginthreadex函数
//创建线程
函数说明:
unsigned long _beginthreadex(
void *security, //第1个参数:安全属性,NULL为默认安全属性
unsigned stack_size, //第2个参数:指定线程堆栈的大小。如果为0,则 程堆栈大小和创建它的线程的相同。一般用0
unsigned ( __stdcall start_address )( void ),
//第3个参数:指定线程函数的地址,也就是线程调用执行的函数地址(用函数名称即可,函数名称就表示地址)
void *arglist, //第4个参数:传递给线程的参数的指针,可以通过传入对象的指针,在线程函数中再转化为对应类的指针
unsigned initflag, //第5个参数:线程初始状态,0:立即运行;//CREATE_SUSPEND:suspended(悬挂)
unsigned *thrdaddr //第6个参数:用于记录线程ID的地址
);
重点在第三个参数,其实这个我开始认为只需要随便找个类中的方法名就可以了,因为我的理解是这里是线程启动时候需要调用的一个入口方法。经过分析,理解正确,但是使用起来有些难度,因为定义死了这个方法的类型必须得是(__stdcall *start_address)类型,所以为了不找麻烦,就干脆在线程类中,造一个这个类型的方法算了,然后在这个方法中,把这个线程类实例化一下,调用入口方法即可。

2.WaitForSingleObject()函数
DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);
参数:
hHandle:对象句柄。可以指定一系列的对象,如Event、Job、Memory resource notification、Mutex、Process、Semaphore、Thread、Waitable timer等。
dwMilliseconds:定时时间间隔,单位为milliseconds(毫秒).如果指定一个非零值,函数处于等待状态直到hHandle标记的对象被触发,或者时间到了。如果dwMilliseconds为0,对象没有被触发信号,函数不会进入一个等待状态,它总是立即返回。如果dwMilliseconds为INFINITE,对象被触发信号后,函数才会返回。
说明:
等待函数可使线程自愿进入等待状态,直到一个特定的内核对象变为已通知状态为止。

3.WaitForMultipleObjects()函数
DWORD WaitForMultipleObjects(
DWORD nCount, // number of handles in the handle array
CONST HANDLE *lpHandles, // pointer to the object-handle array
BOOL fWaitAll, // wait flag
DWORD dwMilliseconds // time-out interval in milliseconds
);
其中参数:
nCount 句柄的数量 最大值为MAXIMUM_WAIT_OBJECTS(64)
HANDLE 句柄数组的指针。
HANDLE 类型可以为(Event,Mutex,Process,Thread,Semaphore )数组
BOOL fWaitAll 等待的类型,如果为TRUE 则等待所有信号量有效在往下执行,FALSE 当有其中一个信号量有效时就向下执行.
DWORD dwMilliseconds 超时时间 超时后向执行。 如果为WSA_INFINITE 永不超时。如果没有信号量就会在这死等。

白话解释:
WaitForMultipleObjects与WaitForSingleObject函数很相似,区别在于它允许调用线程同时查看若干个内核对象的已知状态。 WaitForMultipleObjects函数的返回值告诉调用线程,为什么它会被重新调度。
可能的返回值是WAIT_FAILED和WAIT_TIMEOUT,这两个值的作用是很清楚的,就是等待失败和等待超时。
如果为fWaitAll参数传递TRUE,同时所有对象均变为已通知状态,那么返回值是WAIT_OBJECT_0。 如果为fWaitAll参数传递FALSE,那么一旦任何一个对象变为已通知状态,该函数便返回。在这种情况下,你可能想要知道哪个对象变为已通知状态。返回值是WAIT_OBJECT_0与(WAIT_OBJECT_0+dwCount-1)之间的一个值。
换句话说,如果返回值不是WAIT_TIMEOUT,也不是WAIT_FAILED,那么应该从返回值中减去WAIT_OBJECT_0。产生的数字是作为第二个参数传递给WaitForMultipleObjects的句柄数组中的索引。

4.SingleObjectAndWait()
DWORD SingleObjectAndWait(HANDLE hObjectToSignal,HANDLE hObjectToWaitOn,DWORD dwMilliseconds,BOOL fAlertable);
函数用于在单个原子方式的操作中发出关于内核对象的通知并等待另一个内核对象:hObjectToSignal参数必须标识一个互斥对象、信号对象或事件对象。hObjectToWaitOn参数用于标识下列任何一个内核对象:互斥对象、信标、事件、定时器、进程、线程、作业、控制台输入和修改通知。与平常一样,dwMilliseconds参数指明该函数为了等待该对象变为已通知状态,应该等待多长时间,而fAlertable标志则指明线程等待时该线程是否应该能够处理任何已经排队的异步过程调用。

5.MsgWaitForMultipleObjects(Ex)
MsgWaitForMultipleObjects(
_In_DWORDnCount,
In_reads_opt(nCount)CONSTHANDLE*pHandles,
_In_BOOLfWaitAll,
_In_DWORDdwMilliseconds,
_In_DWORDdwWakeMask);
参数:
nCount Long,指定列表中的句柄数量
pHandles Long,指定对象句柄组合中的第一个元素
fWaitAll Long,如果为TRUE,表示除非对象同时发出信号,否则就等待下去。如果为FALSE,表示任何对象发出信号即可。
dwMilliseconds Long,指定要等待的毫秒数。
dwWakeMask Long,带有QS_??前缀的一个或多个常数,用于标识特定的消息类型。
MsgWaitForMultipleObjects和MsgWaitForMultipleObjectsEx这些函数与WaitForMultipleObjects函数十分相似。差别在于它们允许线程在内核对象变成已通知状态或窗口消息需要调度到调用线程创建的窗口中时被调度。
创建窗口和执行与用户界面相关的任务的线程,应该调用MsgWaitForMultipleObjectsEx函数,而不应该调用WaitForMultipleObjects函数,因为后面这个函数将使线程的用户界面无法对用户作出响应。

二、关键段CRITICAL_SECTION
// critical section是每个线程中访问临界资源的那段代码,不论是硬件临//界资源,还是软件临界资源,多个线程必须互斥地对它进行访问。
//关键段可以用于线程间的互斥,但不可以用于同步。

关键段CRITICAL_SECTION的四个函数:
void InitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函数功能:初始化
函数说明:定义关键段变量后必须先初始化。

void DeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函数功能:销毁
函数说明:用完之后记得销毁。

void EnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函数功能:进入关键区域
函数说明:系统保证各线程互斥的进入关键区域。

void LeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函数功能:离开关关键区域
关键段总结
可以将关键段比作旅馆的房卡,调用EnterCriticalSection()即申请房卡,得到房卡后自己当然是可以多次进出房间的,在你调用LeaveCriticalSection()交出房卡之前,别人自然是无法进入该房间。
关键段的特性:
1.关键段共初始化化、销毁、进入和离开关键区域四个函数。
2.关键段可以解决线程的互斥问题,但因为具有“线程所有权”,所以无法解决同步问题。
3.推荐关键段与旋转锁配合使用。

补充:
1. 关键段
关键段是一小段代码,在执行之前需要独占一些共享资源的访问权。
关键段可以使多行代码以“原子方式”来对资源进行访问,此处的“原子方式”指的是除了此处线程,其他线程不能访问此资源。
2. 有关旋转锁
当线程试图进入一个关键段,但此关键段正被另一个线程占用的时候,函数会立即把调用线程切换到等待状态;这意味着线程必须从用户模式切换到内核模式(大约1000个CPU周期),这个切换的开销非常大。
为了提高关键段的性能,把旋转锁合并到关键段中。因此在调用EnterCriticalSection的时候,它会用一个旋转锁不断地循环,尝试在一段时间内获得资源的访问权,只有当尝试失败的时候,线程才会切换到内核模式并进入等待状态。

为了使用关键段的同时使用旋转锁,我们必须调用下面的函数来初始化关键段:

BOOL InitializeCriticalSectionAndSpinCount(

PCRITICAL_SECTION pcs,

DWORD dwSpinCount);//第二个参数是希望旋转锁循环的次数

SetCriticalSection函数来改变关键段的旋转次数:

DWORD SetCriticalSection(

PCRITICAL_SECTION p_cs,

DWORD dwSpinCount);

为了达到最佳性能,最简单的方式就是尝试各种值,直到对性能满意为止。用来保护进程的关键段所使用的旋转次数大约是4000,这是一个参考值。

三、事件-Event
//事件Event实际上是个内核对象,它的使用非常方便。下面列出一些常用的函数。
CreateEvent函数:
函数功能:创建事件
函数原型:
HANDLECreateEvent(

LPSECURITY_ATTRIBUTESlpEventAttributes,

BOOLbManualReset,

BOOLbInitialState,

LPCTSTRlpName

);
函数说明:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。打个小小比方,手动置位事件相当于教室门,教室门一旦打开(被触发),所以有人都可以进入直到老师去关上教室门(事件变成未触发)。自动置位事件就相当于医院里拍X光的房间门,门打开后只能进入一个人,这个人进去后会将门关上,其它人不能进入除非门重新被打开(事件重新被触发)。
第三个参数表示事件的初始状态,传入TRUR表示已触发。
第四个参数表示事件的名称,传入NULL表示匿名事件。

OpenEvent函数:
函数功能:根据名称获得一个事件句柄。
函数原型:
HANDLEOpenEvent(

DWORDdwDesiredAccess,

BOOLbInheritHandle,

LPCTSTRlpName //名称

);
函数说明:
第一个参数表示访问权限,对事件一般传入EVENT_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示事件句柄继承性,一般传入TRUE即可。
第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个事件。
SetEvent函数:
函数功能:触发事件
函数原型:BOOLSetEvent(HANDLEhEvent);
函数说明:每次触发后,必有一个或多个处于等待状态下的线程变成可调度状
ResetEvent函数:
函数功能:将事件设为末触发
函数原型:BOOLResetEvent(HANDLEhEvent);

PulseEvent函数:
函数功能:将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲。
函数原型:BOOLPulseEvent(HANDLEhEvent);
函数说明:这是一个不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent();此时情况可以分为两种:
1.对于手动置位事件,所有正处于等待状态下线程都变成可调度状态。
2.对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。
此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态。
事件的清理与销毁
函数功能:最后一个事件的清理与销毁
由于事件是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。

事件Event总结
1.事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。
2.事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。
3.事件可以解决线程间同步问题,因此也能解决互斥问题。

四、互斥量Mutex
互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问。互斥量与关键段的行为非常相似,并且互斥量可以用于不同进程中的线程互斥访问资源。
使用互斥量Mutex主要将用到四个函数:

CreateMutex函数:
函数功能:创建互斥量(注意与事件Event的创建函数对比)
函数原型:
HANDLECreateMutex(

LPSECURITY_ATTRIBUTESlpMutexAttributes,

BOOLbInitialOwner,

LPCTSTRlpName

);
函数说明:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数用来确定互斥量的初始拥有者。如果传入TRUE表示互斥量对象内部会记录创建它的线程的线程ID号并将递归计数设置为1,由于该线程ID非零,所以互斥量处于未触发状态。如果传入FALSE,那么互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这意味互斥量不为任何线程占用,处于触发状态。
第三个参数用来设置互斥量的名称,在多个进程中的线程就是通过名称来确保它们访问的是同一个互斥量。
函数访问值:
成功返回一个表示互斥量的句柄,失败返回NULL。

OpenMutex函数:
//打开互斥量
函数原型:
HANDLEOpenMutex(

DWORDdwDesiredAccess,

BOOLbInheritHandle,

LPCTSTRlpName //名称

);
函数说明:
第一个参数表示访问权限,对互斥量一般传入MUTEX_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示互斥量句柄继承性,一般传入TRUE即可。
第三个参数表示名称。某一个进程中的线程创建互斥量后,其它进程中的线程就可以通过这个函数来找到这个互斥量。
函数访问值:
成功返回一个表示互斥量的句柄,失败返回NULL。

ReleaseMutex函数:
//触发互斥量
函数原型:
BOOLReleaseMutex (HANDLEhMutex)
函数说明:
访问互斥资源前应该要调用等待函数,结束访问时就要调用ReleaseMutex()来表示自己已经结束访问,其它线程可以开始访问了。

CloseHandle()函数:
//清理互斥量
由于互斥量是内核对象,因此使用CloseHandle()就可以(这一点所有内核对象都一样)。
互斥量Mutex总结
最后总结下互斥量Mutex:
1.互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。
2.互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。

五、信号量Semaphore
信号量Semaphore常用有三个函数:

CreateSemaphore函数:
函数功能:创建信号量
函数原型:
HANDLE CreateSemaphore(

LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,

LONG lInitialCount,

LONG lMaximumCount,

LPCTSTR lpName

);
函数说明:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数表示初始资源数量。
第三个参数表示最大并发数量。
第四个参数表示信号量的名称,传入NULL表示匿名信号量。

OpenSemaphore函数:
函数功能:打开信号量
函数原型:
HANDLE OpenSemaphore(

DWORD dwDesiredAccess,

BOOL bInheritHandle,

LPCTSTR lpName

);
函数说明:
第一个参数表示访问权限,对一般传入SEMAPHORE_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示信号量句柄继承性,一般传入TRUE即可。
第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个信号量。

ReleaseSemaphore函数:
函数功能:递增信号量的当前资源计数
函数原型:
BOOL ReleaseSemaphore(

HANDLE hSemaphore,

LONG lReleaseCount,

LPLONG lpPreviousCount

);
函数说明:
第一个参数是信号量的句柄。
第二个参数表示增加个数,必须大于0且不超过最大资源数量。
第三个参数可以用来传出先前的资源计数,设为NULL表示不需要传出。
注意:当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽故信号量处于末触发。在对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。
最后一个 信号量的清理与销毁
由于信号量是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。
信号量Semaphore总结
由于信号量可以计算资源当前剩余量并根据当前剩余量与零比较来决定信号量是处于触发状态或是未触发状态,因此信号量的应用范围相当广泛。

补充:
有关进程、程序、线程、多线程的问题

1、进程(process)

狭义定义:进程就是一段程序的执行过程。

广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

简单的来讲进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。

进程状态:进程有三个状态,就绪、运行和阻塞。就绪状态其实就是获取了出cpu外的所有资源,只要处理器分配资源就可以马上执行。就绪状态有排队序列什么的,排队原则不再赘述。运行态就是获得了处理器分配的资源,程序开始执行。阻塞态,当程序条件不够时候,需要等待条件满足时候才能执行,如等待i/o操作时候,此刻的状态就叫阻塞态。

2、程序

说起进程,就不得不说下程序。先看定义:程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程则是在处理机上的一次执行过程,它是一个动态的概念。这个不难理解,其实进程是包含程序的,进程的执行离不开程序,进程中的文本区域就是代码区,也就是程序。

3、线程

通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

4、多线程

在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。

最简单的比喻多线程就像火车的每一节车厢,而进程则是火车。车厢离开火车是无法跑动的,同理火车也不可能只有一节车厢。多线程的出现就是为了提高效率。

区别:

1、进程与线程的区别:

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.

2) 线程的划分尺度小于进程,使得多线程程序的并发性高。

3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

优缺点:

线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP(多核处理机)机器上运行,而进程则可以跨机器迁移。

附一个简单的示例:

//经典线程同步互斥问题  #include <stdio.h>  #include <process.h>  #include <windows.h>  long g_nNum; //全局资源  unsigned int __stdcall Fun(void *pPM); //线程函数  const int THREAD_NUM = 10; //子线程个数  int main()  {      g_nNum = 0;      HANDLE  handle[THREAD_NUM];      int i = 0;      while (i < THREAD_NUM)       {          handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);          i++;//等子线程接收到参数时主线程可能改变了这个i的值      }      //保证子线程已全部运行结束      WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);        return 0;  }  unsigned int __stdcall Fun(void *pPM)  {  //由于创建线程是要一定的开销的,所以新线程并不能第一时间执行到这来      int nThreadNum = *(int *)pPM; //子线程获取参数      Sleep(50);//some work should to do      g_nNum++;  //处理全局资源      Sleep(0);//some work should to do      printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);      return 0;  }  

1.主线程创建子线程并传入一个指向变量地址的指针作参数,由于线程启动须要花费一定的时间,所以在子线程根据这个指针访问并保存数据前,主线程应等待子线程保存完毕后才能改动该参数并启动下一个线程。这涉及到主线程与子线程之间的同步。

2.子线程之间会互斥的改动和输出全局变量。要求全局变量的输出必须递增。这涉及到各子线程间的互斥。

原创粉丝点击