线程同步是多线程程序设计的核心内容,它的目的是正确处理多线程并发时的各种问题,例如线程的等待、多个线程访问同一数据时的互斥,防死锁等。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();
       }
};

转载于:https://www.cnblogs.com/highmayor/archive/2008/01/28/1056352.html

Win32环境下两种用于C++的线程同步类(上)相关推荐

  1. android登录加密传输,android环境下两种md5加密方式(示例代码)

    在平时开发过程中,MD5加密是一个比較经常使用的算法,最常见的使用场景就是在帐号注冊时,用户输入的password经md5加密后,传输至server保存起来.尽管md5加密经经常使用.可是md5的加密 ...

  2. Win32环境下动态链接库(DLL)编程原理

    Win32环境下动态链接库(DLL)编程原理 比较大应用程序都由很多模块组成,这些模块分别完成相对独立的功能,它们彼此协作来完成整个软件系统的工作.其中可能存在一些模块的功能较为通用,在构造其它软件系 ...

  3. Win32环境下代码注入与API钩子的实现

    Win32环境下代码注入与API钩子的实现 本文详细的介绍了在Visual Studio(以下简称VS)下实现API钩子的编程方法,阅读本文需要基础:有操作系统的基本知识(进程管理,内存管理),会在V ...

  4. Win32 环境下的堆栈

    原文已经找不到,作者应该是:http://blog.csdn.net/slimak   但是没有找到此文,其中丢了2幅图 简介 在Win32环境下利用调试器调试应用程序的时候经常要和堆栈(Stack) ...

  5. 单例模式的5种实现方式,以及在多线程环境下5种创建单例模式的效率

    这段时间从头温习设计模式.记载下来,以便自己复习,也分享给大家. [java] view plaincopy package com.iter.devbox.singleton; /** * 饿汉式 ...

  6. Win32环境下轻松调试单板安全模式软件下载功能

    Win32环境下轻松调试单板安全模式软件下载功能 温辉敏(wenhm@sina.com) 摘要:本文首先提出了在单板环境下调试软件下载功能的低效率和复杂性,然后进行了Win32环境下调试软件下载功能的 ...

  7. centos下两种方法安装git

    centos 5 64位下两种方法安装git 这里来给大家介绍下编译安装和yum安装git.   系统:centos 5.5 64位   需要的软件包:git-latest.tar.gz epel-r ...

  8. Linux环境下几种常用的文件系统

    Linux环境下几种常用的文件系统: 1.ext2 ext2是为解决ext文件系统的缺陷而设计的可扩展的.高性能的文件系统,又被称为二级扩展文件系统.它是Linux文件系统中使用最多的类型,并且在速度 ...

  9. 早教产品微商怎么做精准引流?早教机构引流活动可以分为线上和线下两种

    早教产品微商怎么做精准引流?早教机构引流活动可以分为线上和线下两种 一般来说,根据媒介的不同,早教机构引流活动可以分为线上和线下两种,线上的引流方式有:微信.自媒体.短视频;线下的引流方式有地推和异业 ...

最新文章

  1. Java设计模式:抽像工厂模式
  2. 最短路计数(spfa)
  3. 学习 PixiJS — 交互工具
  4. 使用Forge插件在现有Java EE项目上启用Arquillian
  5. 发布 项目_项目发布会活动到底应该怎么办
  6. 60-008-026-使用-命令-如何在flink中传递参数
  7. 简单的PL/SQl链接远程ORACLE数据库方法
  8. 实体和电商哪个更能赚到?
  9. 杨氏模量_快!准!狠!——5分钟搞定A-Level物理必考知识点杨氏模量曲线...
  10. MSSQL日期格式转换函数(使用CONVERT)
  11. 搭建apache_??4、Apache环境web搭建
  12. 第一行代码--笔记(3)
  13. 教程贴--DISM 安装系统
  14. Redis docker安装及redis.conf配置文件解析
  15. 魅力网络技术博客图像处理正文 ps入门教程、ps修图基本工具使用方法视频教学...
  16. 出差日程安排软件哪个好
  17. 小人物解决四大数学问题:记传奇华人数学家李天岩
  18. 感知机算法基础形式及对偶形式算法
  19. html5 i标签什么意思,快速了解HTML5 b和i标签
  20. 浅谈cocos2d游戏中天气系统的简单实现

热门文章

  1. 当面试官问我Mybatis初始化原理时,我笑了
  2. python 对指定URL获取其子链接
  3. Spring Cloud构建微服务架构(四)分布式配置中心
  4. itemchanged信号找不到_失物 | 求FDU同学帮转帮找蓝牙键盘,坐标东区宿舍19号楼...
  5. 网络故障排除工具 | 快速定位网络故障
  6. 数据中心水冷系统备品备件管理新思路
  7. 选择海外数据中心是否等级越高越好
  8. bootstrap项目更改为vue_取代Jquery,用Vue 构建Bootstrap 4 应用
  9. java什么是服务治理平台_Java | Spring Cloud 是如何实现服务治理的
  10. AI:2020年6月16日晚20点陆奇博士演讲《正视挑战把握创业创新机会》