不得不说的一些编程技巧

作者在 2011-10-27 21:56:05 发布以下内容

         一个人会一门语言不难,难的是会玩出花样,这就是技巧,当然玩的人多了这些技巧也就不怎么稀奇了,哈。

 

我就先说说一个叫“引用计数”的东西。顾名思义,就是记录一个对象被引用了几次。那么它的出发点是什么,这可能需要一个场景来描述。假设你有一个叫A的对象,在T1线程中被创建,之后可能会在T2线程或者T3线程中被使用。当然对象A是线程安全的,可以再任何线程中被安全使用,但问题是对象A该由谁来销毁?假如说T1T2已经不需要使用对象A了,它们想留给T3线程来销毁,因为T3线程可能还在使用。这都是假设,反过来T3线程可能也不需要使用对象A了,那么它怎么知道T1T2线程也不需要使用了呢?没错,引用计数该出现了。对象AT1T2T3中被共享使用,那么它应该引用计数为3,当T1线程不需要使用对象A时对引用计数减1,这情况对T2T3同样适用,只是在引用计数在变为0时就应该立马销毁对象A。很简单不是吗?实现的代码如下:

 

template<typename t_Counter>
class SharedObjectT
{
public:
    SharedObjectT(void) : m_ref(0)
    {
    }
protected:
    virtual ~SharedObjectT(void) = 0
    {
    }
public:
    Long AddRef(void)
    {
        return ++m_ref;
    }
    Long Release(void)
    {
        Long ref = --m_ref;
        if (0 == ref)
        {
            delete this;
            return 0;
        }
        return ref;
    }

private:
    t_Counter    m_ref;
};

class SafeCounter
{
public:
    SafeCounter(Long _c = 0) : m_c(_c)
    {
    }
    Long operator ++ (void)
    {
        return ::InterlockedIncrement(&m_c);
    }
    Long operator -- (void)
    {
        return ::InterlockedDecrement(&m_c);
    }
    operator volatile const Long& (void) const volatile
    {
        return m_c;
    }
    operator volatile Long& (void) volatile
    {
        return m_c;
    }

private:
    Long m_c;
};

typedef SharedObjectT<SafeCounter>    SharedObject;

 

         SharedObject就是所谓的引用计数,只不过它叫共享对象,意味着只要从它这里派生的子类就是一个共享对象。细心的你可能会发现,SharedObjectT的析构函数被限制在“保护”,这似乎没什么意义,因为SharedObjectT本来就是抽象基类,生成不了实例。其实这里是要告诉派生类,最好也应该把析构函数进行“保护”。因为一个共享对象它不能存在于栈上,所以使用“保护”析构函数来达到禁止在栈上构建对象。这也是一个技巧。

        

         我把递增递减单独实现为一个叫SafeCounter的类中,我觉得这样有更多的选择,比如你可以实现在Linux下的SafeCounter

 

         当赋予对象一个分身术时,管理它的引用计数成了一个麻烦,你不得不小心的增加一次引用和释放一次引用,以免破坏了对象引用次数。只要你够懒,就会想到一个方法。能不能让它也表现的像一个普通对象?我实在是不想看到AddRefRelease,看到都想吐。这时候“智能引用”该出现了。不要被名字迷惑,很难解释“智能”的含义。只有看代码才能体会:

 

template<typename t_Type>
class SharedObjectHandlerT
{
public:
    SharedObjectHandlerT(t_Type* _obj = Null)
        : m_obj(_obj)
    {
        if (m_obj)
            m_obj->AddRef();
    }
    SharedObjectHandlerT(const SharedObjectHandlerT& _rhs)
        : m_obj(_rhs.m_obj)
    {
        if (m_obj)
            m_obj->AddRef();
    }
    virtual ~SharedObjectHandlerT(void)
    {
        Safe_Release(m_obj);
    }

