【问题标题】:Is the function thread-safe?函数是线程安全的吗?
【发布时间】:2015-12-10 15:14:38
【问题描述】:

我想避免从进程中的线程多次调用 RegisterClassEx()。为此,我修改了现有功能,如下所示。

当前代码结构对使用替代方法来实现线程安全性施加了限制,因此目前我试图坚持最少的更改。

请让我知道您对以下代码的看法。

foo()
{   
    static ATOM atom = 0;
    if( atom == 0 )
    {
        {
            EnterCriticalSection(&m_CSRegisterClassEx);
            if( atom == 0 )
            {
                atom = RegisterClassEx(&tCls);      

                if( atom == 0)
                {
                    ERROR(L"RegisterClassEx failed! );
                    LeaveCriticalSection(&m_CSRegisterClassEx); 
                    return 0; 
                }
                else
                {
                    ERROR(L"RegisterClassEx good!");
                    LeaveCriticalSection(&m_CSRegisterClassEx);
                    return atom;
                }
            }

        }
    }
    else
    {
        ERROR(L"using atom[%ld] from last call!", atom);
        return atom; 
    }
}   

在此处输入代码

【问题讨论】:

  • 您在atom 上存在明显的数据竞争,导致您的代码格式不正确。
  • 当您从多个线程访问一个值时,需要在显式互斥体中保护它以确保互斥,或者包装在 std::atomic 中。
  • 如果那是我的代码,我会编写一个函数 ATOM get_atom() 来完成所有 RegisterClassEx 的内容减去关键部分的内容,然后只写 ATOM foo(){ static ATOM atom = get_atom(); return atom;} 并依赖 magic statics (requires VS 2015)
  • 我相信多个线程可以找到第一个 if 语句为真,其中只有一个会通过 EnterCriticalSection() 然后注册该类,而其他线程则在睡眠队列中。当他们醒来并进入临界区时,我做的第一件事就是再次检查 atom 的值,这将被更新。这够了吗?我缺少哪个代码路径?
  • 代码不安全。这是双重检查锁定模式的实现,为了安全起见,您需要使用内存屏障(考虑使用std::atomic<int> ATOM 而不是int ATOM)。

标签: c++ multithreading winapi thread-safety


【解决方案1】:

双重检查模式不是线程安全的,请查看这篇文章http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf

这里有一个解决方案,类似于Qt的Q_GLOBAL_STATIC实现和chrome的signleton实现。

template<class T>
class  TwStaticObject
{
public:
TwStaticObject(void)
    : p(nullptr)
    , x(0)
{

}
~TwStaticObject(void)
{

}

struct Deleter
{
    Deleter(TwStaticObject& This)
    : __this(This)
    {
        ;
    }
    ~Deleter()
    {
        if (__this.p)
        {
            delete __this.p;
            __this.p = 0;
            __this.x = 0;

        }
    }
    TwStaticObject& __this;
};

static T* singletonInstance(TwStaticObject& thisobj)
{ 
    if (InterlockedCompareExchange(&thisobj.x, 1, 0) == 0)
    {
        static T* obj = new T;
        InterlockedExchangePointer((volatile PVOID *)&thisobj.p, (PVOID)obj);
        static Deleter ThisDelter(thisobj);
    }
    else
    {
        while (thisobj.p == nullptr)
        {
            Sleep(0);
        }
    }
    return thisobj.p;
}

T* volatile p ;
volatile long x ;
};


#define  TwDefine_SingleTon(Type, FUN)  \
static TwStaticObject<Type> ThisSingleTon##Type##FUN;\
static Type* FUN()\
{\
    return TwStaticObject<Type>::singletonInstance(ThisSingleTon##Type##FUN);\
}

然后,注册:

class Register{
public:
    Register(){
        atom = RegisterClassEx(&tCls); 
    }

    ATOM atom;
};

TwDefine_Static(Register, _register);

需要时调用 _register()。

【讨论】:

    【解决方案2】:

    这是经典的双重检查锁定。这里的主要问题如下: 比方说,线程一第一次进入函数。检查 atom 为 0,锁定临界区并开始初始化。它将原子设置为非 0,并且此更改反映在主内存中(因为它可以!)。但是,RegisterClass 调用的结果不会反映在主内存中,而是留在 CPU 缓存中。之后,CPU 判断它过热并进入睡眠状态。

    在这个完美的时间,另一个线程进入不同 CPU 上的函数。它看到 ATOM 不为空,并愉快地返回它 - 但无论谁要使用该原子,都会大吃一惊!

    编辑

    刚刚注意到有问题的编译器。由于VS2010不支持线程安全的静态变量,这里还有一个问题。原子可能会被重新初始化为 0 秒。这可能发生在以下情况:

    线程一进入函数,检查与原子相关的隐藏“初始化”值,发现它为假,将原子设置为 0,初始化为真,进入临界区并注册类 - 此时它进入睡眠状态。第二个线程进入,检查 inited 值,发现它是假的 - 因为它的一侧没有阅读栅栏,所以它可以读取过时的值 - 并将 atom 设置为 0。需要我继续吗?

    明显的解决方案(不是最好的解决方案,但考虑到约束)是在定义静态之前进入临界区。

    【讨论】:

    • 您也应该添加一个解决方案。这可能归结为扩展了关键部分,因为 VS2010 自然不适用于 C++11。
    • 这是错误的。一旦RegisterClassEx 返回,则窗口类已完全注册,ATOM 有效。像所有 USER 函数一样,它是开箱即用的线程安全的。而且您错过了真正的问题:ATOM 不是原子的,更改可能不会反映在其他线程中。
    • @nwp 问题不是寻求解决方案,这当然是显而易见的——在原子周围也使用临界区。
    • @andlabs:内存访问是无序的。编译器和 CPU 都可以重新排序内存读取和写入。您需要建立内存屏障以确保跨线程对内存访问进行排序。 std::atomic 是解决此问题的一种方法。发布的代码还有另一个问题:atom 没有正确的 cv 限定,编译器可以驱逐关键部分内的内存访问并重新使用之前加载到寄存器中的值。即使线程被序列化,代码仍然可以使用陈旧的值。
    • 编译器可以实现任何优化,只要它遵守 as-if 规则。知道EnterCriticalSection 不会修改atom 的编译器可以实现此优化。如果您认为这不是真的,请提供对明确禁止此优化的 C++ 标准的引用。
    猜你喜欢
    • 2023-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-24
    • 2021-12-16
    • 1970-01-01
    相关资源
    最近更新 更多