C ++:信号/插槽库中的线程安全

时间:2015-02-15 12:40:10

标签: c++ multithreading thread-safety signals-slots

我正在实现一个Signal / Slot框架,并且我希望它是线程安全的。我已经得到了Boost邮件列表的大量支持,但由于这与推文无关,我会在这里提出我的未决问题。

什么时候信号/槽实现(或任何调用其自身外部功能的框架,用户以某种方式指定)被认为是线程安全的?它是否应该安全w.r.t.它自己的数据,即与其实施细节相关的数据?或者它是否还应考虑用户的数据,无论函数传递给框架,哪些数据可能会被修改,也可能不被修改?

这是邮件列表上给出的示例(编辑:这是一个示例用例 - 用户代码 - 。我的代码是对Emitter对象的调用的背后):

int * somePtr = nullptr;
Emitter<Event> em; // just an object that can emit the 'Event' signal    

void mainThread()
{
    em.connect<Event>(someFunction);

    // now, somehow, 2 threads are created which, at some point
    // execute the thread1() and thread2() functions below
}

void someFunction()
{
    // can somePtr change after the check but before the set?
    if (somePtr)
        *somePtr = 17;
}

void cleanupPtr()
{
    // this looks safe, but compilers and CPUs can reorder this code:
    int *tmp = somePtr;
    somePtr = null;
    delete tmp;
}

void thread1()
{
    em.emit<Event>();
}

void thread2()
{
    em.disconnect<Event>(someFunction);
    // now safe to cleanup (?)
    cleanupPtr();
}

在上面的代码中,可能会发生Event,导致someFunction被执行。如果somePtr不是null,而是在null之后,但在转让之前成为if,我们就遇到了麻烦。从thread2的角度来看,这并不明显,因为它在调用someFunction之前断开cleanupPtr

我可以看出为什么这可能会导致麻烦,但是谁负责呢?我的图书馆是否应该保护用户不以任何不负责任但可想象的方式使用它?

3 个答案:

答案 0 :(得分:2)

最后一个问题很简单。如果你说你的库是线程安全的,它应该是线程安全的。说它是部分线程安全是没有意义的,或者如果你不滥用它,它只是线程安全的。在这种情况下,你必须解释什么不是线程安全。

现在你的第一个问题是someFunction: 该操作是非原子的。这意味着CPU可以在ifassigment之间中断。那会发生,我知道:-)另一个线程可以随时擦除指针。即使在两个简短而快速的陈述之间。

现在到cleanupPtr: 我不是编译专家,但是如果您希望在您在代码中编写它的同一时刻发现您的分配,那么您应该在volatile的声明前写下关键字somePtr。编译器现在知道您在多线程情况下使用该属性,并且不会将该值缓冲在CPU的寄存器中。

如果您有一个带有读者线程和编写器线程的线程情况,关键字volatile可以(恕我直言)足以同步它们。只要用于在线程之间交换信息的属性是通用的。 对于其他情况,您可以使用互斥或​​原子。我将举例说明互斥量。我使用C ++ 11,但它与使用boost的以前版本的C ++类似。

使用互斥锁:

int * somePtr = nullptr;
Emitter<Event> em; // just an object that can emit the 'Event' signal    
std::recursive_mutex g_mutex;

void mainThread()
{
    em.connect<Event>(someFunction);

    // now, somehow, 2 threads are created which, at some point
    // execute the thread1() and thread2() functions below
}

void someFunction()
{
    std::lock_guard<std::recursive_mutex> lock(g_mutex);
    // can somePtr change after the check but before the set?
    if (somePtr)
        *somePtr = 17;
}

void cleanupPtr()
{
    std::lock_guard<std::recursive_mutex> lock(g_mutex);
    // this looks safe, but compilers and CPUs can reorder this code:
    int *tmp = somePtr;
    somePtr = null;
    delete tmp;
}

void thread1()
{
    em.emit<Event>();
}

void thread2()
{
    em.disconnect<Event>(someFunction);
    // now safe to cleanup (?)
    cleanupPtr();
}

我只在这里添加了一个递归互斥锁,而不更改样本的任何其他代码,即使它现在是货物代码。 std中有两种互斥。一个完全没用的std::mutexstd::recursive_mutex就像你期望的那样工作应该有效。 std::mutex即使从同一个线程也排除了对任何进一步调用的访问。如果需要互斥保护的方法调用使用相同互斥锁的公共方法,则会发生这种情况。 std::recursive_mutex可重入同一个帖子。 原子(或win32中的互锁)是另一种方式,但只能在线程之间交换值或同时访问它们。你的例子缺少这样的值,但在你的情况下,我会更深入地了解它们(std :: atomic)。

<强>更新

如果您是开发人员未明确声明为线程安全的库的用户,请将其视为非线程安全,并使用互斥锁屏蔽每次调用它。 坚持这个例子。如果您无法更改someFunction,则必须将函数包装为:

void threadsafeSomeFunction()
{
  std::lock_guard<std::recursive_mutex> lock(g_mutex);
  someFunction();
}

答案 1 :(得分:1)

我怀疑没有明确的好答案,但是要记录您希望对并发访问Emitter对象所做的保证。

对我来说,一个保证级别是线程安全承诺所隐含的,是:

  • 保证对象上的并发操作使对象保持一致状态(至少从访问线程的角度来看。)
  • 将执行非交换操作,就像它们按某些(未知)顺序连续安排一样。

然后问题是,emit方法在语义上有什么承诺:将控制传递给连接的例程,或者评估函数?如果是前者,那么你的工作听起来已经完成了;如果是后者,那么'as-if ordered'要求意味着你需要强制执行某种程度的同步。

图书馆的用户可以使用其中任何一个,只要明确承诺的内容。

答案 2 :(得分:1)

首先,最简单的可能性是:如果您没有声称您的库是线程安全的,那么您不必为此烦恼。

(但即便如果你这样做): 在您的示例中,用户必须注意线程安全,因为即使不使用您的事件系统,这两个函数都可能是危险的(恕我直言,这是确定谁应该关注这些问题的一个很好的方法) 。他在C ++ 11中执行此操作的可能方法可能是:

#include <mutex>

// A mutex is used to control thread-acess to a shared resource
std::mutex _somePtr_mutex;

int* somePtr = nullptr;

void someFunction()
{
    /*
        Create a 'lock_guard' to manage your mutex.

        Is the mutex '_somePtr_mutex' already locked?
            Yes: Wait until it's unlocked.
            No: Lock it and continue execution.
    */
    std::lock_guard<std::mutex> lock(_somePtr_mutex);

    if(somePtr)
        *somePtr = 17;

    // End of scope: 'lock' gets destroyed and hence unlocks '_somePtr_mutex'
}

void cleanupPtr()
{
    /*
        Create a 'lock_guard' to manage your mutex.

        Is the mutex '_somePtr_mutex' already locked?
            Yes: Wait until it's unlocked.
            No: Lock it and continue execution.
    */
    std::lock_guard<std::mutex> lock(_somePtr_mutex);

    int *tmp = somePtr;
    somePtr = null;
    delete tmp;

    // End of scope: 'lock' gets destroyed and hence unlocks '_somePtr_mutex'
}