简体   繁体   English

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

[英]C++: Thread Safety in a Signal/Slot Library

I'm implementing a Signal/Slot framework, and got to the point that I want it to be thread-safe. 我正在实现一个Signal / Slot框架,并且我希望它是线程安全的。 I already had a lot of support from the Boost mailing-list, but since this is not really boost-related, I'll ask my pending question here. 我已经得到了Boost邮件列表的大量支持,但由于这与推文无关,我会在这里提出我的未决问题。

When is a signal/slot implementation (or any framework that calls functions outside itself, specified in some way by the user) considered thread-safe? 什么是信号/槽实现(或任何调用其自身外部功能的框架,由用户以某种方式指定)被认为是线程安全的? Should it be safe wrt its own data, ie the data associated to its implementation details? 是否应该安全自己的数据,即与其实施细节相关的数据? Or should it also take into account the user's data, which might or might not be modified whatever functions are passed to the framework? 或者它是否还应考虑用户的数据,无论函数传递给框架,哪些数据可能修改也可能不修改?

This is an example given on the mailing-list ( Edit: this is an example use-case --ie user code--. My code is behind the calls to the Emitter object ): 这是邮件列表中给出的一个示例( 编辑:这是一个示例用例 - 用户代码 - 。我的代码位于对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();
}

In the above code, it might happen that Event is emitted, causing someFunction to be executed. 在上面的代码中,可能会发生Event发出,导致someFunction被执行。 If somePtr is non- null , but becomes null just after the if , but before the assignment, we're in trouble. 如果somePtr为非null ,但在if ,但在赋值之前变为null ,则我们遇到麻烦。 From the point of view of thread2 , this is not obvious because it is disconnecting someFunction before calling cleanupPtr . 从视图的角度thread2 ,因为它切断这不明摆着someFunction之前调用cleanupPtr

I can see why this could potentially lead to trouble, but who's responsibility is this? 我可以看出为什么这可能会导致麻烦,但谁负责呢? Should my library protect the user from using it in every irresponsible but imaginable way? 我的图书馆是否应该保护用户不以任何不负责任但可想象的方式使用它?

The last question is easy. 最后一个问题很简单。 If you say your library is threadsafe, it should threadsafe. 如果你说你的库是线程安全的,它应该是线程安全的。 It makes no sense to say it is partly threadsafe or, it is only threadsafe if you do not abuse it. 说它是部分线程安全是没有意义的,或者如果你不滥用它,它只是线程安全的。 In that case you have to explain what exactly is not threadsafe. 在这种情况下,你必须解释什么不是线程安全。

Now to your first question regarded someFunction : The operation is non atomic. 现在你的第一个问题someFunction :这个操作是非原子的。 Which means the CPU can interrupt between the if and the assigment . 这意味着CPU可以在ifassigment之间中断。 And that will happen, I know that :-) The other thread can erase the pointer anytime. 那会发生,我知道:-)另一个线程可以随时擦除指针。 Even between two short and fast looking statements. 即使在两个简短而快速的陈述之间。

Now to cleanupPtr : I am not a compiler expert, but if you want to be shure that your assigment take place in the same moment you wrote it in code you should write the keyword volatile in front of the declaration of somePtr . 现在去cleanupPtr :我不是编译专家,但是如果你想避免在你用代码编写它的同一时刻发生你的分配,你应该在somePtr的声明前写下关键字volatile The compiler will now know that you use that attribute in a multithreaded situation and will not buffer the value in a register of the CPU. 编译器现在知道您在多线程情况下使用该属性,并且不会将该值缓冲在CPU的寄存器中。

If you have a thread situation with a reader thread and a writer thread, the keyword volatile can (IMHO) be enough to sync them. 如果你有一个带有读者线程和编写器线程的线程情况,那么关键字volatile can(IMHO)足以同步它们。 As long as the attributes you use to exchange information between threads are generic. 只要用于在线程之间交换信息的属性是通用的。 For other situations you can use mutex or atomics. 对于其他情况,您可以使用互斥或​​原子。 I will give you an example for mutex. 我将举例说明互斥量。 I use C++11 for that, but it works similar with previous versions of C++ using boost. 我使用C ++ 11,但它与使用boost的以前版本的C ++类似。

