⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 teach_sp_44.htm

📁 VC专题教程 -- 第一章 Internet相关开发 打包下载 --- 1.1 如何编写CGI程序 ------ 1.2 一种更亲切的CGI开发系统WinCGI ------ 1.3 利用ISAPI开
💻 HTM
📖 第 1 页 / 共 2 页
字号:

<!-- LANGUAGE='JavaScript'>write_body();<-->
<!-- LANGUAGE='JavaScript'>write_bar();<-->



<table width=98% cellspacing="0" cellpadding="0" align=center><!--整体框架-->
<tr><td>

<table border=0 width="100%" cellspacing="0" cellpadding="2"><!--标记放置区域-->
<tr>
	<td width="30%" align="center" bgcolor="#8E8E8E" valign=middle><img src=../../img/brand_200_60.gif width=200 height=60 alt="LOGO1"></td>
	<td width="70%" align="center" bgcolor="#8E8E8E" valign=middle><!-- LANGUAGE='JavaScript'>write_ban();<--></td>
</tr>
<tr>
	<td colspan="2" bgcolor="#939393" align=center><font color=white>您当前位置</font> <a href=../../index.htm><font color=white>首页</font></a> <a href=../index.htm><font color=white>开发教程</font></a> <a href=index.htm><font color=white><font class=engul>Visual C++/MFC</font>专题讲座</font></a> <font color=white>4.4 进程/线程间同步</font> <font color=white><!-- LANGUAGE='JavaScript'>write_command();<--></font></td>
</tr>
</table><!--标记放置区域 END-->

<table border=0 width=100% cellspacing="0" cellpadding="0">
<tr bgcolor="#F4F4F4">
<td><!-- article title begin here-->
<br>
<p align=center><big>4.4 进程/线程间同步</big></p>
<p>这一节的内容比较多请你耐心的看完,因为进程/线程间同步的方法比较多,每种方法都有不同的用途:这节中会讲通过<a href=#critical_section>临界区</a>,<a href=#mutex>互斥量</a>,<a href=#semaphore>信号灯</a>,<a href=#event>事件</a>来进行同步。
<p>由于进程/线程间的操作是并行进行的,所以就产生了一个数据的问题同步,我们先看一段代码:
<pre>
int iCounter=0;//全局变量
DOWRD threadA(void* pD)
{
	for(int i=0;i<100;i++)
	{
		int iCopy=iCounter;
		//Sleep(1000);
		iCopy++;
		//Sleep(1000);
		iCounter=iCopy;
	}
}
</pre>
现在假设有两个线程threadA1和threadA2在同时运行那么运行结束后iCounter的值会是多少,是200吗?不是的,如果我们将Sleep(1000)前的注释去掉后我们会很容易明白这个问题,因为在iCounter的值被正确修改前它可能已经被其他的线程修改了。这个例子是一个将机器代码操作放大的例子,因为在CPU内部也会经历数据读/写的过程,而在线程执行的过程中线程可能被中断而让其他线程执行。<font color=red>变量iCounter在被第一个线程修改后,写回内存前如果它又被第二个线程读取,然后才被第一个线程写回,那么第二个线程读取的其实是错误的数据,这种情况就称为脏读(dirty read)。</font>这个例子同样可以推广到对文件,资源的使用上。
<p>那么要如何才能避免这一问题呢,假设我们在使用iCounter前向其他线程询问一下:有谁在用吗?如果没被使用则可以立即对该变量进行操作,否则等其他线程使用完后再使用,而且在自己得到该变量的控制权后其他线程将不能使用这一变量,直到自己也使用完并释放为止。经过修改的伪代码如下:
<pre>
int iCounter=0;//全局变量
DOWRD threadA(void* pD)
{
	for(int i=0;i<100;i++)
	{
		ask to lock iCounter
		wait other thread release the lock
		lock successful
		
		{
			int iCopy=iCounter;
			//Sleep(1000);
			iCopy++;
		}
		iCounter=iCopy;
		
		release lock of iCounter
	}
}
</pre>
<p>幸运的是OS提供了多种同步对象供我们使用,并且可以替我们管理同步对象的加锁和解锁。我们需要做的就是对每个需要同步使用的资源产生一个同步对象,在使用该资源前申请加锁,在使用完成后解锁。接下来我们介绍一些同步对象:
<a name=critical_section></a><p>临界区:临界区是一种最简单的同步对象,它只可以在同一进程内部使用。它的作用是保证只有一个线程可以申请到该对象,例如上面的例子我们就可以使用临界区来进行同步处理。几个相关的API函数为:
<ul>
<li>VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection );产生临界区
</li>
<li>VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection );删除临界区
</li>
<li>VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection );进入临界区,相当于申请加锁,如果该临界区正被其他线程使用则该函数会等待到其他线程释放
</li>
<li>BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection );进入临界区,相当于申请加锁,和EnterCriticalSection不同如果该临界区正被其他线程使用则该函数会立即返回FALSE,而不会等待
</li>
<li>VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection );退出临界区,相当于申请解锁
</li>
</ul>
<p>下面的示范代码演示了如何使用临界区来进行数据同步处理:
<pre>
//全局变量
int iCounter=0;
CRITICAL_SECTION criCounter;

DWORD threadA(void* pD)
{
	int iID=(int)pD;
	for(int i=0;i<8;i++)
	{
		EnterCriticalSection(&criCounter);
		int iCopy=iCounter;
		Sleep(100);
		iCounter=iCopy+1;
		printf("thread %d : %d\n",iID,iCounter);
		LeaveCriticalSection(&criCounter);
	}
	return 0;
}
//in main function
{
		//创建临界区
		InitializeCriticalSection(&criCounter);
		//创建线程
		HANDLE hThread[3];
		CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
		CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
		CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
		hThread[0]=pT1->m_hThread;
		hThread[1]=pT2->m_hThread;
		hThread[2]=pT3->m_hThread;
		//等待线程结束
		//至于WaitForMultipleObjects的用法后面会讲到。
		WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
		//删除临界区
		DeleteCriticalSection(&criCounter);
		printf("\nover\n");

}
<a name=mutex></a></pre>
<p>接下来要讲互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。下面介绍可以用在互斥量上的API函数:
<pre>创建互斥量:
HANDLE CreateMutex(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,// 安全信息
  BOOL bInitialOwner,  // 最初状态,
  //如果设置为真,则表示创建它的线程直接拥有了该互斥量,而不需要再申请
  LPCTSTR lpName       // 名字,可以为NULL,但这样一来就不能被其他线程/进程打开
);
打开一个存在的互斥量:
HANDLE OpenMutex(
  DWORD dwDesiredAccess,  // 存取方式
  BOOL bInheritHandle,    // 是否可以被继承
  LPCTSTR lpName          // 名字
);
释放互斥量的使用权,但要求调用该函数的线程拥有该互斥量的使用权:
BOOL ReleaseMutex(//作用如同LeaveCriticalSection
  HANDLE hMutex   // 句柄
);
关闭互斥量:
BOOL CloseHandle(
  HANDLE hObject   // 句柄
);
<a name=waitfor></a></pre>
<p><font color=red>你会说为什么没有名称如同EnterMutex,功能如同EnterCriticalSection一样的函数来获得互斥量的使用权呢?的确没有!</font>获取互斥量的使用权需要使用函数:
<pre>
DWORD WaitForSingleObject(
  HANDLE hHandle,        // 等待的对象的句柄
  DWORD dwMilliseconds   // 等待的时间,以ms为单位,如果为INFINITE表示无限期的等待
);
返回:
WAIT_ABANDONED 在等待的对象为互斥量时表明因为互斥量被关闭而变为有信号状态
WAIT_OBJECT_0 得到使用权
WAIT_TIMEOUT 超过(dwMilliseconds)规定时间
</pre>
<p>在线程调用WaitForSingleObject后,如果一直无法得到控制权线程讲被挂起,直到超过时间或是获得控制权。
<p>讲到这里我们必须更深入的讲一下WaitForSingleObject函数中的对象(Object)的含义,这里的对象是一个具有信号状态的对象,对象有两种状态:有信号/无信号。而等待的含义就在于等待对象变为有信号的状态,对于互斥量来讲如果正在被使用则为无信号状态,被释放后变为有信号状态。当等待成功后WaitForSingleObject函数会将互斥量置为无信号状态,这样其他的线程就不能获得使用权而需要继续等待。WaitForSingleObject函数还进行排队功能,保证先提出等待请求的线程先获得对象的使用权,下面的代码演示了如何使用互斥量来进行同步,代码的功能还是进行全局变量递增,通过输出结果可以看出,先提出请求的线程先获得了控制权:
<pre>
int iCounter=0;

DWORD threadA(void* pD)
{
	int iID=(int)pD;
	//在内部重新打开
	HANDLE hCounterIn=OpenMutex(MUTEX_ALL_ACCESS,FALSE,"sam sp 44");

	for(int i=0;i<8;i++)
	{
		printf("%d wait for object\n",iID);
		WaitForSingleObject(hCounterIn,INFINITE);
		int iCopy=iCounter;
		Sleep(100);
		iCounter=iCopy+1;
		printf("\t\tthread %d : %d\n",iID,iCounter);
		ReleaseMutex(hCounterIn);
	}
	CloseHandle(hCounterIn);
	return 0;
}

//in main function
{
		//创建互斥量
		HANDLE hCounter=NULL;
		if( (hCounter=OpenMutex(MUTEX_ALL_ACCESS,FALSE,"sam sp 44"))==NULL)
		{
			//如果没有其他进程创建这个互斥量,则重新创建
			hCounter = CreateMutex(NULL,FALSE,"sam sp 44");
		}

		//创建线程
		HANDLE hThread[3];
		CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
		CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
		CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
		hThread[0]=pT1->m_hThread;
		hThread[1]=pT2->m_hThread;
		hThread[2]=pT3->m_hThread;
		//等待线程结束
		WaitForMultipleObjects(3,hThread,TRUE,INFINITE);

		//关闭句柄
		CloseHandle(hCounter);
	}
}
</pre>
<p><font color=red>在这里我没有使用全局变量来保存互斥量句柄,这并不是因为不能这样做,而是为演示如何在其他的代码段中通过名字来打开已经创建的互斥量。其实这个例子在逻辑上是有一点错误的,因为iCounter这个变量没有跨进程使用,所以没有必要使用互斥量,只需要使用临界区就可以了。</font>假设有一组进程在同时使用一个文件那么我们可以使用互斥量来保证该文件只同时被一个进程使用(如果只是利用OS的文件存取控制功能则需要添加更多的错误处理代码),此外在调度程序中也可以使用互斥量来对资源的使用进行同步化。
<p>现在我们回过头来讲WaitForSingleObject这个函数,从前面的例子中我们看到WaitForSingleObject这个函数将等待一个对象变为有信号状态,那么具有信号状态的对象有哪些呢?下面是一部分:
<ul>
<li>Mutex </li>
<li>Event </li>
<li>Semaphore </li>
<li>Job </li>
<li>Process </li>
<li>Thread </li>
<li>Waitable timer </li>
<li>Console input </li>
</ul>
<p><font color=red>互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作</font>,而其他的对象与数据同步操作无关,<font color=red>但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态</font>。所以我们可以使用WaitForSingleObject来等待进程和线程退出。(至于信号灯,事件的用法我们接下来会讲)我们在前面的例子中使用了WaitForMultipleObjects函数,这个函数的作用与WaitForSingleObject类似但从名字上我们可以看出,WaitForMultipleObjects将用于等待多个对象变为有信号状态,函数原型如下:
<pre>
DWORD WaitForMultipleObjects(
  DWORD nCount,             // 等待的对象数量
  CONST HANDLE *lpHandles,  // 对象句柄数组指针
  BOOL fWaitAll,            // 等待方式,
  //为TRUE表示等待全部对象都变为有信号状态才返回,为FALSE表示任何一个对象变为有信号状态则返回
  DWORD dwMilliseconds      // 超时设置,以ms为单位,如果为INFINITE表示无限期的等待
);
</pre>
<p>返回值意义:<br>
WAIT_OBJECT_0 到 (WAIT_OBJECT_0 + nCount – 1):当fWaitAll为TRUE时表示所有对象变为有信号状态,当fWaitAll为FALSE时使用返回值减去WAIT_OBJECT_0得到变为有信号状态的对象在数组中的下标。<br>
WAIT_ABANDONED_0 到 (WAIT_ABANDONED_0 + nCount – 1):当fWaitAll为TRUE时表示所有对象变为有信号状态,当fWaitAll为FALSE时表示对象中有一个对象为互斥量,该互斥量因为被关闭而成为有信号状态,使用返回值减去WAIT_OBJECT_0得到变为有信号状态的对象在数组中的下标。<br>
WAIT_TIMEOUT:表示超过规定时间。
<pre>
前面的例子中的如下代码表示等待三个线程都变为有信号状态,也就是说三个线程都结束。
		HANDLE hThread[3];
		CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
		CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
		CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
		hThread[0]=pT1->m_hThread;
		hThread[1]=pT2->m_hThread;
		hThread[2]=pT3->m_hThread;
		//等待线程结束
		WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
此外,在<a href=http://www.vchelp.net/cgi-bin/columnist/columnist_paper.pl?aid=wyy_cq&pid=runpro>启动和等待进程结束</a>一文中就利用这个功能等待进程结束。
<a name=semaphore></a></pre>
<p>通过互斥量我们可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,你的老板会要求你根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。对信号灯的操作伪代码大致如下:
<pre>
Semaphore sem=3;

dword threadA(void*)
{
	while(sem <= 0)
	{// 相当于 WaitForSingleObject
		wait ...
	}
	// sem > 0
	// lock the Semaphore
	sem -- ;
	do functions ...
	// release Semaphore
	sem ++ ;
	return 0;
}
</pre>
<p>这里信号灯有一个初始值,表示有多少进程/线程可以进入,当信号灯的值大于0时为有信号状态,小于等于0时为无信号状态,所以可以利用WaitForSingleObject进行等待,当WaitForSingleObject等待成功后信号灯的值会被减少1,直到释放时信号灯会被增加1。用于信号灯操作的API函数有下面这些:
<pre>
创建信号灯:
HANDLE CreateSemaphore(
  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,// 安全属性,NULL表示使用默认的安全描述
  LONG lInitialCount,  // 初始值
  LONG lMaximumCount,  // 最大值
  LPCTSTR lpName       // 名字
);
打开信号灯:
HANDLE OpenSemaphore(
  DWORD dwDesiredAccess,  // 存取方式
  BOOL bInheritHandle,    // 是否能被继承
  LPCTSTR lpName          // 名字
);
释放信号灯:
BOOL ReleaseSemaphore(
  HANDLE hSemaphore,   // 句柄
  LONG lReleaseCount,  // 释放数,让信号灯值增加数
  LPLONG lpPreviousCount   // 用来得到释放前信号灯的值,可以为NULL
);
关闭信号灯:
BOOL CloseHandle(
  HANDLE hObject   // 句柄
);
</pre>

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -