我们看下下面这个例子:
class A{
public:
A(){}
void changBackgroundImage(const std::istream& img);
private:
Mutex m_mutex{};
Image* m_pImg { nullptr };
int m_times{ 0 };
}
下面是 changBackgroundImage 函数的某个实现:
viod A::changBackgroundImage(const std::istream& img)
{
lock(&m_mutex);
delete m_image;
++m_times;
m_pImg = new Image(img);
unlock(&m_mutex);
}
上面这个函数咋一看,好像没什么问题,但如果细究下来,漏洞百出。我们在使用 C API 函数处理类型为 Mutex 的互斥器对象时,通常也是有两个函数。
void lock(Mutex* mutex); //锁定 void unlock(Mutex* mutex); //解锁
上面的用法也符合成对出现的规则。
但是上面 changBackgroundImage 函数的实现中存在的问题是:
- 如果在 new Image(img); 的时候抛出了异常, 那么下面的语句 unlock(&m_mutex); 将永远不会被执行,互斥器的锁定将永远存在,不符合异常安全性函数的不泄露任何资源原则。
- 如果在 new Image(img); 的时候抛出了异常,那么原本的对象 m_pImg 其实已经被析构 m_pImg 指向了一个已经被析构的对象, m_times 也已经被修改,不符合异常安全性函数的不允许数据被破坏原则。
当然,为了确保我们不会忘记一个被锁定的 Mutex 对象解锁,我们会建立一个 class 来解决这个问题。这个 class 的基本结构由 RAII 原则来框画。也就是满足“资源在构造时被申请,在析构时被释放”。
class Lock{
public:
explicit Lock(Mutex* mutex) : m_pMutex(mutex)
{
lock(m_pMutex);
}
~Lock()
{
unlock(m_pMutex);
}
private:
Lock(const Lock&);
Lock& operator=(const Lock&);
private:
Mutex* m_pMutex{ nullptr };
}
现在我们再来看下 changBackgroundImage 函数的实现过程:
viod A::changBackgroundImage(const std::istream& img)
{
Lock ml(&m_mutex);
delete m_image;
++m_times;
m_pImg = new Image(img);
}
我们使用了类 Lock 来进行资源 mutex 的管理。这样就能避免我们上面所说的第一个问题,当在 new Image(img); 的过程中抛出异常时,我们也能够正常的释放 m_mutex 锁定的资源。
上面的类 Lock 在定义的时候,我们将其 copy 构造和 复制操作符声明为 private 类型,并且不去实现这两个函数,其目的是禁用类 Lock 的拷贝行为。
相对来说,上面我们通过 **以类的形式管理资源(资源管理类)**方法解决了资源泄漏的问题,当然这种方式使得函数 changBackgroundImage 的函数体也更小了,代码越少,出错的机会就越少。下面我们就需要专心解决数据被破坏的问题。
我们看下,changBackgroundImage 函数内其实是有一个指针指向的对象存储了图像的资源,而我们前面也提到过,以对象的形式进行资源管理,所以,很快就有了下面的改变。
class A{
private:
std::tr1::shared_ptr m_pImg;
}
viod A::changBackgroundImage(const std::istream& img)
{
Lock ml(&m_mutex);
m_pImg.reset(new Image(img));
++m_times;
}
上面这个函数已经有了很大的改变,将指针定义为我们前面说过的智能指针,通过引用计数的方式来保证资源的释放。并且能够缩小函数体。m_pImg 指向的内存也只有在 new Image(img) 执行成功的前提下被重新设置。也就是说 delete 操作只在新对象被重新分配之后才被执行。
这样也就保证了数据不被破坏,同时我们将 m_times 的自增放在了后面。保证 m_pImg 在正常进行reset之后才标记次数的改变。
一般情况下,上面的操作基本能够保证异常安全。但是上面的函数我们注意到使用的参数类型是std的istream,所以她可能存在的一个问题是,当函数体中抛出异常时,读取的标记可能被清除了。那该怎么解决呢?
或许我们可以通过修改参数类型的方式进行修改,但是今天他不是主角。今天的主角是 copy-and-swap。
我在前面学习stl的时候,曾经有提到过这种方法。这种方法的核心就是先复制一份,然后在复制的这一份上做修改,确保没有任何问题之后,再在安全的环境下将原对象和复制的对象进行交换。
struct PmImp{
std::tr1::shared_ptr m_pImg;
int m_times{ 0 };
}
class A{
private:
Mutex m_mutex{};
std::tr1::shared_ptr m_pImp;
}
viod A::changBackgroundImage(const std::istream& img)
{
Lock ml(&m_mutex);
std::tr1::shared_ptr pNew(new PmImp(*m_pImp)); //先进行拷贝
pNew->m_pImg.reset(new Image(img));
++pNew->m_times;
std::swap(m_pImp, pNew);
}



