线程安全的数据结构设计

时间:2010-03-19 10:37:30

标签: multithreading synchronization data-structures

我必须设计一个在多线程环境中使用的数据结构。基本API很简单:插入元素,删除元素,检索元素,检查元素是否存在。结构的实现使用隐式锁定来保证单个API调用的原子性。在我实现这一点后,很明显,我真正需要的是几个API调用的原子性。例如,如果调用者在尝试插入元素之前需要检查元素的存在,即使每个单独的API调用 原子,他也不能自动执行该操作:

if(!data_structure.exists(element)) {
   data_structure.insert(element);
}

这个例子有点尴尬,但基本的一点是我们在从原子上下文返回后不能再信任“exists”调用的结果了(生成的程序集清楚地显示了两次调用之间上下文切换的次要机会) )。

我目前要解决的问题是通过数据结构的公共API公开锁。这样客户端就必须明确锁定东西,但至少他们不必创建自己的锁。是否有一个更好的常见解决方案来解决这些问题?只要我们参与其中,您能否就线程安全设计提供一些好的文献?

编辑:我有一个更好的例子。假设元素检索返回引用或指向存储元素的指针而不是它的副本。在调用返回后,如何保护调用者以安全地使用此指针\引用?如果您认为不返回副本是一个问题,那么请考虑深层副本,即应该复制其指向内部的其他对象的对象。

谢谢。

5 个答案:

答案 0 :(得分:4)

您要么提供外部锁定机制(错误),要么重新设计API,例如putIfAbsent。后一种方法例如用于Java的concurrent data-structures.

而且,对于这些基本的集合类型,您应该检查您选择的语言是否已经在其标准库中提供它们。

[编辑]澄清:外部锁定对类的用户不利,因为它引入了潜在错误的另一个来源。是的,有时,当性能考虑事项确实使并发数据结构比外部同步数据结构更糟糕时,但这些情况很少见,然后它们通常只能由具有比我更多的知识/经验的人来解决/优化。

一个,也许很重要,性能提示可以在下面的Will's answer中找到。 [/编辑]

[edit2]给出你的新例子:基本上你应该尽量保持集合和元素的同步分离。如果元素的生命周期与其在一个集合中的存在绑定,则会遇到问题;使用GC时,这种问题实际上变得更加简单。否则,您将不得不使用一种代理而不是原始元素在集合中;在最简单的C ++情况下,您可以使用boost::shared_ptr,它使用原子引用计数。在此处插入通常的效果免责声明。当您使用C ++时(我怀疑您在讨论指针和引用时),boost::shared_ptrboost::make_shared的组合应该足够了。 [/ EDIT2]

答案 1 :(得分:3)

创建要插入的元素有时很昂贵。在这些场景中,您无法正常地创建可能已经存在的对象,以防它们存在。

一种方法是insertIfAbsent()方法返回一个被锁定的'游标' - 它将一个占位符插入到内部结构中,这样任何其他线程都不会认为它不存在,但是不插入新对象。占位符可以包含一个锁,以便其他想要访问该特定元素的线程必须等待它插入。

在像C ++这样的RAII语言中,您可以使用智能堆栈类来封装返回的游标,以便在调用代码未提交时自动回滚。在Java中,它使用finalize()方法更加延迟,但仍可以使用。

另一种方法是让调用者创建不存在的对象,但如果另一个线程“赢得了比赛”,则偶尔会在实际插入时失败。这就是例如memcache更新的完成方式。它可以很好地工作。

答案 2 :(得分:0)

将存在检查移到.insert()方法怎么样?客户端调用它,如果它返回false,您就知道出了问题。很像malloc()在普通旧C中的作用 - 如果失败则返回NULL,设置ERRNO

显然你也可以返回异常或对象的实例,并使你的生活复杂化。

但请不要依赖用户设置自己的锁。

答案 3 :(得分:0)

首先,您应该真正区分您的疑虑。你有两件事需要担心:

     
  1. 数据结构及其方法。
  2.  
  3. 线程同步。
  4. 我强烈建议您使用代表您正在实现的数据结构类型的接口或虚拟基类。创建一个根本不执行任何锁定的简单实现。然后,创建第二个实现,它包装第一个实现并在其上添加锁定。这将允许更高性能的实现,其中不需要锁定,并将大大简化您的代码。

    看起来你正在实现某种字典。您可以做的一件事是提供具有与组合语句等效的语义的方法。例如,setdefault是一个合理的函数,只有在字典中不存在相应的键时才会设置值。

    换句话说,我的建议是找出经常一起使用的方法组合,并简单地创建以原子方式执行操作组合的API方法。

答案 4 :(得分:0)

以RAII风格的方式,您可以创建访问器/句柄对象(不知道它的调用方式,可能存在这种模式),例如:一个清单:

template <typename T>
class List {
    friend class ListHandle<T>;
    // private methods use NO locking
    bool _exists( const T& e ) { ... }
    void _insert( const T& e ) { ... }
    void _lock();
    void _unlock();
public:
    // public methods use internal locking and just wrap the private methods
    bool exists( const T& e ) {
        raii_lock l;
        return _exists( e );
    }
    void insert( const T& e ) {
        raii_lock l;
        _insert( e );
    }
    ...
};

template <typename T>
class ListHandle {
    List<T>& list;
public:
    ListHandle( List<T>& l ) : list(l) {
        list._lock();
    }
    ~ListHandle() {
        list._unlock();
    }
    bool exists( const T& e ) { return list._exists(e); }
    void insert( const T& e ) { list._insert(e); }
};


List<int> list;

void foo() {
    ListHandle<int> hndl( list ); // locks the list as long as it exists
    if( hndl.exists(element) ) {
        hndl.insert(element);
    }
    // list is unlocked here (ListHandle destructor)
}

您复制(甚至重复)公共界面,但您可以选择在需要的地方选择内部安全和舒适的外部锁定。