    operator void* (void) const
    {
        return m_obj;
    }
    t_Type* operator -> (void)
    {
        return m_obj;
    }
    const t_Type* operator -> (void) const
    {
        return m_obj;
    }
    t_Type& operator * (void)
    {
        return *m_obj;
    }
    const t_Type& operator * (void) const
    {
        return *m_obj;
    }
    bool operator == (const SharedObjectHandlerT& _rhs) const
    {
        return m_obj == _rhs.m_obj;
    }
    bool operator != (const SharedObjectHandlerT& _rhs) const
    {
        return m_obj != _rhs.m_obj;
    }
    bool operator == (const t_Type* _obj) const
    {
        return m_obj == _obj;
    }
    bool operator != (const t_Type* _obj) const
    {
        return m_obj != _obj;
    }

    SharedObjectHandlerT& operator = (SharedObjectHandlerT _rhs)
    {
        if (m_obj == _rhs.m_obj)
            return *this;

        Safe_Release(m_obj);
        if (_rhs.m_obj)
        {
            _rhs.m_obj->AddRef();
            m_obj = _rhs.m_obj;
        }
        return *this;
    }
    SharedObjectHandlerT& operator = (t_Type* _obj)
    {
        if (m_obj == _obj)
            return *this;

        Safe_Release(m_obj);
        if (_obj)
        {
            _obj->AddRef();
            m_obj = _obj;
        }
        return *this;
    }

private:
    t_Type*    m_obj;
};

 

         SharedObjectHandlerT的模板参数必须传入SharedObject类型或者兼容它的类型。这就是所谓的“智能引用”,它在构造函数中增加引用,在析构函数中释放引用,利用C++对象的这一个特性实现自动的引用计数功能。实际上它没看上去那么简单,看到它那个赋值函数吗?它的参数不是引用(&)参数,是传值的,之所以这样设计是有一个复杂的理由,这个理由会绕晕你的脑袋。假设有一个智能引用M,我们知道它内部有一个m_obj是共享对象,假设这个共享对象内部也包含了一个智能引用NMN是相同类型的SharedObjectHanderT实例),如果出现下面语句会怎么样?

 

M = M->N; // 操作符“->”是对m_obj成员的调用,看上面SharedObjectHanderT实现。

 

         是不是感觉到菊花一紧?根据SharedObjectHanderT的赋值函数的实现,它需要先释放内部m_obj的一次引用,而正是因为要释放引用,假如这次释放引用正好达到销毁m_obj的条件,那么在m_obj中的N还能存活吗?因此,需要传值,对N进行分身,即使m_obj被销毁,N的分身还存在于_rhs中,如此才能成功赋值!

 

         SharedObjectHanderT在多线程中不能被引用传递给多个线程使用,这不是它的能力,但是可以成为某个共享对象的数据成员。在多线程穿梭中,可以把这个数据成员拷贝给任何线程,除非保证线程安全否则不要重新设置这个数据成员。

        

         对多线程编程的一点经验:线程中的临时对象是绝对线程安全的,因为它是这个线程堆栈中的对象。

 

         给个问题:两个不同的SharedObjectHanderT对象XYm_obj都指向同一个共享对象,在多线程中如果XY都被赋值新的值(调用赋值函数),那么m_obj安全吗?最后他们的值如何?

 