Using mutex: 使用互斥锁:

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();
}

I only added a recursive mutex here without changing any other code of the sample, even if it's now cargo code. 我只在这里添加了一个递归互斥锁,而不更改样本的任何其他代码,即使它现在是货物代码。 There are two kinds of mutex in the std. std中有两种互斥。 A utterly useless std::mutex and the std::recursive_mutex which work like you expect a mutex should work. 一个完全无用的std::mutexstd::recursive_mutex就像你期望的互斥量一样工作。 The std::mutex exclude the access of any further call even from the same thread. std::mutex甚至从同一个线程中排除了对任何进一步调用的访问。 Which can happen if a method which needs mutex protection calls a public method which use the same mutex. 如果需要互斥保护的方法调用使用相同互斥锁的公共方法,则会发生这种情况。 std::recursive_mutex is reentrant for the same thread. std::recursive_mutex对于同一个线程是可重入的。 Atomics (or interlocks in win32) are another way, but only to exchange values between threads or access them concurrently. 原子(或win32中的互锁)是另一种方式,但只能在线程之间交换值或同时访问它们。 Your example is missing such values, but in your case, I would look a little deeper in them (std::atomic). 你的例子缺少这样的值,但在你的情况下,我会更深入地了解它们(std :: atomic)。

UPDATE UPDATE

If your are the user of a library which is not explicit declared as threadsafe by the developer, take it as non threadsafe and shield every call to it with a mutex lock. 如果您是开发人员未明确声明为线程安全的库的用户,请将其视为非线程安全,并使用互斥锁屏蔽每次调用它。 To stick with the example. 坚持这个例子。 If you cannot change someFunction the you have to wrap the function like: 如果你不能改变someFunction ,你必须包装函数,如:

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

I suspect there is no clearly good answer, but clarity will come from documenting the guarantees you wish to make about concurrent access to an Emitter object. 我怀疑没有明确好的答案,但清晰度将来自记录您希望对并发访问Emitter对象的保证。

One level of guarantee, which to me is what is implied by a promise of thread safety, is that: 对我来说,一个保证级别是线程安全承诺所隐含的,是:

  • Concurrent operations on the object are guaranteed to leave the object in a consistent state (at least, from the point of view of the accessing threads.) 保证对象上的并发操作使对象保持一致状态(至少从访问线程的角度来看)。
  • Non-commutative operations will be performed as if they were scheduled serially in some (unknown) order. 将执行非交换操作,就像它们以某种(未知)顺序串行安排一样。

Then the question is, what does the emit method promise semantically: passing control to the connected routine, or evaluation of the function? 那么问题是,emit方法在语义上有什么承诺:将控制传递给连接的例程,还是评估函数? If the former, then your work sounds like it is already done; 如果是前者,那么你的工作听起来已经完成了; if the latter, then the 'as-if ordered' requirement would mean that you need to enforce some level of synchronisation. 如果是后者,则“按订单排序”要求意味着您需要强制执行某种级别的同步。

Users of the library can work with either, provided it is clear what is being promised. 如果明确承诺的话,图书馆的用户可以使用其中任何一个。

Firstly the simplest possibility: If you don't claim your library to be thread-safe, you don't have to bother about this. 首先是最简单的可能性:如果您没有声称您的库是线程安全的,那么您不必为此烦恼。

(But even) if you do: In your example the user would have to take care about thread-safety, since both functions could be dangerous, even without using your event-system (IMHO, this is a pretty good way to determine who should take care about those kind of problems). (但即便如果你这样做):在你的例子中,用户必须注意线程安全,因为这两个函数都可能是危险的,即使不使用你的事件系统(恕我直言,这是一个很好的方法来确定谁应该照顾那些问题)。 A possible way for him to do this in C++11 could be: 他在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'
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM