Windows多线程程序设计之线程同步分析(结合互斥对象)(上)

原创 2013年06月23日 13:16:54

Windows中的多线程程序设计一直是所有编程人员感到困难的一个地方,主要是在Windows多线程程序设计中,往往要考虑很多的东西,线程启用的多少,线程之间的同

步问题等等...   这里我将将我学习过程中碰到的问题总结在此,如果有遇到相同困惑的人也能够尽快的明白过来。


MultiThread1.cpp

#include <windows.h>
#include <iostream>

using namespace std ;

DWORD WINAPI ThreadProc1(LPVOID lpParameter) ;
DWORD WINAPI ThreadProc2(LPVOID lpParameter) ;

int index ;
int tickets = 100 ;
HANDLE hMutex ;			// 声明一个全局的互斥对象句柄

int main()
{
	HANDLE hThread1 ;
	HANDLE hThread2 ;

	hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL) ;
	hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL) ;
	CloseHandle(hThread1) ;		// 关闭线程内核对象句柄, 并没有阻止新建线程的结束, 但是将线程内核对象的引用计数减1, 
	CloseHandle(hThread2) ;		// 当新建线程执行完成后, 引用计数也会减1。 当引用计数为0时, 系统就会释放线程内核对象
					// 如果此处不使用CloseHandle()函数关闭新建线程内核对象, 则在该线程执行完成后, 其引用计数也不会为0,
					// 则该线程内核对象会一直存在在系统内核当中, 所以, 在我们创建了一个新线程之后, 如果后面的代码不会
					// 用到此线程内核对象, 则应该调用CloseHandle()函数关闭该线程的线程内核对象

	hMutex = CreateMutex(NULL, FALSE, NULL) ;	// 创建一个不属于当前线程的匿名互斥对象

	Sleep(4000) ;

	return 0 ; 
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter) 
{	
	while (TRUE)
	{
		WaitForSingleObject(hMutex, INFINITE) ;
		if (tickets > 0)
		{
			Sleep(1) ;
			cout << "thread one sell ticket: " << tickets-- << endl ;
		}
		else 
			break ;

		ReleaseMutex(hMutex) ;		// 释放hMutex互斥对象的所有权
	}

	return 0 ;
}

DWORD WINAPI ThreadProc2(LPVOID lpParameter) 
{
	while (TRUE)
	{
		WaitForSingleObject(hMutex, INFINITE) ;
		if (tickets > 0)
		{
			Sleep(1) ;
			cout << "thread two sell ticket: " << tickets-- << endl ;
		}
		else 
			break ;

		ReleaseMutex(hMutex) ;
	}
	
	return 0 ;
}


代码如上,我们先看看运行的结果,如下:


我们可以看到线程一和线程二之间交替的执行,直到将票卖完。   那么这样的结果,肯定是缺少不了线程之间的同步,那么Windows程序中,线程之间是如何维持线程直

接的同步的呢??   这也主要靠windows操作系统为我们提供的一个内核对象 ------> (互斥对象) 来实现的。下面我们将主要分析程序是如何执行的;

分析:

当我们创建互斥对象(hMutex)时(上述代码中的第26行), 第二个参数传递为FALSE,那么当前就没有线程拥有该互斥对象(hMutex)的所有权,那么操作系统就会将该互斥

对象(hMutex)设置为已通知状态,也就是信号态;当程序执行到第一个线程(ThreadProc1)的WaitForSingleObject()函数时,因为此时的互斥对象(hMutex)处于信

号态,那么该线程(ThreadProc1)就请求获得了该互斥对象(hMutex)的所有权,那么操作系统就会将该互斥对象(hMutex)的线程ID设置成线程一(ThreadProc1)的线

程ID,此时,线程一(ThreadProc1)便拥有了该互斥对象(hMutex),同时,操作系统立即将该互斥对象(hMutex)设置为未通知状态(即非信号态),程序继续向下运行,

执行到Sleep()函数时,线程一(ThreadProc1)休眠,这时,操作系统便会选择线程二(ThreadProc2)开始运行,同样执行到WaitForSingleObject()函数,因为此时互斥对

象(hMutex)被线程一(ThreadProc1)所拥有,处于未通知状态(即非信号态),这样线程二(ThreadProc2)便不能获得该互斥对象(hMutex)的所有权,WaitForSingleObject()

函数便处于等待状态,直到线程1(ThreadProc1)休眠结束,继续执行线程一(ThreadProc1)中的后续代码(即线程一(ThreadProc1)中Sleep()函数后的代码),线程一