c++ | 阅读 1512 次
文章评论,共10条
O123
2011-10-28 09:16
1
写的不错。鲜花!!回答下问题,m_obj不安全。当X引用计数为1时赋于其新值,m_obj所指对象将被析构,此时Y的引用计数还不为0,Y的m_obj此时就是野指针了。
O123
2011-10-28 09:24
2
举例(伪码):<br />
X x = p1;//X的引用计数为1。<br />
Y y = p1;//Y的引用计数为1。<br />
x = p2; //此时p1所指对象将析构。参看上面的 operator = 重载方法。<br />
y.method();//此处将使用野指针。
鑫乐源(作者)
2011-10-28 20:12
3
<div class="quote"><span class="q"><b>O123</b>: 举例(伪码):<br />
X x = p1;//X的引用计数为1。<br />
Y y = p1;//Y的引用计数为1。<br />
x = p2; //此时p1所指对象将析构。参看上面的 operator = 重载方法。<br />
y.method();//</span></div>感谢你参与讨论。<br />
可能我的那个问题不够清楚,现在我具体说说这问题是什么样一种情况。<br />
<br />
问题的伪代码是:<br />
SharedObject *A = new SharedObject; // 这是一个共享对象<br />
SharedObjectHandler X = A;&nbsp;&nbsp;// A的引用次数为1<br />
SharedObjectHandler Y = A;&nbsp;&nbsp;// A的引用次数为2<br />
这就是所谓的m_obj都指向同一个共享对象<br />
在多线程情况:<br />
1、两个线程T1和T2<br />
 T1()<br />
{<br />
&nbsp; &nbsp;&nbsp;&nbsp;X = B;&nbsp;&nbsp;// X被赋予了新的共享对象,其内部的A对象引用减1<br />
}<br />
T2()<br />
{<br />
&nbsp; &nbsp;&nbsp;&nbsp;Y = C;&nbsp;&nbsp;// X被赋予了新的共享对象,其内部的A对象引用减1<br />
}<br />
T1和T2执行的先后是不确定的,但是共享对象A是安全的,也是说X和Y得m_obj都被赋予了新值,共享对象A被销毁。<br />
2、同样是两个线程<br />
T1()<br />
{<br />
&nbsp; &nbsp; X = B;<br />
&nbsp; &nbsp; Y = B;<br />
}<br />
T2()<br />
{<br />
&nbsp; &nbsp; X = C;<br />
&nbsp; &nbsp; Y = C;<br />
}<br />
这是一个严重的错误,X和Y的值是不可预知的。并且连共享对象A也变得不可琢磨,SharedObjectHandlerT的赋值函数实现在这种情况下发生了灾难。面对这种事情,我们应该对T1和T2进行同步处理。<br />
<br />
呃,话唠了
sun_shang001
2011-10-28 23:33
4
基本看不懂&nbsp;&nbsp;对我这种刚接触c语言的人来说&nbsp;&nbsp;真的是严重打击&nbsp;&nbsp;努力!!!
O123
2011-10-31 14:45
5
2、同样是两个线程<br />
T1()<br />
{<br />
&nbsp; &nbsp; X = B;<br />
&nbsp; &nbsp; Y = B;<br />
}<br />
T2()<br />
{<br />
&nbsp; &nbsp; X = C;<br />
&nbsp; &nbsp; Y = C;<br />
}<br />
这是一个严重的错误,X和Y的值是不可预知的。并且连共享对象A也变得不可琢磨。<br />
===================<br />
1. 我之前理解错了。Y y = p1 时 p1 的引用计数为 2 了。此时不会出现后面的野指针情况。<br />
2. 我想你所说的X和Y的值不可预知应该是指X或Y的值被其他线程修改了,以至于使用时产生意外的结果。<br />
举个执行序列可能导致这种情况出现:(为清晰起见同时标出A,B,C的引用计数。)<br />
语句 A的计数 B的计数 C的计数<br />
X=A&nbsp; &nbsp; 1&nbsp; &nbsp;&nbsp; &nbsp; 0&nbsp; &nbsp;&nbsp; &nbsp; 0<br />
Y=A&nbsp; &nbsp; 2&nbsp; &nbsp;&nbsp; &nbsp; 0&nbsp; &nbsp;&nbsp; &nbsp; 0<br />
-----------------------------<br />
X=B&nbsp; &nbsp; 1&nbsp; &nbsp;&nbsp; &nbsp; 1&nbsp; &nbsp;&nbsp; &nbsp; 0&nbsp;&nbsp;(T1)<br />
X=C&nbsp; &nbsp; 1&nbsp; &nbsp;&nbsp; &nbsp; 0&nbsp; &nbsp;&nbsp; &nbsp; 1&nbsp;&nbsp;(T2)<br />
此时在T1中我们试图通过X调用B的方法 X-&gt;MethodB,由于在T2中已经将X封装的对象修改为C了,所以此时的调用很有可能就报错。<br />
3. 共享对象A也变得不可琢磨。这个还没找到执行序列(类似第2点)加以说明。是不是说A对等的理解为B或C,还是说A可能产生不同于第2点的异常情况。<br />
求教!<img src="image/face/24.gif" class="face">
鑫乐源(作者)
2011-10-31 19:48
6
<div class="quote"><span class="q"><b>O123</b>: 2、同样是两个线程<br />
T1()<br />
{<br />
&nbsp; &nbsp; X = B;<br />
&nbsp; &nbsp; Y = B;<br />
}<br />
T2()<br />
{<br />
&nbsp; &nbsp; X = C;<br />
&nbsp; &nbsp; Y = C;<br />
}<br />
这是一个严重的错误,X和Y的值是不可预知的。并且连共享对象A也变得不可琢</span></div>3. 共享对象A也变得不可琢磨。这个还没找到执行序列(类似第2点)加以说明。是不是说A对等的理解为B或C,还是说A可能产生不同于第2点的异常情况。<br />
=============================<br />
对着这个函数加以说明吧<br />
SharedObjectHandlerT&amp; operator = (SharedObjectHandlerT _rhs)<br />
&nbsp; &nbsp; {<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;if (m_obj == _rhs.m_obj)<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;return *this;<br />
<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;// Safe_Release(m_obj); 的实现如下:<br />
// 在多线程T1和T2的情况下,下面语句的实现将会是致命的。<br />
// 假设T1执行完if语句,然后切换到T2线程,T2将执行if语句以及Release<br />
// 这时如果m_obj引用计数为0了,那么m_obj将被销毁。之后切换到T1线程<br />
// ,这时m_obj是一个无效的值,在T1中执行m_obj-&gt;Release()语句有可<br />
// 能引发访问违规,有可能什么也不发生。总之不可琢磨,调试的时候很蛋疼。<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;if (m_obj) {<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; m_obj-&gt;Release();<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;m_obj = Null;<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;}<br />
// 下面的语句在多线程下,m_obj为最后一个线程设置的值。然后_rhs.m_obj<br />
// 的引用计数被破坏。<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;if (_rhs.m_obj)<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;{<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;_rhs.m_obj-&gt;AddRef();<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;m_obj = _rhs.m_obj;<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;}<br />
&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;return *this;<br />
&nbsp; &nbsp; }
鑫乐源(作者)
2011-10-31 19:54
7
总之,SharedObjectHandlerT在我说的第一种多线程情况下是安全的。
O123
2011-11-01 09:40
8
明白了。谢谢!<br />
追问一句:可以做到让 SharedObjectHandlerT 本身就是多线程安全的吗?而不必在多线程中使用时进行线程同步处理?
鑫乐源(作者)
2011-11-02 00:32
9
<div class="quote"><span class="q"><b>O123</b>: 明白了。谢谢!<br />
追问一句:可以做到让 SharedObjectHandlerT 本身就是多线程安全的吗?而不必在多线程中使用时进行线程同步处理?</span></div>我曾经也想过这问题,但后来实践证明这样做除了增加运行成本,其他的没什么好处。因为我还没碰到上面说的第二种多线程情况,这里只是对问题深入探讨而已。如果你有好的用例,不妨说说。<br />
<br />
关于在何处进行线程同步,这可能涉及到多方面,一个是需求(哪里需要在哪里),一个是设计原则(只保护资源,根据前面需求)。目前我也没有一个好的定理,或者理论来支持最好在哪线程同步。希望哪位达人说说,拜托了。<br />
<br />
目前我所能知道的线程同步有两种情况。1、同步对象本身;2、同步对象执行状态。<br />
<br />
1、同步对象本身<br />
这个对象是建立在堆上的,然后在多线程中要访问这个对象。这时必须确保这个对象是否有效,这就是要同步对象本身。<br />
2、同步对象执行状态<br />
当调用对象的某个方法后会对对象产生副作用(即改变了对象的状态),那么这个状态在多线程中必须被同步,这样对象的状态才不会混乱。<br />
<br />
SharedObject属于第一种同步情况,它只关注对象本身,对对象的执行状态不能进行同步。<br />
SharedObjectHanderT属于第二种同步情况,但它需要在对象外部同步。<br />
<br />
上面的同步情况都不是绝对的,只有根据实际情况加以设计。<br />
<br />
以上只是个人泛泛之谈,仅作参考!!
jsyesj
2011-11-04 19:52
10
<img src="image/face/29.gif" class="face">
游客请输入验证码
浏览13806次