我有一些我想从数据库缓存的状态数据。多个线程中的任何一个都可以修改状态数据。修改数据后,它将被写入数据库。数据库写入将始终由底层数据库访问层按顺序完成,该数据库访问层将数据库操作排入不同的进程,因此我不关心这些操作的竞争条件。
仅修改来自多个线程的静态数据是否有问题?从理论上讲,修改可以实现为读取,修改,写入,但在实践中我无法想象这是如此。
我的数据处理类看起来像这样:
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行设置的状态。这没关系。
这是一种合理的使用方法还是有更好的方法?
[编辑说我只关心最后的状态 - 顺序不重要]
答案 0 :(得分:8)
不,这不安全。
生成的代码用于写入m_activityStarted,而其他代码可能是原子的,但这并不是garantueed。此外,在你的setter中你做两件事:设置一个布尔值并进行调用。这肯定不是原子的。
你最好使用某种锁来同步这里。
例如,一个线程可以调用第一个函数,在该线程进入“WriteDB()”之前,另一个线程可能会调用另一个函数并进入WriteDB()而不会先进入那里。然后,状态可能以错误的顺序写入DB中。
如果您担心死锁,那么您应该修改整个并发策略。
答案 1 :(得分:3)
在多CPU机器上,无法保证在不发出同步指令的情况下,以正确顺序在不同CPU上运行的线程将看到内存写入。只有在您发出同步订单时,例如互斥锁定或解锁,保证每个线程的数据视图保持一致。
为了安全起见,如果你想在你的线程之间共享状态,你需要使用某种形式的同步。
答案 2 :(得分:3)
你永远不知道在较低级别如何实现事情。特别是当你开始处理多个内核,各种缓存级别,流水线执行等时。至少没有大量工作,实现经常更改!
如果你没有互斥它,最终你会后悔!
我最喜欢的例子涉及整数。这个特定的系统在两次写入中写入了它的整数值。例如。不是原子的。当然,当线程在这两个写操作之间被中断时,你从一个set()调用获得了高位字节,而从另一个调用获得了低位字节()。一个经典的错误。但远非可能发生的最坏情况。
Mutexing是微不足道的。
你提到: 我不想使用锁,因为应用程序的这一部分已经存在几个锁,添加更多会增加死锁的可能性。
只要遵循黄金法则,你就没事了:
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有something。 Intel TBB也是如此。我相信大多数操作系统也都有必要的钩子来实现这一点。
答案 6 :(得分:1)
虽然你可能害怕死锁,但我相信你会为你的代码感到非常自豪,因为它知道它完美无缺。
所以我建议你扔掉锁,你可能还想考虑信号量,一种更原始(也许更通用)的锁类型。
答案 7 :(得分:0)
你可以用bool来逃避它,但是如果被改变的静态物体具有任何复杂性的类型,那么将会发生可怕的事情。我的建议 - 如果你打算从多个线程写入,总是使用同步对象,否则你迟早会被咬伤。
答案 8 :(得分:0)
这不是一个好主意。有许多变量会影响不同线程的时序。
如果没有某种锁定,您将无法保证拥有正确的最后状态。
可能无法将两个状态更新无序写入数据库。
只要锁定代码设计得当,死锁就不应该像这样简单的过程出现问题。
答案 9 :(得分:0)
正如其他人所指出的,这通常是一个非常糟糕的主意(有一些警告)。
仅仅因为您在测试时没有在您的特定计算机上发现问题,并不能证明该算法正常运行。对于并发应用程序尤其如此。例如,当您切换到具有不同内核数量的计算机时,交错可能会发生显着变化。
警告:如果你的所有制定者都在进行原子写作,如果你不关心它们的时间,那么你可能没问题。
根据你所说的,我认为你可能只有一个在设置者中设置的脏标志。单独的数据库写入线程会经常轮询脏标志并将更新发送到数据库。如果某些项目需要额外的原子性,他们的setter将需要锁定互斥锁。数据库写入线程必须始终锁定互斥锁。