从多个线程编写静态数据几乎是安全的

时间:2009-02-26 12:05:28

标签: c++ multithreading

我有一些我想从数据库缓存的状态数据。多个线程中的任何一个都可以修改状态数据。修改数据后,它将被写入数据库。数据库写入将始终由底层数据库访问层按顺序完成,该数据库访问层将数据库操作排入不同的进程,因此我不关心这些操作的竞争条件。

仅修改来自多个线程的静态数据是否有问题?从理论上讲,修改可以实现为读取,修改,写入,但在实践中我无法想象这是如此。

我的数据处理类看起来像这样:

class StatusCache
{
public:
    static void SetActivityStarted(bool activityStarted)
        { m_activityStarted = activityStarted; WriteToDB(); }
    static void SetActivityComplete(bool activityComplete);
        { m_activityComplete = activityComplete; WriteToDB(); }
    static void SetProcessReady(bool processReady);
        { m_processReady = processReady; WriteToDB(); }
    static void SetProcessPending(bool processPending);
        { m_processPending = processPending; WriteToDB(); }
private:
    static void WriteToDB(); // will write all the class data to the db (multiple requests will happen in series)
    static bool m_activityStarted;
    static bool m_activityComplete;
    static bool m_processReady;
    static bool m_processPending;
};

我不想使用锁,因为应用程序的这一部分已经有几个锁,添加更多会增加死锁的可能性。

数据库更新中的两个线程之间是否存在某些重叠并不重要,例如

thread 1                        thread 2                    activity started in db
SetActivityStarted(true)        SetActivityStarted(false)   
  m_activityStated = true
                                  m_activityStarted = false
                                  WriteToDB()               false
  WriteToDB()                                               false

因此数据库显示最近由m _... = x行设置的状态。这没关系。

这是一种合理的使用方法还是有更好的方法?

[编辑说我只关心最后的状态 - 顺序不重要]

10 个答案:

答案 0 :(得分:8)

不,这不安全。

生成的代码用于写入m_activityStarted,而其他代码可能是原子的,但这并不是garantueed。此外,在你的setter中你做两件事:设置一个布尔值并进行调用。这肯定不是原子的。

你最好使用某种锁来同步这里。

例如,一个线程可以调用第一个函数,在该线程进入“WriteDB()”之前,另一个线程可能会调用另一个函数并进入WriteDB()而不会先进入那里。然后,状态可能以错误的顺序写入DB中。

如果您担心死锁,那么您应该修改整个并发策略。

答案 1 :(得分:3)

在多CPU机器上,无法保证在不发出同步指令的情况下,以正确顺序在不同CPU上运行的线程将看到内存写入。只有在您发出同步订单时,例如互斥锁定或解锁,保证每个线程的数据视图保持一致。

为了安全起见,如果你想在你的线程之间共享状态,你需要使用某种形式的同步。

答案 2 :(得分:3)

你永远不知道在较低级别如何实现事情。特别是当你开始处理多个内核,各种缓存级别,流水线执行等时。至少没有大量工作,实现经常更改!

如果你没有互斥它,最终你会后悔!

我最喜欢的例子涉及整数。这个特定的系统在两次写入中写入了它的整数值。例如。不是原子的。当然,当线程在这两个写操作之间被中断时,你从一个set()调用获得了高位字节,而从另一个调用获得了低位字节()。一个经典的错误。但远非可能发生的最坏情况。

Mutexing是微不足道的。

你提到: 我不想使用锁,因为应用程序的这一部分已经存在几个锁,添加更多会增加死锁的可能性。

只要遵循黄金法则,你就没事了:

  • 不要混用互斥锁定命令。例如。 A.lock(); B.lock()在一个地方和B.lock(); A.lock();在另一个。使用一个订单或另一个订单!
  • 锁定最短的时间。
  • 请勿尝试将一个互斥锁用于多种用途。使用多个互斥锁。
  • 尽可能使用递归或错误检查互斥锁。
  • 使用RAII或宏来确保解锁。

E.g:

#define RUN_UNDER_MUTEX_LOCK( MUTEX, STATEMENTS ) \
   do { (MUTEX).lock();  STATEMENTS;  (MUTEX).unlock(); } while ( false )

