网络编程——线程同步和线程死锁

标签: 网络 编程 线程 | 发表时间:2011-10-15 12:00 | 作者:乘风736 Xin
出处:http://www.cnblogs.com/

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

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

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

    要使用事件对象,首先要创建事件对象,创建事件对象的函数是: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,二者就这样僵持下去,从而发生了线程死锁。在使用关键代码段时一定要避免线程死锁的发生。

作者: 乘风736 发表于 2011-10-15 12:00 原文链接

评论: 0 查看评论 发表评论


最新新闻:
· iPad 3将于明年3月正式发布,产量将有所增加(2011-10-16 01:12)
· 营造类似黑客帝国360度旋转特效的全景照相机(视频)(2011-10-16 01:11)
· 50 人的 Google 自动驾驶汽车团队已经在跟主流汽车厂商讨论生产真车(2011-10-16 01:10)
· 三星律师未能在法庭上区分出iPad和Galaxy Tab(2011-10-16 01:10)
· Facebook变更用户量统计方法 Zynga受影响(2011-10-15 18:32)

编辑推荐:那些曾伴我走过编程之路的软件

网站导航:博客园首页  我的园子  新闻  闪存  小组  博问  知识库

相关 [网络 编程 线程] 推荐:

网络编程——线程同步和线程死锁

- Xin - 博客园-首页原创精华区
    在上一篇《网络编程——多线程技术》中已经说过,在一如多线程技术之后,在一个进程中可以创建多个线程,这多个线程在需要访问同一个资源时,肯定会发生争用现象,在争夺资源的过程中,假如第一个线程先访问这一资源,并对其做了修改,在这个线程没有执行完毕但时间片到了,第二个线程又访问该资源,就可能得到错误的结果.

epoll网络编程实例

- - CSDN博客推荐文章
       在前面已经经过了PPC、TPC、select之类( TPC就是使用进程处理data,TPC就是使用线程处理 ),前面两个的缺点大家应该都是知道的是吧,对于select( 其实poll和他差不多 ),缺点是能同时连接的fd是在是不多,在linux中一般是1024/2048,对于很大的服务器来说是不够的.

高性能网络编程5--IO复用与并发编程

- - CSDN博客云计算推荐文章
对于服务器的并发处理能力,我们需要的是:每一毫秒服务器都能及时处理这一毫秒内收到的数百个不同TCP连接上的报文,与此同时,可能服务器上还有数以十万计的最近几秒没有收发任何报文的相对不活跃连接. 同时处理多个并行发生事件的连接,简称为并发;同时处理万计、十万计的连接,则是高并发. 服务器的并发编程所追求的就是处理的并发连接数目无限大,同时维持着高效率使用CPU等资源,直至物理资源首先耗尽.

谈一谈网络编程学习经验(06-08更新)

- Leo - C++博客-首页原创精华区
PDF 版下载:https://github.com/downloads/chenshuo/documents/LearningNetworkProgramming.pdf. 本文谈一谈我在学习网络编程方面的一些个人经验. “网络编程”这个术语的范围很广,本文指用Sockets API开发基于TCP/IP的网络应用程序,具体定义见“网络编程的各种任务角色”一节.

网络与服务器编程框架库 acl_3.0.13 发布

- - 开源中国社区最新新闻
acl 包括以下丰富的常用函数库:. 1、常见网络应用库: SMTP 客户端库/PING 库/memcache 客户端库/handlersocket 客户端库/beanstalk 客户端库. 2、HTTP 网络库:HTTP 客户端/服务端库,C++版 HttpServlet 类,HTTP COOKIE/HTTP SESSION 等.

网络编程中的C10K问题总结

- - 三棵杏软件工作室
第一件秘密武器:epoll/IOCP/kqueue新模型       传统的web服务器采用同步socket处理,即每一线程服务于一个客户(apache就是这样),或者是使用传统的select/poll模型. 在连接数小的情况,性能也不会很差,但随着连接数的上升,性能会直线下降,超过一定数量时,会导致服务器无法提供服务.

高性能网络编程7--tcp连接的内存使用

- - CSDN博客互联网推荐文章
当服务器的并发TCP连接数以十万计时,我们就会对一个TCP连接在操作系统内核上消耗的内存多少感兴趣. socket编程方法提供了SO_SNDBUF、SO_RCVBUF这样的接口来设置连接的读写缓存,linux上还提供了以下系统级的配置来整体设置服务器上的TCP内存使用,但这些配置看名字却有些互相冲突、概念模糊的感觉,如下(sysctl -a命令可以查看这些配置):.

linux多线程编程的七条准则

- MadFrog - CSDN博客推荐文章
准则1:不依赖于信号收发的设计. 给其它进程以及自己发送异步信号并改变处理流程的设计不要做. 不要把信号和线程一起使用,这将使得程序动作的预测和调试变得困难. 准则2:要知道信号处理函数中可以做哪些处理. 在sigaction()函数登记的信号处理函数中可以做的处理是被严格限定的. 仅允许: 局部变量的相关处理.