(ThreadProc1)的工作完成后,我们便要释放线程一(ThreadProc1)对该互斥对象(hMutex)的所有权(即ReleaseMutex()的操作),当我们调用ReleaseMutex()函数时, 操

作系统便会将该互斥对象(hMutex)的线程ID置为0并且将该互斥对象(hMutex)设置为已通知状态(即信号态),而此时处于等待状态的线程二(ThreadProc2)便立即请求到该互

斥对象(hMutex)的所有权,同时操作系统立即会将互斥对象(hMutex)的线程ID设置为线程二的线程ID并且将其(hMutex)状态转变为未通知状态(即非信号态),线程二

(ThreadProc2)中的相关代码继续向下执行,当线程二(ThreadProc2)的工作完成后, 也会调用ReleaseMutex()函数释放线程二(ThreadProc2)对互斥对象(hMutex)的所有权 -

---> 这样程序会一直在线程一(ThreadProc1)和线程二(ThreadProc2)之间交替的运行直到数据的处理完毕为止。而且,这样做还能保持线程之间的同步,对数据的处理会

更加的安全。


现在应该对互斥对象以及windows是如何通过互斥对象来实现多个线程之间的同步的。


我们知道互斥对象使操作系统维护的一个数据结构,那么该数据结构都包含什么东西呢??

 * 互斥对象

   -- 互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。
   -- 互斥对象包含一个使用数量,一个线程ID和一个计数器。(如果该互斥对象属于那个线程,则该互斥对象的线程ID便设置为拥有者线程的线程ID)
   -- ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。


可以看到互斥对象除了一个线程ID还有一个计数器,那么,我们再来看看计数器是怎么回事以及它是如何运作的,在此之前,我们先来看一段代码,如下:

MultiThread2.cpp

#include <windows.h>
#include <iostream>

using namespace std ;

DWORD WINAPI ThreadProc1(LPVOID lpParameter) ;
DWORD WINAPI ThreadProc2(LPVOID lpParameter) ;

int index ;
int tickets = 100 ;
HANDLE hMutex ;			// 声明一个全局的互斥对象句柄

int main()
{
	HANDLE hThread1 ;
	HANDLE hThread2 ;

	hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL) ;
	hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL) ;
	CloseHandle(hThread1) ;			// 关闭线程内核对象句柄, 并没有阻止新建线程的结束, 但是将线程内核对象的引用计数减1, 
	CloseHandle(hThread2) ;			// 当新建线程执行完成后, 引用计数也会减1。 当引用计数为0时, 系统就会释放线程内核对象
						// 如果此处不使用CloseHandle()函数关闭新建线程内核对象, 则在该线程执行完成后, 其引用计数也不会为0,
						// 则该线程内核对象会一直存在在系统内核当中, 所以, 在我们创建了一个新线程之后, 如果后面的代码不会
						// 用到此线程内核对象, 则应该调用CloseHandle()函数关闭该线程的线程内核对象

//	hMutex = CreateMutex(NULL, FALSE, NULL) ;	// 创建一个不属于当前线程的匿名互斥对象

	hMutex = CreateMutex(NULL, TRUE, NULL) ;	// 创建一个属于当前线程的匿名互斥对象
	ReleaseMutex(hMutex) ;				// 释放当前线程对互斥对象的所有权

	Sleep(4000) ;

	return 0 ; 
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter) 
{	
	while (TRUE)
	{
		WaitForSingleObject(hMutex, INFINITE) ;
		if (tickets > 0)
		{
			Sleep(1) ;
			cout << "thread one sell ticket: " << tickets-- << endl ;
		}
		else 
			break ;

		ReleaseMutex(hMutex) ;		// 释放hMutex互斥对象的所有权
	}

	return 0 ;
}

DWORD WINAPI ThreadProc2(LPVOID lpParameter) 
{
	while (TRUE)
	{
		WaitForSingleObject(hMutex, INFINITE) ;
		if (tickets > 0)
		{
			Sleep(1) ;
			cout << "thread two sell ticket: " << tickets-- << endl ;
		}
		else 
			break ;

		ReleaseMutex(hMutex) ;
	}
	
	return 0 ;
}


和MultiThread1.cpp的代码差不多,只是略微的修改了一下,将MultiThread1.cpp代码中的第26行注释掉,在第28行重新创建一个互斥对象(注意这两行(26和28)代码的

区别,并且在29行又多添加了一行代码,【关于这两行代码的意义,在程序的注释中写的很详细】我们运行程序,看看结果,如下:


程序也是能够正常的执行,分析可以结合MultiThread1.cpp程序的分析,自己研究。   

我们继续向下,MultiThread2.cpp程序也是能够正常的执行,但是如果我们在MultiThread2.cpp程序的第28和第29行之间加上一行代码,完整代码如下:

MultiThread3.cpp

#include <windows.h>
#include <iostream>

using namespace std ;

DWORD WINAPI ThreadProc1(LPVOID lpParameter) ;
DWORD WINAPI ThreadProc2(LPVOID lpParameter) ;

int index ;
int tickets = 100 ;
HANDLE hMutex ;			// 声明一个全局的互斥对象句柄

int main()
{
	HANDLE hThread1 ;
	HANDLE hThread2 ;

	hThread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL) ;
	hThread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL) ;
	CloseHandle(hThread1) ;			// 关闭线程内核对象句柄, 并没有阻止新建线程的结束, 但是将线程内核对象的引用计数减1, 
	CloseHandle(hThread2) ;			// 当新建线程执行完成后, 引用计数也会减1。 当引用计数为0时, 系统就会释放线程内核对象
						// 如果此处不使用CloseHandle()函数关闭新建线程内核对象, 则在该线程执行完成后, 其引用计数也不会为0,
						// 则该线程内核对象会一直存在在系统内核当中, 所以, 在我们创建了一个新线程之后, 如果后面的代码不会
						// 用到此线程内核对象, 则应该调用CloseHandle()函数关闭该线程的线程内核对象

//	hMutex = CreateMutex(NULL, FALSE, NULL) ;	// 创建一个不属于当前线程的匿名互斥对象

	hMutex = CreateMutex(NULL, TRUE, NULL) ;	// 创建一个属于当前线程的匿名互斥对象
	WaitForSingleObject(hMutex, INFINITE) ;
	ReleaseMutex(hMutex) ;				// 释放当前线程对互斥对象的所有权

	Sleep(4000) ;

	return 0 ; 
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter) 
{	
	while (TRUE)
	{
		WaitForSingleObject(hMutex, INFINITE) ;
		if (tickets > 0)
		{
			Sleep(1) ;
			cout << "thread one sell ticket: " << tickets-- << endl ;
		}
		else 
			break ;

		ReleaseMutex(hMutex) ;		// 释放hMutex互斥对象的所有权
	}

	return 0 ;
}

DWORD WINAPI ThreadProc2(LPVOID lpParameter) 
{
	while (TRUE)
	{
		WaitForSingleObject(hMutex, INFINITE) ;
		if (tickets > 0)
		{
			Sleep(1) ;
			cout << "thread two sell ticket: " << tickets-- << endl ;
		}
		else 
			break ;

		ReleaseMutex(hMutex) ;
	}
	
	return 0 ;
}


和上面的两个程序(MultiThread1.cpp和MultiThread2.cpp)一样,先来看看运行结果,如下:

  

声明:我在运行程序时候出现了两种结果,如上,读者可以自己运行看看,不过我觉得第一个结果出现的概率应该非常低(可遇而不可求嘛)

我们在来分析一下程序的执行过程,如下:

分析

代码中的第28行中创建了一个互斥对象(hMutex),并且被当前线程(主线程main)所拥有(因为CreaeMutex()函数的第二个参数为TRUE),之后,程序向下执行到

WaitForSingleObject()函数时, 操作系统会去判断拥有的互斥对象(hMutex)的线程(主线程main)的线程ID是否等于请求线程(此处,请求的线程就是主线程main)的线程ID,

如果相等,那么即使此时互斥对象(hMutex)处于未通知状态(即非信号态),当前的请求线程(主线程main)也能获得此互斥对象(hMutex)的所有权,那么

WaitForSingleObject()函数就会返回。【 对于一个线程多次拥有一个互斥对象是通过互斥对象内部的计数器来记录的】当我们第一次创建互斥对象是(hMutex),主线程拥

有了这个互斥对象(hMutex),操作系统除了将该互斥对象(hMutex)的线程ID设置为主线程的线程ID,同时将该互斥对象(hMutex)的计数器变为1,当用WaitForSingleObject()

函数去请求该互斥对象(hMutex)时,而此时的互斥对象(hMutex)是处于未通知状态(即非信号态)的,但是此时请求互斥对象(hMutex)的线程ID等于该互斥对象(hMutex)中的

线程ID,所以我们仍能够请求到该互斥对象,那么操作系统通过互斥对象中的计数器来记录我们请求了多少次互斥对象,于是,此时该互斥对象(hMutex)在加1变为2,当我

