能够进行对象级线程同步和引用计数垃圾回收的智能指针






4.89/5 (17投票s)
1999年11月22日
8分钟阅读

184511

1800
一个智能指针包装类。
更新内容
引言
SmartPtr 类是一个封装指针的通用包装器。它跟踪引用计数的总数,并在指向的对象不再被任何“指针”引用时执行垃圾回收。此外,SmartPtr 还可以对指向的对象进行对象级线程同步。这意味着,调用指向对象的成员会被封装在 Windows 临界区中,一次只有一个线程可以使用该对象。
SmartPtr 将引用计数器和临界区存储在单独的内存块中,而不是指向的对象中。这导致您无需继承您的类,只需继承一个特殊的基类即可使用 SmartPtr 作为指针。另一个好处是,您可以使用 SmartPtr 指向标量类型(如 int、short、long、double 等)的对象。
SmartPtr 被实现为一个模板类。由于它有三个模板参数,每次都指定所有参数会很麻烦。因此,我定义了三个额外的模板类,它们继承自 SmartPtr,并具有适当的默认模板参数,以实现三种不同的行为。
- 引用计数垃圾回收 - SmartPtr 将自动释放动态分配的内存。
- 带引用计数垃圾回收的同步访问 - SmartPtr 将仅执行线程同步。您需要自行释放动态分配的内存。
- 不带引用计数垃圾回收的同步访问 - SmartPtr 将仅执行线程同步。您应该自己释放动态分配的内存。
这些是类
类名 | 描述 |
---|---|
SmartPtrBase | SmartPtrBase 类是 SmartPtr 类的非模板基类。它不应直接使用。SmartPtr 使用它来进行适当的类型转换。 |
SmartPtr | SmartPtr 类是以下类的模板基类。 |
RefCountPtr | 仅执行引用计数垃圾回收的智能指针。 |
SyncPtr | 仅执行不带引用计数垃圾回收的同步访问的智能指针。 |
SyncRefCountPtr | 同时执行同步访问和引用计数垃圾回收的智能指针。 |
所有这些类在实现上都是相同的。通过不同的默认模板参数实现不同的行为。您可以使用这四个类中的任何一个来获得这三种行为中的任何一种,但那时您需要指定所有参数。
以下类在 SmartPtr 实现中使用,并且永远不应直接使用。
- CRefCountRep
- CSyncAccessRep
- CSyncRefCountRep
- CSyncAccess
SmartPtr 的使用示例
您应该在项目中包含 SmartPtr.h
文件。
我通常会在我的 StdAfx.h
文件中包含这一行。
#include "SmartPtr.h"
假设我们有一个类 CSomething
1. 对 CSomething 类的引用计数垃圾回收
class CSomething { CSomething(); ~CSomething(); ..... void do(); }; typedef RefCountPtr<CSomething> LPSOMETHING; void TestFunct() { LPSOMETHING p1 = new CSomething; LPSOMETHING p2 = p1; if( p1 == NULL ) { .... } p2->do(); p1 = NULL; }// Here the object pointed by p2 WILL BE destroyed automatically /////////////////////////////////////////////////////////////////////////////
2. CSomething 对象级别的线程同步
class CSomething { CSomething(); ~CSomething(); ..... void do(); }; typedef SyncPtr<CSOMETHING><CSomething> LPSOMETHING; void TestFunct() { LPSOMETHING p1 = new CSomething; LPSOMETHING p2 = p1; if( p1.IsNull() ) { .... } StartThread( p1 ); p2->do(); // Synchronized with the other thread p1 = NULL; } // Here the object pointed by p2 will NOT be destroyed automatically void ThreadFunc( LPSOMETHING p ) { p->do(); // Synchronized with the other thread } // Here the object pointed by p will NOT be destroyed automatically /////////////////////////////////////////////////////////////////////////////
在此示例中,您将遇到内存泄漏,但两个线程在尝试调用对象的成员时将同步。由您负责释放动态分配的内存。
3. CSomething 对象的对象级线程同步和引用计数垃圾回收
class CSomething { CSomething(); ~CSomething(); ..... void do(); }; typedef SyncRefCountPtr<CSOMETHING><CSomething> LPSOMETHING; void TestFunct() { LPSOMETHING p1 = new CSomething; LPSOMETHING p2 = p1; if( p1.IsNull() ) { .... } StartThread( p1 ); p2->do(); // Synchronized with the other thread p1 = NULL; } // Here the object pointed by p2 WILL BE destroyed automatically // if p in ThreadFunc has already released the object void ThreadFunc( LPSOMETHING p ) { p->do(); // Synchronized with the other thread } // Here the object pointed by p WILL BE destroyed automatically // if p2 in TestFunc has already released the object /////////////////////////////////////////////////////////////////////////////
在此示例中,您不会遇到内存泄漏,并且两个线程在尝试调用对象的成员时将同步。您无需释放动态分配的内存。SmartPtr 将为您处理。
SmartPtr 是如何工作的?
SmartPtr 的定义是:
template<class T, class REP=CRefCountRep<T>, class ACCESS = T*> class SmartPtr : public SmartPtrBase { ... };
其中,T 是指向对象的类型,REP 是用于处理指针的表示类,ACCESS 是用于安全地访问底层实际指针的类。
关于引用计数垃圾回收,没有什么特别有趣的。这是一个标准的实现。唯一有趣的是,引用计数器和实际指针存储在表示对象中,而不是指向的对象中。
更有趣的是对象级线程同步是如何工作的。让我们看看 SyncPtr 类的定义:
template<class T, class REP=CSyncAccessRep<T>, class ACCESS=CSyncAccess<T> > class SyncPtr { ... ACCESS operator -> (); };
看看引用运算符。它返回 ACCESS 类型对象,默认情况下为 CSyncAccess<T>
类型。诀窍在于,如果引用运算符返回的不是实际指针,则编译器会调用返回对象的 operator->()
。如果它再次返回的不是实际指针,编译器会调用返回对象的 operator->()
。依此类推,直到某个引用运算符返回实际指针,例如 T*
。嗯,为了实现对象级同步,我利用了引用运算符的这种行为。我定义了一个类 CSyncAccess<T>
。
template <class T> class CSyncAccess { ... virtual ~CSyncAccess(); T* operator -> (); }; /////////////////////////////////////////////////////////////////////////////
在这种情况下使用的表示类有一个 CRITICAL_SECTION
类型的成员。
template <class T> class CSyncAccessRep { ... CRITICAL_SECTION m_CriticalSection; }; /////////////////////////////////////////////////////////////////////////////
现在让我们看看示例代码:
typedef SyncPtr<CSomething> LPSOMETHING; LPSOMETHING p = new CSomething; p->do();
当调用 p->do()
时会发生什么?
- 编译器调用
SyncPtr::operator->()
,它返回CSyncAccess
类型的对象。这个对象是临时的,存储在栈顶。在其构造函数中,临界区对象被初始化。 - 编译器调用
CSyncAccess:operator->()
,它拥有临界区,从而阻止其他线程拥有它,然后返回指向对象的实际指针。 - 编译器调用指向对象的
do()
方法。 - 编译器销毁栈上的
CSyncAccess
对象。这将调用其析构函数,其中临界区被释放,其他线程可以拥有它。
SmartPtr 的优点在于,指向的对象不必像许多其他引用计数实现那样继承自任何特殊的基类。引用计数器和实际对象指针存储在 CRefCountRep、CSyncAccessRep 和 CSyncRefCountRep(称为表示对象)的动态分配对象中,具体取决于我们使用的 SmartPtr 类型。这些对象由 SmartPtr 分配和释放。这种方法使我们能够为任何类的对象创建 SmartPtr,而不仅仅是继承自特殊基类的对象。然而,这也是 SmartPtr 的一个弱点。想象一下您有这样一段代码:
LPSOMETHING p1 = new CSomething;
CSomething* p2 = (CSomething*)p1;
LPSOMETHING p3 = p2;
结果是,在这段代码的末尾,您认为 p1 和 p3 将指向同一个对象。是的,它们会。但 p1 和 p3 将拥有不同的表示对象。因此,当 p3 被销毁时,底层 CSomething 对象也将被销毁,而 p1 将指向无效内存。所以我有一个简单的技巧:如果您使用 SmartPtr,切勿使用指向底层对象的实际指针(如上面的代码)。但如果您愿意,在处理其他对象时仍然可以使用实际指针。
让我们看看这段代码:
LPSOMETHING p1 = new CSomething; SomeFunc( p1 ); void SomeFunc( CSomething* pSth ) { LPSOMETHING p3 = pSth; }
p3 将创建自己的表示,即它自己的引用计数器,并且当 SomeFunc 退出时,p3 将释放 p1 指向的内存。您最好将此函数定义改写如下:
void SomeFunc( LPSOMETHING pSth );
我提到这些是为了通知您,使用 T* 参数调用 SmartPtr 的构造函数或operator = 会创建新的表示对象,这会导致 SmartPtr 的“奇怪”行为。
上述 SmartPtr 行为的弱点可以通过添加静态关联表来解决:T* <-> CxxxxxRep*,每次 SmartPtr 接收到 T* 时,我们都可以搜索该表并获取已创建的相应 CxxxRep 对象。但这会导致更多的内存和 CPU 开销。目前,我认为遵循上述规则比增加开销更好。
SmartPtr 与常规指针和整数值一样高效。
下表显示了一些 sizeof() 的结果:
大小(字节) | |
---|---|
sizeof( SmartPtr ) | 4 |
sizeof( CSomething* ) | 4 |
sizeof( int ) | 4 |
SmartPtr 对象的大小与指针和 int 值相同。因此,将 SmartPtr 对象作为函数(方法)参数传递或将 SmartPtr 对象作为函数返回值,其效率与传递常规指针或 int 值相同。
SmartPtr 对象可以接受不同的指针类型。
SmartPtr 类有一个非模板基类 SmartPtrBase,它是各种构造函数和赋值运算符的参数。因此,可以使用如下代码:
class B
{
...
};
class A : public B { ... };
class C
{
...
};
RefCountPtr<A> a = new A;
RefCountPtr<B> b = a;
b = a;
即使这样,在需要一个指向基类的指针来持有派生类对象的场合,您也可以这样做:
RefCountPtr<A> a = new A; RefCountPtr<B> b = new B; SyncPtr<C> c = a; a = b; a = c; c = a; c = b;
并且编译器不会警告您存在隐式类型转换。请注意,class C 和 class B 之间没有关系。
实际上,SmartPtr 在这些赋值中隐式地传递了底层表示对象。指针的行为取决于底层表示对象。因此,无论持有指针的意图如何(在上面的示例中,“c”是同步指针),指针的行为都取决于表示对象创建者的类型。
在上面的示例中,在最后一行(c = b;)之后,“c”将持有“b”指向的对象,即使“c”被声明为“同步”指针,在处理其对象时,它也会像“引用计数”指针一样行为,因为原始创建者是“b”,它属于该类型。
附带的演示项目是一个简单的控制台应用程序,演示了 SmartPtr 的不同情况和用法。它由 VC ++ 5.0 创建。SmartPtr.h 可以用警告级别 4 进行编译。
就是这样。