基于继承的设计与构造函数和析构函数的开销之间的关系
来源:互联网 发布:美国大律师 知乎 编辑:程序博客网 时间:2024/04/30 07:35
首先讨论关于线程同步构造的实现。在多线程应用程序中,会经常为了限制并发访问共享资源而提供线程同步。线程同步最常见的三种形式是:信号(semphore)、互斥(mutex)、和临界区(critical section)。
信号方式提供了受限的并发。它允许最多为给定上限的线程访问共享资源。当并发线程的最大数量为1时,称这种特殊的信号量为互斥(MUTual EXclusion)。互斥方式通过在任何时间允许且只允许一个线程对共享资源进行操作来保护共享资源。一般情况下共享资源可以由分散于应用程序的各个代码段进行操作。
以一个共享队列为例。该队列中元素的个数通常由enqueue()和dequeue()共同管理。很明显,不能允许多个线程同时对元素个数进行修改。
<span style="font-size:14px;"> Type& dequeue() { get_the_lock(queueLock); ... numberOfElements--; ... release_the_lock(queueLock); }</span>
<span style="font-size:14px;"> void enqueue(const Type& value) { get_the_lock(queueLock); ... numberOfElements++; ... release_the_lock(queueLock); }</span>如果enqueue()和dequeue()都能并发地修改numberOfElements,那很可能会得到一个包含错误的numberOfElements。修改这个变量必须以原子操作完成。
互斥锁最简单的应用是临界区。临界区是指某一时间只能由一个线程执行的一个代码段。线程在进入临界区之前就必须为获得锁而竞争来达到互斥的效果。成功获取锁的线程就可以进入临界区。当要退出临界区时,该线程释放锁以允许其他线程进入。
get_the_lock(CSLock);
{
//临界区开始
...//被保护的计算
//临界区结束
}
release_the_lock(CSLock);
在例子dequeue()中,很容易就可以检查代码,验证每一个锁操作都有与之相匹配的解锁操作。如果在代码执行的过程中于某处获得了锁,那么在执行任何一条返回语句之前必须释放锁。你可以想象这将是维护人员的苦痛,几乎肯定会有Bug在这里出现。大型工程可能有许多人在编写代码和修复Bug。当你在有100行代码的例程中添加一条返回语句时,很可能忽略获得锁这个事实。This is a first problem。The second problem is the exception:如果抛出一个异常的同时又持有锁,那么必须在捕获异常时手工释放锁。这是很别扭的。
C++为这两个难题提供了一个完美的解决方法。当一个对象到达其作用与结尾时,会自动调用析构函数。故而可以利用自动调用的析构函数解决锁的维护问题。把锁封装在对象内部并让构造函数获得锁,析构函数将自动释放锁。如果在由100行代码的例程组成的构造函数内部定义这样的对象,就无须担心多条返回声明了。编译器在每条返回语句之前插入对带锁的析构函数的调用,于是锁总是能被释放。
使用构造函数和析构函数对去获取和释放共享资源会导致如下所示的锁类实现:
class Lock
{
public:
Lock(pthread_mutex_t& key)
:theKey(key){pthread_mutex_lock(&theKey);}
~Lock()
{ pthread_mutex_unlock(&theKey) ;}
private:
pthread_mutex_t &theKey;
};
编程环境通常提供多种风格的同步构造。风格的区别表现在以下几个方面:
1.并发级别:信号允许不多于给定最大数量的线程共享资源。互斥只允许一个线程访问共享资源。
2.嵌套:某些构造允许线程在已持有一个锁的情况下再次获得锁。而这锁嵌套在另外一些构造的情况下会发生死锁。
3.通知:在资源变为可用时,有一些同步构造会通知所有正在等待的线程。这种方式是很低效的,因为除了第一个线程之外,其他所有线程被唤醒后会发现他们不够快,因为资源已经被其它线程获得。一个更为有效的通知方案仅唤醒一个正在等待的线程。4.读/写锁:允许多个线程读取一个受保护的值,但是只允许一个线程修改它。
5.内核/用户空间:某些同步机制只在内核空间中有效。
6.进程间/进程内:一般情况下,同一进程中的线程间同步 要比不同进程中的线程间同步更为高效。
尽管这些同步构造在语义和性能方面差别很大,但是它们使用相同的锁/解锁协议。这是非常值得学习的,因为可以把这种相似性转换为一种基于继承的锁类层级,而这些锁类均来自同一基类。在开发产品时,最开始找到了一种实现,如下所示:
class BaseLock
{
public:
BaseLock(pthread_mutex_t &theKey,LogSource &lsrc) {};
virtual ~BaseLock() {};
}
就像你看到的,BaseLock这个类没什么功能。它的构造函数和析构函数都是空的。设计这个BaseLock类的意义在于可以从这个类中派生出各种各样的锁类。风格各异的锁类自然要以不同的BaseLock的子类来实现。MutexLock就是一个派生出来的类:
class MutexLock:public BaseLock
{
public:
MutexLock (pthread_mutex_t &theKey,LogSource &lsrc);
~MutexLock();
private:
pthread_mutex_t &theKey;
LogSource &src;
};
MutexLock的构造函数和析构函数有如下的实现:
MutexLock::MutexLock(pthread_mutex_t& aKey,const LogSource &source)
:BaseLock(aKey,source),
theKey(aKey),
src(source)
{
pthread_mutex_lock(&theKey);
#if defined(DEBUG)
cout<<"MutexLock"<<"&aKey"<<"created at"<<src.file()<<"line"<<src.line()<<endl;
#endif
}
MutexLock::~MutexLock()
{
pthread_mutex_unlock(&theKey);
#if defined(DEBUG)
cout<<"MutexLock"<<"&aKey"<<"destroyed at"<<src.file()<<"line"<<src.line()<<endl;
#endif
}
MutexLock的实现使用了LogSource对象。LogSource对象必须在该对象创建时捕获文件名和源代码行号。在记录错误和跟踪信息是时,有必要描述信息源的位置。C程序员会用(char*)处理文件名,用int处理行号。但C++开发人员则选择一个LogSource对象中把它们全部封装进去。又一次构建了一个什么也不做的基类,不过由此得到的将是一个更为有用的派生类:
class BaseLogSource
{
public:
BaseLogSource() {};
virtual ~BaseLogSource() {}
};
class LogSource:public BaseLogSource
{
public:
BaseLogSource(const char *name,int num):filename(name),lineNum {};
virtual ~LogSource() {};
char *filename;
int line();
private:
char *filename;
int lineNum;
};
LogSource对象就这样被创建并作为参数传递给MutexLock对象的构造函数。在得到琐时LogSource对象捕获源文件和行号。这些在调试死锁时会派上用场。
假设sharedCounter是一个可以由多个线程访问且需要序列化的整型变量。通过在局部范围内插入一个锁对象来提供互斥:
{
MutexLock_myLock(theKey,LogSource(_FILE_,_LINE_));
SharedCounter++;
}
MutexLock和LogSource对象的创建同时触发了对它们各自基类的调用。
这一小段代码调用了大量构造函数:
BaseLogSource
LogSource
BaseLock
MutexLock
当sharedCouunter变量的值递增之后,通过作用域的结尾,并触发了几个相应的析构函数:
MutexLock
BaseLogSource
对共享资源的操作总共用到8个构造函数和7个析构函数。代码复用和性能之间的矛盾一直是一个令人困扰的问题。若抛弃这些对象,只去开发一个只做必须做的事情而不做其他的手工版本,找出都有哪些开销是一件很有趣的事。也就是说,只更新sharedCounter的前后进行关于锁的工作:
{
pthread_mutex_lock(&theKey);
sharedCounter++;
pthread_mutex_unlock(&theKey);
}
显然上述版本比前面版本更为高效。采用面向对象的设计使我们不得不用更多的指令。那些指令都是专门用于创建和销毁对象的。考虑开销要根据应用背景来判断:若在一个性能敏感的流程里,那么就需要这样做。在一些特定的情况下,若总的计算开销很小而执行那些指令的代码又调用的比较频繁,那么那些附加指令的开销就变得十分显著。而关注的是被浪费的指令与所有计算总指令的比率。刚才讨论的代码仅取自于一个网关的实现,它把数据包从一个通信适配器路由导向到另一个。这是一个大约包含5000条指令的关键执行路径。MutexLock对象在该路径中被用到几次。而这几次调用累计起来的指令开销占到全部开销的10%,这是非常可观的。
使用锁的对象的好处在于它可以实现:
1.对包含多个返回点的复杂子程序的维护;
2.从异常中恢复。
3.锁操作中的状态。
4.日常记录的多态。
未完待续...
- 基于继承的设计与构造函数和析构函数的开销之间的关系
- 构造函数的继承关系
- 继承中,构造函数,赋值操作符,析构函数与虚函数的关系
- 有继承关系的构造函数和析构函数的执行次序
- 构造函数、析构函数与虚函数的关系
- 虚函数与构造函数,析构函数的关系
- 构造函数、析构函数与虚函数的关系
- 继承(子类与父类构造函数的关系)
- 非继承的 构造函数 与 析构函数
- 类函数, 原型对象 与 构造方法之间的关系
- c++ 继承关系的构造函数
- Java继承时构造函数的关系
- Java继承关系构造函数的调用
- C++ 构造函数和析构函数的继承
- C++在继承的构造函数和析构函数
- C++在继承的构造函数和析构函数
- c++继承里面的构造函数和析构函数
- 继承的三种方式与派生类的构造函数和析构函数~
- 【BZOJ4519】【Sdoi2016】游戏 线段树
- Android系列之Activity
- 唯爱小粽子:软件架构的典型组成部分-安全性
- 解决get方法传递URL参数中文乱码问题
- Java 注解
- 基于继承的设计与构造函数和析构函数的开销之间的关系
- 记录lua的table转string一个问题
- 程序员面试经典之链表分割
- hiho 50 Fleury算法求欧拉路径
- 我理解的设计模式:工厂模式
- Android小错误收集
- ios开发总结之UIImageView常用属性
- 如何使 类的成员函数作为回调函数
- JavaScript与OC的相互调用