class StatusCache
{
public:
    static void SetActivityStarted(bool activityStarted)
        { RUN_UNDER_MUTEX_LOCK( mMutex, mActivityStarted = activityStarted );
          WriteToDB(); }
    static void SetActivityComplete(bool activityComplete);
        { RUN_UNDER_MUTEX_LOCK( mMutex, mActivityComplete = activityComplete );
          WriteToDB(); }
    static void SetProcessReady(bool processReady);
        { RUN_UNDER_MUTEX_LOCK( mMutex, mProcessReady = processReady );
          WriteToDB(); }
    static void SetProcessPending(bool processPending);
        { RUN_UNDER_MUTEX_LOCK( mMutex, mProcessPending = processPending );
          WriteToDB(); }

private:
    static void WriteToDB(); // read data under mMutex.lock()!

    static Mutex mMutex;
    static bool  mActivityStarted;
    static bool  mActivityComplete;
    static bool  mProcessReady;
    static bool  mProcessPending;
};

答案 3 :(得分:1)

我不是c ++家伙,但如果你没有某种同步,我不认为写它是安全的。

答案 4 :(得分:1)

看起来你有两个问题。

#1是你的布尔赋值不一定是原子的,即使它是你代码中的一个调用。所以,在引擎盖下,你可能会有不一致的状态。如果你的线程/并发库支持使用atomic_set(),你可以考虑使用它。

#2是您的阅读和写作之间的同步。从您的代码示例中,看起来您的WriteToDB()函数会写出所有4个变量的状态。 WriteToDB在哪里序列化?你是否有一种情况,其中thread1启动WriteToDB(),它读取m_activityStarted但没有完成将其写入数据库,然后被thread2抢占,它一直写入m_activityStarted。然后,thread1恢复,并完成将其不一致状态写入数据库。至少,我认为在进行数据库更新所需的读取访问时,您应该对锁定的静态变量具有写访问权。

答案 5 :(得分:1)

  

理论上,修改可以实现为读取,修改,写入,但在实践中我无法想象这是如此。

除非你设置某种事务内存,否则通常都是如此。变量通常存储在RAM中,但在硬件寄存器中进行了修改,因此读取不仅仅是为了解决问题。读取是将值从RAM复制到可以修改的位置(甚至与另一个值进行比较)所必需的。当数据在硬件寄存器中被修改时,如果有人想将其复制到另一个硬件寄存器中,那么陈旧值仍然在RAM中。当修改后的数据被写回RAM时,其他人可能正在将其复制到硬件寄存器中。

在C ++中,保证至少占用一个字节的空间。这意味着它们实际上可能有一个非真或假的值,比如由于竞争条件,其中读取发生在写入的中途。

在.Net上有一些静态数据和静态方法的自动同步。标准C ++中没有这样的保证。

如果你只关注整数,布尔和(我认为)多头,你可以选择原子读/写和加法/减法。 C ++ 0x有somethingIntel TBB也是如此。我相信大多数操作系统也都有必要的钩子来实现这一点。

答案 6 :(得分:1)

虽然你可能害怕死锁,但我相信你会为你的代码感到非常自豪,因为它知道它完美无缺。
所以我建议你扔掉锁,你可能还想考虑信号量,一种更原始​​(也许更通用)的锁类型。

答案 7 :(得分:0)

你可以用bool来逃避它,但是如果被改变的静态物体具有任何复杂性的类型,那么将会发生可怕的事情。我的建议 - 如果你打算从多个线程写入,总是使用同步对象,否则你迟早会被咬伤。

答案 8 :(得分:0)

这不是一个好主意。有许多变量会影响不同线程的时序。

如果没有某种锁定,您将无法保证拥有正确的最后状态。

可能无法将两个状态更新无序写入数据库。

只要锁定代码设计得当,死锁就不应该像这样简单的过程出现问题。

答案 9 :(得分:0)

正如其他人所指出的,这通常是一个非常糟糕的主意(有一些警告)。

仅仅因为您在测试时没有在您的特定计算机上发现问题,并不能证明该算法正常运行。对于并发应用程序尤其如此。例如,当您切换到具有不同内核数量的计算机时,交错可能会发生显着变化。

警告:如果你的所有制定者都在进行原子写作,如果你不关心它们的时间,那么你可能没问题。

根据你所说的,我认为你可能只有一个在设置者中设置的脏标志。单独的数据库写入线程会经常轮询脏标志并将更新发送到数据库。如果某些项目需要额外的原子性,他们的setter将需要锁定互斥锁。数据库写入线程必须始终锁定互斥锁。