线程同步和线程死锁

时间:2022-06-11 23:29:13

 

    在上一篇《网络编程——多线程技术》中已经说过,在一如多线程技术之后,在一个进程中可以创建多个线程,这多个线程在需要访问同一个资源时,肯定会发生争用现象,在争夺资源的过程中,假如第一个线程先访问这一资源,并对其做了修改,在这个线程没有执行完毕但时间片到了,第二个线程又访问该资源,就可能得到错误的结果。这是非常严重的问题。为了解决这一问题,引入了进程同步的概念。实现线程同步,可以有多种方法。在《网络编程——多线程技术》的火车票售票程序我们使用了创建互斥对象来实现线程同步。这里再介绍另外两种实现线程同步的方法:分别是事件对象和关键代码段(也成为临界区)。下面详细介绍这两种线程同步的方法:

一、用“事件对象”实现线程同步

    事件对象和互斥对象一样都属于内核对象,它包含一个使用计数,一个用于标识该事件是一个自动重置还是一个人工重置的布尔值,和另一个用于指定该事件处于已通知状态还是未通知状态的布尔值。 事件对象可分为两种,一种是人工重置的,另一种是自动重置的。当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。而当一个自动事件得到通知时,等待该事件的所有线程中只有一个线程变为可调度线程。我们可以像使用互斥对象一样来使用事件对象。

    要使用事件对象,首先要创建事件对象,创建事件对象的函数是:CreateEvent(),该函数的第二个参数指明该事件对象是自动重置的事件对象还是人工重置的事件对象,第三个参数可以设定该事件对象的初始状态是否为有信号状态。

    事件对象只有在有信号状态下线程才有可能申请获得该事件的所有权,但是当某一线程获得所有权后,应当将该事件对象设定为无信号状态,实现线程同步。设定事件对象为有信号状态的函数为:SetEvent(),设定事件对象为无信号状态的函数为:ResetEvent()

下面还用火车票售票的程序示例来实现事件对象实现的线程同步:

#include "stdafx.h"
#include <Windows.h>
#include <iostream>
using namespace std;

DWORD WINAPI ThreadProc1(
 LPVOID lpParameter   // thread data
 );
DWORD WINAPI ThreadProc2(
 LPVOID lpParameter   // thread data
 );

int tickets=100;
HANDLE hEvent;
int _tmain(int argc, _TCHAR* argv[])
{
 HANDLE hThread1;
 HANDLE hThread2;
 hThread1=CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
 hThread2=CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
 CloseHandle(hThread1);
 CloseHandle(hThread2);
 hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);
 SetEvent(hEvent);

 Sleep(4000);
 CloseHandle(hEvent);
 system("pause");
 return 0;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
 while (TRUE)
 {
  WaitForSingleObject(hEvent,INFINITE);
  if (tickets>0)
  {
   Sleep(1);
   cout<<"Thread1 sell tickets:"<<tickets--<<endl;
  }
  else
   break;
  SetEvent(hEvent);
 }
 return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
 while (TRUE)
 {
  WaitForSingleObject(hEvent,INFINITE);
  if (tickets>0)
  {
   Sleep(1);
   cout<<"Thread2 sell tickets:"<<tickets--<<endl;
  }
  else
   break;
  SetEvent(hEvent);
 }
 return 0;
}

运行结果如下:

线程同步和线程死锁


上面的示例,我们创建了自动重置的事件对象,如果创建人工重置的事件对象,当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。这样线程1和线程2都可以访问临界资源,起不到线程同步的效果。

而且自动重置的事件对象得到通知时,等待该事件的所有线程中只有一个线程变为可调度线程。同时操作系统自动将该事件对象设置为无信号状态。所以当某一线程运行结束,应该调用SetEvent(hEvent);将事件对象设置为有信号状态,供下一线程调用。

二、关键代码段(临界区)实现线程同步

    用关键代码段对关键代码的保护也可以实现线程同步,使某一时刻只有一个线程访问临界资源。使用关键代码段实现线程同步,首先要创建临界区对象,当某一线程进入该临界区后,可以独占对资源的访问权。当然为了让下一个线程也能进入临界区,前一个线程访问完毕,应离开临界区。实现函数如下:

InitializeCriticalSection()初始化临界区对象

EnterCriticalSection()等待临界区对象的所有权

LeaveCriticalSection()释放临界区兑现的所有权

DeleteCriticalSection()程序退出之前释放临界区对象的所有资源

仍然用火车票售票程序实现关键代码段实现的线程同步:

#include "stdafx.h"
#include <Windows.h>
#include <iostream>
using namespace std;

DWORD WINAPI ThreadProc1(
 LPVOID lpParameter   // thread data
 );
DWORD WINAPI ThreadProc2(
 LPVOID lpParameter   // thread data
 );

int tickets=100;
CRITICAL_SECTION cs;
int _tmain(int argc, _TCHAR* argv[])
{
 HANDLE hThread1;
 HANDLE hThread2;
 hThread1=CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
 hThread2=CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
 CloseHandle(hThread1);
 CloseHandle(hThread2);
 InitializeCriticalSection(&cs);
 Sleep(4000);
 DeleteCriticalSection(&cs);
 system("pause");
 return 0;
}

DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
 while (TRUE)
 {
  EnterCriticalSection(&cs);
  if (tickets>0)
  {
   Sleep(1);
   cout<<"Thread1 sell tickets:"<<tickets--<<endl;
  }
  else
   break;
  LeaveCriticalSection(&cs);
 }
 return 0;
}
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
 while (TRUE)
 {
  EnterCriticalSection(&cs);
  if (tickets>0)
  {
   Sleep(1);
   cout<<"Thread2 sell tickets:"<<tickets--<<endl;
  }
  else
   break;
  LeaveCriticalSection(&cs);
 }
 return 0;
}

运行结果如下:

线程同步和线程死锁

三、线程死锁

    假如线程1拥有资源A的访问权等待资源B才能运行,而线程2拥有资源B而要获得资源A才能运行。但是二者都不愿意先释放自己拥有的资源,这样就一直僵持下去,这样就造成了线程死锁。线程死锁的经典问题就是“哲学家进餐问题”。

    在线程同步的三种方法中,关键代码段是比较方便的,而且同步速度比较快。但是关键代码段最容易发生线程死锁现象。所以在使用关键代码段时一定要注意线程死锁的问题。下面的火车票售票程序就发生了线程死锁:

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

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

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

int tickets=100;

CRITICAL_SECTION csA;
CRITICAL_SECTION csB;

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

 InitializeCriticalSection(&csA);
 InitializeCriticalSection(&csB);

 Sleep(4000);

 DeleteCriticalSection(&csA);
 DeleteCriticalSection(&csB);

}

DWORD WINAPI Fun1Proc(
  LPVOID lpParameter   // thread data
)
{
 while(TRUE)
 {
  EnterCriticalSection(&csA);
  Sleep(1);
  EnterCriticalSection(&csB);
  if(tickets>0)
  {
   Sleep(1);
   cout<<"thread1 sell ticket : "<<tickets--<<endl;
  }
  else
   break;
  LeaveCriticalSection(&csB);
  LeaveCriticalSection(&csA);

 }
 
 return 0;
}

DWORD WINAPI Fun2Proc(
  LPVOID lpParameter   // thread data
)
{
 
 while(TRUE)
 {
  EnterCriticalSection(&csB);
  Sleep(1);
  EnterCriticalSection(&csA);
  if(tickets>0)
  {
   Sleep(1);
   cout<<"thread2 sell ticket : "<<tickets--<<endl;
  }
  else
   break;
  LeaveCriticalSection(&csA);
  LeaveCriticalSection(&csB);

 }
 cout<<"thread2 is running!"<<endl;
 return 0;
}

上面的示例就是线程1先获得临界区对象A,然后线程B获得临界区对象B,然后线程1又等待临界区对象B,而线程2又等待临界区对象A,二者就这样僵持下去,从而发生了线程死锁。在使用关键代码段时一定要避免线程死锁的发生。