Win32下两种用于C++的线程同步类(上)

2016-02-19 21:33 3 1 收藏

给自己一点时间接受自己,爱自己,趁着下午茶的时间来学习图老师推荐的Win32下两种用于C++的线程同步类(上),过去的都会过去,迎接崭新的开始,释放更美好的自己。

【 tulaoshi.com - 编程语言 】

线程同步是多线程程序设计的核心内容,它的目的是正确处理多线程并发时的各种问题,例如线程的等待、多个线程访问同一数据时的互斥,防死锁等。Win32提供多种内核对象和手段用于线程同步,如互斥量、信号量、事件、临界区等。所不同的是,互斥量、信号量、事件都是Windows的内核对象,当程序对这些对象进行控制时会自动转换到核心态,而临界区本身不是内核对象,它是工作在用户态的。我们知道从用户态转换到核心态是需要以时间为代价的,所以假如能在用户态就简单解决的问题,就可以不必劳烦核心态了。
  
  这里我要说的是两种用于C++的多线程同步类,通过对这两种类的使用就可以方便的实现对变量或代码段的加锁控制,从而防止多线程对变量不正确的操作。
  
  所谓加锁,就是说当我们要访问某要害变量之前,都需要首先获得答应才能继续,假如未获得答应则只有等待。一个要害变量拥有一把锁,一个线程必须先得到这把锁(其实称为钥匙可能更形象)才可以访问这个变量,而当某个变量持有这把锁的时候,其他线程就不能重复的得到它,只有等持有锁的线程把锁归还以后其他线程才有可能得到它。之所以这样做,就是为了防止一个线程读取某对象途中另一线程对它进行了修改,或两线程同时对一变量进行修改,例如:
  
  // 全局:
  strUCt MyStruct ... { int a, b; } ;
  MyStruct s;
  // 线程1:
  int a = s.a;
  int b = s.b;
  // 线程2:
  s.a ++ ;
  s.b -- ;
  假如实际的执行顺序就是上述书写的顺序那到没有什么,但假如线程2的执行打断了线程1,变为如下顺序:
  
  int a = s.a; //线程1
  s.a++; //线程2
  s.b++; //线程2
  int b = s.b; //线程1
  那么这时线程1读出来的a和b就会有问题了,因为a是在修改前读的,而b是在修改后读的,这样读出来的是不完整的数据,会对程序带来不可预料的后果。天知道两个程的调度顺序是什么样的。为了防止这种情况的出现,需要对变量s加锁,也就是当线程1得到锁以后就可以放心的访问s,这时假如线程2要修改s,只有等线程1访问完成以后将锁释放才可以,从而保证了上述两线程交叉访问变量的情况不会出现。
  
  使用Win32提供的临界区可以方便的实现这种锁:
  
  // 全局:
  CRITICAL_SECTION cs;
  InitializeCriticalSection( & cs);
  // 线程1:
  EnterCriticalSection( & cs);
  int a = s.a;
  int b = s.b;
  LeaveCriticalSection( & cs);
  // 线程2:
  EnterCriticalSection( & cs);
  s.a ++ ;
  s.b -- ;
  LeaveCriticalSection( & cs);
  // 最后:
  DeleteCriticalSection( & cs);
  代码中的临界区变量(cs)就可以看作是变量s的锁,当函数EnterCriticalSection返回时,当前线程就获得了这把锁,之后就是对变量的访问了。访问完成后,调用LeaveCriticalSection表示释放这把锁,答应其他线程继续使用它。
  
  假如每当需要对一个变量进行加锁时都需要做这些操作,显得有些麻烦,而且变量cs与s只有逻辑上的锁关系,在语法上没有什么联系,这对于锁的治理带来了不小的麻烦。程序员总是最懒的,可以想出各种偷懒的办法来解决问题,例如让被锁的变量与加锁的变量形成物理上的联系,使得锁变量成为被锁变量不可分割的一部分,这听起来是个好主意。
  
  首先想到的是把锁封闭在一个类里,让类的构造函数和析构函数来治理对锁的初始化和锁毁动作,我们称这个锁为“实例锁”:
  
  class InstanceLockBase
  ... {
  CRITICAL_SECTION cs;
  protected :
  InstanceLockBase() ... { InitialCriticalSection( & cs); }
  ~ InstanceLockBase() ... { DeleteCriticalSection( & cs); }
  } ;
  假如熟悉C++,看到这里一定知道后面我要干什么了,对了,就是继续,因为我把构造函数和析构函数都声明为保护的(protected),这样唯一的作用就是在子类里使用它。让我们的被保护数据从这个类继续,那么它们不就不可分割了吗:
  
  struct MyStruct: public InstanceLockBase
  ... { … } ;
  什么?结构体还能从类继续?当然,C++中结构体和类除了成员的默认访问控制不同外没有什么不一样,class能做的struct也能做。此外,也许你还会问,假如被锁的是个简单类型,不能继续怎么办,那么要么用一个类对这个简单类型进行封装(记得Java里有int和Integer吗),要么只好手工治理它们的联系了。假如被锁类已经有了基类呢?没关系,C++是答应多继续的,多一个基类也没什么。
  
  现在我们的数据里面已经包含一把锁了,之后就是要添加加锁和解锁的动作,把它们作为InstanceLockBase类的成员函数再合适不过了:
  
  class InstanceLockBase
  ... {
   CRITICAL_SECTION cs;
   void Lock() ... { EnterCriticalSection( & cs); }
   void Unlock() ... { LeaveCriticalSection( & cs); }
   …
  } ;
  看到这里可能会发现,我把Lock和Unlock函数都声明为私有了,那么如何访问这两个函数呢?是的,我们总是需要有一个地方来调用这两个函数以实现加锁和解锁的,而且它们总应该成对出现,但C++语法本身没能限制我们必须成对的调用两个函数,假如加完锁忘了解,那后果是严重的。这里有一个例外,就是C++对于构造函数和析构函数的调用是自动成对的,对了,那就把对Lock和Unlock的调用专门写在一个类的构造函数和析构函数中:
  
  
  class InstanceLock
  ... {
   InstanceLockBase * _pObj;
   public :
  InstanceLock(InstanceLockBase * pObj)
  ... {
   _pObj = pObj; // 这里会保存一份指向s的指针,用于解锁
   if (NULL != _pObj)
   _pObj - Lock(); // 这里加锁
  }
  ~ InstanceLock()
  ... {
   if (NULL != _pObj)
   _pObj - Unlock(); // 这里解锁
   }
  } ;
  最后别忘了在类InstanceLockBase中把InstanceLock声明为友元,使得它能正确访问Lock和Unlock这两个私有函数:
  
  class InstanceLockBase
  ... {
   friend class InstanceLock;
   …
  } ;
  好了,有了上面的基础,现在对变量s的加解锁治理变成了对InstanceLock的实例的生命周期的治理了。假如我们有一个函数ModifyS中要对s进行修改,那么只要在函数一开始就声明一个InstaceLock的实例,这样整个函数就自动对s加锁,一旦进入这个函数,其他线程就都不能获得s的锁了:
  
  void ModifyS()
  ... {
   InstanceLock lock ( & s); // 这里已经实现加锁了
   // some operations on s
  } // 一旦离开lock对象的作用域,自动解锁
  假如是要对某函数中一部分代码加锁,只要用一对大括号把它们括起来再声明一个lock就可以了:
  
  …
  ... {
   InstanceLock lock ( & s);
   // do something …
  }
  …
  好了,就是这么简单。下面来看一个测试。
  
  首先预备一个输出函数,对我们理解程序有帮助。它会在输出我们想输出的内容同时打出行号和时间:
  
  void Say( char * text)
  ... {
   static int count = 0 ;
   SYSTEMTIME st;
   ::GetLocalTime( & st);
   printf( " %03d [%02d:%02d:%02d.%03d]%s " , ++ count, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, text);
  }
  当然,原则上当多线程都调用这个函数时应该对其静态局部变量count进行加锁,这里就省略了。
  
  我们声明一个非常简单的被锁的类型,并生成一个实例:
  
  class MyClass: public InstanceLockBase
  ... {} ;
  MyClass mc;
  子线程的任务就是对这个对象加锁,然后输出一些信息:
  
  DWord CALLBACK ThreadProc(LPVOID param)
  ... {
   InstanceLock il( & mc);
   Say( " in sub thread, lock " );
   Sleep( 2000 );
   Say( " in sub thread, unlock " );
   return 0 ;
  }
  这里会输出两条信息,一是在刚刚获得锁的时间,二是在释放锁的时候,中间通过Sleep来延迟2秒。
  
  主线程负责开启子线程,然后也对mc加锁:
  
  CreateThread( 0 , 0 , ThreadProc, 0 , 0 , 0 );
  ... {
   InstanceLock il( & mc);
   Say( " in main thread, lock " );
   Sleep( 3000 );
   Say( " in main thread, lock " );
  }
  运行此程序,得到的输出如下:
  
  001 [13:43:23.781]in main thread, lock
  002 [13:43:26.781]in main thread, lock
  003 [13:43:26.781]in sub thread, lock
  004 [13:43:28.781]in sub thread, unlock
  从其输出的行号和时间可以清楚的看到两个线程间的互斥:当主线程恰好首先获得锁时,它会延迟3秒,然后释放锁,之后子线程才得以继续进行。这个例子也证实我们的类工作的很好。
  
  总结一下,要使用InstanceLock系列类,要做的就是:
  
  1、让被锁类从InstanceLockBase继续
  
  2、所有要访问被锁对象的代码前面声明InstanceLock的实例,并传入被锁对象的指针。
  
  附:完整源代码:
  
  #pragma once
  #include windows.h
  
  class InstanceLock;
  
  class InstanceLockBase
  ... {
   friend class InstanceLock;
  
   CRITICAL_SECTION cs;
  
   void Lock()
   ... {
  ::EnterCriticalSection( & cs);
   }
  
   void Unlock()
   ... {
  ::LeaveCriticalSection( & cs);
   }
  
   protected :
   InstanceLockBase()
   ... {
  ::InitializeCriticalSection( & cs);
   }
  
   ~ InstanceLockBase()
   ... {
  ::DeleteCriticalSection( & cs);
   }
  } ;
  
  
   class InstanceLock
  ... {
   InstanceLockBase * _pObj;
   public :
  InstanceLock(InstanceLockBase * pObj)
  ... {
   _pObj = pObj;
   if (NULL != _pObj)
  _pObj - Lock();
  }
  
   ~ InstanceLock()
   ... {
  if (NULL != _pObj)
   _pObj - Unlock();
   }
  } ;

