多线程下的设计模式研究(一):原子对象模式

来源:互联网 发布:网络音乐地址mp3 编辑:程序博客网 时间:2024/06/13 17:07
序言

所谓设计模式,不过是人们在编程活动中总结出来的一些常用的、有效的解决方法而已。自从我开始接触设计后,我就时常留意自己的常用的解决方案,希望能将这些解决方法抽象出来,形成模式。现在这一系列文章,是我这几年编码活动的一个总结,也许其中很多谬误,但至少在目前,它对我工作的帮助,起了很大作用。
现在整理发表出来,与大家互相切磋。如发现其中不合理的地方,恳请不留情面地指出来。

多线程下的设计模式研究(一):原子对象模式

[引言]
相信每一个在多线程环境下工作的程序员,都遇到过死锁、访问冲突等问题。这类问题来无影去无踪,难以重现、难以定位,是程序员中谈虎色变的一大bug。
使用原子对象模式,可以有效解决这个问题。
所谓原子对象模式,就是把需要在多线程环境下使用的数据,封装成一个对象,并在实现的时候保证所有公开的接口都是线程安全的。这样的一类对象,我们就可以称之为原子对象。
这种对象为调用者带来了很大的方便。任何一个线程都可以方便地使用这些对象,使用的时候不用考虑锁的问题。

[举例]
你有多个线程,需要访问一个用户列表。每个线程都有可能查找特定的用户,也有可能增加用户和删除用户。
下面是一个作为原子对象的用户列表的例子。为简单起见,该原子对象仅仅提供增加用户、查找某年龄段内的用户两种方法。

//用户结构体
struct User
{
int id;        //用户ID
string name;    //用户名
int age;    //用户年龄
};
//用户列表类型,是一个map
typedef map<int,TUser> T_mapUser;   

//用户列表原子对象
class MapUserAtom
{
private:
    ...
    //用户列表定义,可能是全局变量,也有可能是一个类的成员变量,该变量作为参数传给其它线程
    T_mapUser m_mapUser;    //用户列表的全局变量;
    CRritica_SECTION m_csForMapUser;    //专门用来锁定g_mapUser的线程锁
   
public:
    ...
    //增加用户接口。此接口是线程安全的
    void AddUser(User user)
    {
        //这是一个“范围锁”。
        //对象构造的时候自动上锁,对象析构的时候自动解锁。这种锁的好处是使用方便,且发生异常时能保证安全解锁。
        //这种锁可以自己实现,也可以在其它类库(如boost的多线程库、ACE)等地方找到。
        //此后的代码里,我都用这种锁作为例子,不再注释。
        CRangeLock tmpLock(m_csForMapUser);
       
        //往列表里增加用户
        m_mapUser.insert(T_mapUser::value_type(user.id,user));
    }
   
    //查找某年龄段内的用户,在lstUser中返回
    //此接口是线程安全的
    void FindUser(int oldAge, int youngAge,list<User> &lstUser)
    {
        //这是一个“范围锁”。
        CRangeLock tmpLock(m_csForMapUser);
       
        //遍历列表,查找用户
        T_mapUser::const_iterator cit = m_mapUser.begin();
        for(;cit!=m_mapUser.end();cit++)
        {
            User &user = (*cit).second;
            if(user.age<=oldAge && user.age>youngAge)
            {
                lstUser.push_back(user);
            }
        }
    }
};


[模式的优点和使用范围]
原子对象模式貌不惊人,但威力巨大。如果用原子对象将系统中的敏感数据全部封装起来,会有如下好处:
1)可以保证不会有访问冲突的情况。因为敏感数据都已经用对象封装起来了,对它们的访问都可以保证是安全的;
2)可以避免死锁,或者令死锁检测变得容易。如果原子对象内,不使用其它原子对象,就不可能产生死锁。因为这样就没有锁嵌套使用的情况。反过来,如果产生了死锁,肯定是在原子对象内使用了其它原子对象,检视这部分代码就可以了。

[模式的代价]
   使用原子对象,需要牺牲一定的性能和使用方便性。例如前面的查找用户函数,它需要将找到的用户保存在另外一个列表中返回,而不能直接遍历该map对象。

[模式要注意的问题]
   要控制好原子对象的粒度。封装得太小,可能起不到效果,也可能导致锁数量过多,系统性能下降。在上面的例子中,原子对象应当是“用户列表”,而不是“用户”本身。反之,如果将系统中所有的敏感数据都封装成一个原子对象,由于锁的使用太频繁,实际上的效果相当于单线程程序,效率也会有极大的影响。
   原子对象内的函数最好不要嵌套使用,因为这样会导致锁嵌套调用,在某些情况下可能会有问题。非公开函数应当设计成不使用锁的,而且不要调用公开函数(因为有锁)。
   原子对象如果允许继承和拷贝,在实现的时候要仔细考虑,这是比较容易出错的地方。
   
原创粉丝点击