们在调用ReleaseMutex()函数时,实际上是将互斥对象内部的计数器进行减1操作,当程序中的互斥对象(hMutex)中的计数器减1操作后,其计数器的值还剩1,这样主线程

还是没有失去对该互斥对象(hMutex)的的所有权,那么,该互斥对象(hMutex)还是处于未通知状态,所以,线程一和线程二就不能请求到对该互斥对象(hMetex)的所有权, 

故线程一和线程二中的代码就不会被执行。


如果想要线程一和线程二中的代码被执行,我们可以在main()函数中再次调用ReleaseMutex()函数,将互斥对象(hMetex)的的计数器递减为0。

我测试程序是可以正常的执行的,这也和我们根据分析得到的结果一致,是不是很高兴啊 !!!  

具体的代码就不贴出来了,程序运行的结果和MultThread1.cpp运行的结果一样。

有兴趣的话,大家可以自己试一试 !!!


<MFC笔记> 四种线程同步(或互斥)方式小结

一,什么是线程同步? 同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。这里的同步千万不要理解成那个同时进行,应是指协同、协助、互相配合。 线程同步是指多线程通过特定的设置(如互斥量,...
  • EbowTang
  • EbowTang
  • 2014年06月10日 21:35
  • 1976

多线程同步互斥实例——多个线程共享数据

• 实例问题         设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1,写出程序。这是一道java线程面试的面试题,这道题在网上有很多答案。那么,答案是如何来的呢? 简单来...
  • lu930124
  • lu930124
  • 2016年04月26日 17:14
  • 1743

秒杀多线程第七篇 经典线程同步 互斥量Mutex

阅读本篇之前推荐阅读以下姊妹篇: 《秒杀多线程第四篇一个经典的多线程同步问题》 《秒杀多线程第五篇经典线程同步关键段CS》 《秒杀多线程第六篇经典线程同步事件Event》   前面介绍了关键...
  • lien0906
  • lien0906
  • 2015年04月15日 16:08
  • 1564

Windows多线程程序设计之线程同步分析(结合事件对象)(下)

本篇介绍命名的事件对象, 命名的事件对象的功能和原理其实和命名的互斥对象的功能和原理差不错, 所以在此篇中就不在进行详细的说明了, 如有疑问请看我的《Windows多线程程序设计之线程同步分析(结合互...
  • CTO_51
  • CTO_51
  • 2013年06月26日 22:44
  • 912

windows多线程系列002 利用互斥对象实现线程同步

接着上一篇文章我们继续讨论多线程的问题,这一次我们利用互斥对象(mutex)来解决火车站售票同步问题。 1 利用互斥对象实现线程同步 互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的...
  • DaveBobo
  • DaveBobo
  • 2016年04月11日 20:56
  • 524

Windows多线程总结(3)-- 线程同步(使用互斥对象实现线程同步)

上一篇说明了多线程是存在着问题的,起始就是多线程操作同一数据而不同步的问题。那么如果实现线程的同步呢?          线程的同步有多种实现方式:         互斥内核对象、事件内核对象、可等待...
  • oBuYiSeng
  • oBuYiSeng
  • 2015年11月27日 18:44
  • 716

多线程实例入门--利用互斥对象实现线程同步

c++利用windows的API来实现多线程的功能 #include #include using namespace std; DWORD WINAPI Fun(LPV...
  • so__sunshine
  • so__sunshine
  • 2016年11月24日 11:19
  • 125

用C++和Windows的互斥对象(Mutex)来实现线程同步锁

准备知识:1,内核对象互斥体(Mutex)的工作机理,WaitForSingleObject函数的用法,这些可以从MSDN获取详情;2,当两个或 更多线程需要同时访问一个共享资源时,系统需要使用同步机...
  • u010800064
  • u010800064
  • 2013年12月16日 19:53
  • 360

VC++多线程,互斥对象,事件对象,关键代码段实现线程同步

// mlti thread.cpp : 定义控制台应用程序的入口点。 // /*#include "stdafx.h" #include using namespace std; ...
  • u011001084
  • u011001084
  • 2014年10月21日 19:46
  • 717

线程同步之临界区(类比互斥对象进行分析)

先来复习两个简单的英语单词吧:        critical : 临界的;关键的        section: 区域;区段          会拉屎的人,一定会利用临界区来实现线程同步。     ...
  • stpeace
  • stpeace
  • 2013年10月03日 23:09
  • 1247
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Windows多线程程序设计之线程同步分析(结合互斥对象)(上)
举报原因:
原因补充:

(最多只允许输入30个字)