来源:http://www.tulaoshi.com/n/20160219/1626492.html

延伸阅读
解说Win32的窗口子类化            作者:李马(home.nuc.edu.cn/~titilima) 下载本文的配套源代码         也许你需要一个特殊的Edit来限制浮点数的输入,但是现有的Edit却并不能完成这项工作——因为它只能够单纯的限制大小写或者纯数字。当你在论坛上求救的时候,某个网友告诉你:...
标签: windows系统
Win8系统下用户账户控制的两种开启方法   下面图老师小编就教大家如何开启win8系统中的用户账户控制功能。 win8系统启用用户账户控制的方法: 第一种: 1、进入win8系统后,在传统桌面下按win+r键,输入msconfig,点击工具选项卡, 2、选择第二项更改UAC设置,点启动,选择通知的级别就行了,建议选择系统默认...
静态成员的提出是为了解决数据共享的问题。实现共享有许多方法,如:设置全局性的变量或对象是一种方法。但是,全局变量或对象是有局限性的。这一章里,我们主要讲述类的静态成员来实现数据的共享。 静态数据成员 !-- frame contents -- !-- /frame contents -- 在类中,静态成员可以实现多个对象之间...
获得 Win32 窗口句柄的更好的方法 ----动态生成并显示 HTML 文档   ----再谈禁用HTML的上下文菜单... 编译/NorthTibet 原文出处:MSDN Magazine C++ Q&A 下载源代码  译者注: 在以前的VC知识库 Online Journal 上有三篇文...
比较大应用程序都由很多模块组成,这些模块分别完成相对独立的功能,它们彼此协作来完成整个软件系统的工作。其中可能存在一些模块的功能较为通用,在构造其它软件系统时仍会被使用。在构造软件系统时,如果将所有模块的源代码都静态编译到整个应用程序EXE文件中,会产生一些问题:一个缺点是增加了应用程序的大小,它会占用更多的磁盘空间...

经验教程

289

收藏

70
微博分享 QQ分享 QQ空间 手机页面 收藏网站 回到头部