线程同步

线程同步的三种方法,以模拟火车票订票为例:

第一种方法:(用互斥对象)

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

//线程1入口函数原型声明
DWORD WINAPI Fun1Proc( 
LPVOID lpParameter // thread data
);

//线程2入口函数原型声明
DWORD WINAPI Fun2Proc( 
LPVOID lpParameter // thread data
);


int index=0;
int tickets=100;
HANDLE hMutex;//创建互斥对象句柄
void main()
{
HANDLE hThread1,hThread2;
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);//Fun1Proc为线程的入口函数
CloseHandle(hThread1);/*关闭线程句柄,,并没有终止新创建的线程,只是表示在主线程中我们对新创建
的线程的引用不感兴趣,所以将它关闭当我们关闭句柄的时候,系统会递减新线程的内核对象的使用技术,
当我们所创建的线程执行完毕之后,系统也会递减线程内核对象的使用技术,当使用技术为0的时候,系统
就会释放线程内核对象,如果我们在主线程中没有关闭线程句柄,那么始终都会保留一个引用,那么这样的话
线程内核对象的引用技术就不会为0,即使我们这个线程执行完毕,线程内核对象也不会被释放,只有等到进程
终止的时候,系统才会为这些残留的对象做清理工作,所以我们应该再不再使用线程的时候将其关闭掉,让这个
线程的内核对象的引h用对象减1
*/
hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread2);
// while(index++<1000)
// cout<<"main thread is running"<<endl;
// Sleep(10);//使主线程暂停执行10毫秒,让线程1有机会运行。
hMutex=CreateMutex(NULL,FALSE,NULL);//创建一个匿名的互斥对象
Sleep(4000);//使主线程暂停执行10毫秒,让线程1有机会运行,此时主线程不占用cpu的执行时间

}


//线程1实现代码
DWORD WINAPI Fun1Proc( 
LPVOID lpParameter // thread data
)
{
// while(index++<1000)
// cout<<"thread 1 is running"<<endl;
while(TRUE)
{
WaitForSingleObject(hMutex,INFINITE);//请求互斥对象,用在需要被保护的代码前面,此时线程1处于未通知状态,即有信号状态
if(tickets>0)
{
Sleep(1);
cout<<"thread 1 sell ticket:"<<tickets--<<endl;
}
else
break;
ReleaseMutex(hMutex);//释放互斥对象,使其处于通知状态,即无信号状态
}

return 0;
}



//线程2实现代码
DWORD WINAPI Fun2Proc( 
LPVOID lpParameter // thread data
)
{
while(TRUE)
{
WaitForSingleObject(hMutex,INFINITE);//请求互斥对象,用在需要被保护的代码前面,此时线程2处于通知状态,即有信号状态
if(tickets>0)
{
Sleep(1);
cout<<"thread 2 sell ticket:"<<tickets--<<endl;
}
else
break;
ReleaseMutex(hMutex);//释放互斥对象,使其处于通知状态,即无信号状态
}

return 0;
}



/*
互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。
互斥对象包含一个使用数量,一个线程ID和一个计数器。(哪个线程拥有该互斥对象,就应该由哪个线程释放该互斥对象,其它线程只能释放本线程的互斥对象,
因为当一个线程拥有互斥对象的时候,互斥对象就为该线程绑定一个ID,不同线程的ID号不同,所以一个线程不能释放其它线程的互斥对象)
ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。(在一个线程中,调用N次互斥对象就应该相应的调用N次ReleaseMutex(hMutex),使互斥对象的计数器减1)
在一个线程中,如果在线程结束之前没有调用ReleaseMutex(hMutex),操作系统会在线程结束之后将互斥对象的ID置为0,计数器置为0,这样其它进程就可请求到该互斥对象了,为了养成良好的编程习惯还是得调用ReleaseMutex(hMutex)释放互斥对象。

*/



第二种方法:(用事件对象)

(1)事件对象也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。
(2)有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。


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

DWORD WINAPI Fun1Proc(
   LPVOID lpParameter    //thread data
   );

DWORD WINAPI Fun2Proc(
   LPVOID lpParameter   //thread data
   );

int tickets=100;
HANDLE g_hEvent;//保存创建的事件对象的句柄

void main()
{
	HANDLE hThread1,hThread2;
	hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
	hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

    g_hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);/*创建一个事件内核对象,设置初始状态为无信号状态;
	                                           注意,对于线程中的同步对象,不要采用人工重置的方式,应该使用
	                                           系统自动重置的方式,所以将第二个参数设为FALSE,因为这将会影响到各个线程的调用情况,详见后面附注*/
/*  g_hEvent=CreateEvent(NULL,FALSE,FALSE,"ticket");//创建一个命名的事件对象,可以替代上面的匿名事件对象,可起到同样的效果
	if(g_hEvent)
	{
		if(ERROR_ALREADY_EXISTS==GetLastError())
		{
			cout<<"only instance can run!"<<endl;
			return ;
		}
	}*/
	SetEvent(g_hEvent);//设置事件对象为有信号状态

	Sleep(4000);
}


//线程1实现代码
DWORD WINAPI Fun1Proc(  
					  LPVOID lpParameter   // thread data
					  )
{
	while(TRUE)
	{
		WaitForSingleObject(g_hEvent,INFINITE);//请求互斥对象,用在需要被保护的代码前面,此时线程1处于未通知状态,即有信号状态
		if(tickets>0)
		{
			Sleep(1);
			cout<<"thread 1 sell ticket:"<<tickets--<<endl;
		}
		else
			break;
		SetEvent(g_hEvent);//将事件对象设置为有信号状态
	}
	
	return 0;
}



//线程2实现代码
DWORD WINAPI Fun2Proc(  
					  LPVOID lpParameter   // thread data
					  )
{
	while(TRUE)
	{
		WaitForSingleObject(g_hEvent,INFINITE);//请求互斥对象,用在需要被保护的代码前面,此时线程2处于通知状态,即有信号状态
		if(tickets>0)
		{
			Sleep(1);
			cout<<"thread 2 sell ticket:"<<tickets--<<endl;
		}
		else
			break;
		SetEvent(g_hEvent);//将事件对象设置为有信号状态
	}
	
	return 0;
}

/*(事件对象)
  1、 事件对象也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,
另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。
  2、 有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时,等待该事件
的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。
*/


运行结果如下图



第三种方法:(用关键代码段)

(1)关键代码段(临界区)工作在用户方式下。

(2)关键代码段(临界区)是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权


需要调用到的4个函数分别为:

1、VOID InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

2、VOID EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection );

3、VOID LeaveCriticalSection( LPCRITICAL_SECTIONlpCriticalSection);

4、VOID DeleteCriticalSection( LPCRITICAL_SECTIONlpCriticalSection);


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

DWORD WINAPI Fun1Proc(
   LPVOID lpParameter    //thread data
   );

DWORD WINAPI Fun2Proc(
   LPVOID lpParameter   //thread data
   );

int tickets=100;
HANDLE g_hEvent;//保存创建的事件对象的句柄
CRITICAL_SECTION g_cs;//创建临界区对象

void main()
{
	HANDLE hThread1,hThread2;
	hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
	hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
	CloseHandle(hThread1);
	CloseHandle(hThread2);

    InitializeCriticalSection(&g_cs);//initializes a critical section object
	Sleep(4000);
    DeleteCriticalSection(&g_cs);// releases all resources used by an unowned critical section object
}


//线程1实现代码
DWORD WINAPI Fun1Proc(  
					  LPVOID lpParameter   // thread data
					  )
{
	while(TRUE)
	{
		EnterCriticalSection(&g_cs);//判断临界区的所有权是否被占用,如果未被占用则可以进入被保护代码段
		if(tickets>0)
		{
			Sleep(1);
			cout<<"thread 1 sell ticket:"<<tickets--<<endl;
		}
		else
			break;
		//这里注意得将临界区释放,否则线程2将无法得到执行的机会
		LeaveCriticalSection(&g_cs);// releases ownership of the specified critical section object
	}
	
	return 0;
}



//线程2实现代码
DWORD WINAPI Fun2Proc(  
					  LPVOID lpParameter   // thread data
					  )
{
	while(TRUE)
	{
		EnterCriticalSection(&g_cs);//判断临界区的所有权是否被占用,如果未被占用则可以进入被保护代码段
		if(tickets>0)
		{
			Sleep(1);
			cout<<"thread 2 sell ticket:"<<tickets--<<endl;
		}
		else
			break;
		LeaveCriticalSection(&g_cs);// releases ownership of the specified critical section object
	}
	
	return 0;
}


下面讨论一下互斥对象、事件对象与关键代码段的比较:

  • 互斥对象和事件对象属于内核对象,利用内核对象进行线程同步,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步
  • 关键代码段是工作在用户方式下,同步速度较快,但在使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值。
          在MFC编程中,我们一般 首选关键代码段,在编写代码的时候可以在构造函数中加入 VOID InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
在析构函数中调用 VOID DeleteCriticalSection( LPCRITICAL_SECTIONlpCriticalSection );然后在保护代码段的前面写 VOID EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection );在保护代码段结尾加上 VOID DeleteCriticalSection( LPCRITICAL_SECTIONlpCriticalSection );




  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值