从多个线程写入静态数据实际上是不是安全
Posted
技术标签:
【中文标题】从多个线程写入静态数据实际上是不是安全【英文标题】:Is it practically safe to write static data from multiple threads从多个线程写入静态数据实际上是否安全 【发布时间】:2009-02-26 12:05:27 【问题描述】:我有一些想要从数据库中缓存的状态数据。几个线程中的任何一个都可以修改状态数据。数据修改后将写入数据库。数据库写入将始终由底层数据库访问层连续完成,该层在不同进程中对数据库操作进行排队,因此我不关心这些操作的竞争条件。
只修改多个线程的静态数据有问题吗?理论上修改是有可能实现为读取、修改、写入,但实际上我无法想象会是这样。
我的数据处理类将如下所示:
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;
;
我不想使用锁,因为应用程序的这一部分已经有几个锁,添加更多锁会增加死锁的可能性。
数据库更新中2个线程之间是否有重叠无关紧要,例如
thread 1 thread 2 activity started in db
SetActivityStarted(true) SetActivityStarted(false)
m_activityStated = true
m_activityStarted = false
WriteToDB() false
WriteToDB() false
所以 db 显示最近由 m_... = x 行设置的状态。没关系。
这是一种合理的使用方法还是有更好的方法?
[编辑说我只关心最后的状态——顺序不重要]
【问题讨论】:
【参考方案1】:不,这不安全。
生成的写入 m_activityStarted 和其他代码的代码可能是原子的,但这不是保证。此外,在您的设置器中,您会做两件事:设置布尔值并拨打电话。这绝对不是原子的。
你最好在这里使用某种锁进行同步。
例如,一个线程可能会调用第一个函数,在该线程进入“WriteDB()”之前,另一个线程可能会调用另一个函数并进入 WriteDB(),而不是第一个进入那里。那么,状态可能是以错误的顺序写入数据库中的。
如果您担心死锁,那么您应该修改整个并发策略。
【讨论】:
我没有说清楚,但是如果一个线程的状态更新是从另一个线程写入数据库的,这并不重要。在任何时候,所有数据库都可以显示最后的已知状态。如果是这种情况,我还需要锁吗? 如果你要序列化所有对 WriteToDB() 的调用,为什么不首先序列化所有对 setter 的访问呢? 这是多进程系统的一部分,数据库写入的序列化由不同的进程完成。【参考方案2】:在多 CPU 机器上,无法保证在不发出同步指令的情况下,运行在不同 CPU 上的线程会以正确的顺序看到内存写入。仅当您发出同步命令时,例如互斥锁或解锁,保证每个线程的数据视图是一致的。
为了安全起见,如果你想在你的线程之间共享状态,你需要使用某种形式的同步。
【讨论】:
【参考方案3】:您永远不知道在较低级别是如何实现的。尤其是当您开始处理多核、各种缓存级别、流水线执行等时。至少不是没有大量工作,而且实现经常更改!
如果不互斥,最终你会后悔的!
我最喜欢的例子涉及整数。这一特定系统在两次写入中写入其整数值。例如。不是原子的。自然,当线程在这两次写入之间被中断时,你会从一个 set() 调用中获得高字节,而从另一个调用中获得低字节 ()。一个经典的错误。但远不是可能发生的最坏情况。
互斥是微不足道的。
您提到:我不想使用锁,因为应用程序的这部分已经有几个锁,添加更多会增加死锁的可能性。强>
只要你遵循黄金法则就可以了:
不要混用互斥锁命令。例如。 A.lock();B.lock() 在一个地方和 B.lock();A.lock();在另一个。使用一个或另一个订单! 锁定尽可能短的时间。 不要试图将一个互斥锁用于多种用途。使用多个互斥锁。 尽可能使用递归或错误检查互斥锁。 使用 RAII 或宏来确保解锁。例如:
#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;
;
【讨论】:
“不要混合互斥锁命令”——这就是我所关心的。我将不得不分析所有的锁以及如何使用它们。 DDJ 有一种有趣的方式来强制执行互斥顺序,我将对此进行调查:ddj.com/cpp/212201754 看看我的例子:没有办法混合互斥锁命令。它自然会从“锁定尽可能短的时间”中消失。【参考方案4】:我不是 c++ 人,但我认为如果你没有某种同步,写它会安全..
【讨论】:
【参考方案5】:您似乎有两个问题。
#1 是你的布尔赋值不一定是原子的,即使它是你代码中的一个调用。所以,在幕后,你可能会有不一致的状态。如果您的线程/并发库支持,您可以考虑使用 atomic_set()。
#2 是阅读和写作之间的同步。从您的代码示例中,看起来您的 WriteToDB() 函数写出了所有 4 个变量的状态。 WriteToDB 序列化在哪里?您是否会遇到这样的情况:thread1 启动 WriteToDB(),它读取 m_activityStarted 但没有完成将其写入数据库,然后被 thread2 抢占,它一直写入 m_activityStarted。然后,thread1 恢复,并完成将其不一致的状态写入数据库。至少,我认为在执行数据库更新所需的读取访问权限时,您应该拥有对锁定的静态变量的写入访问权限。
【讨论】:
【参考方案6】:理论上,修改有可能实现为读取、修改、写入,但实际上我无法想象会是这样。
通常是这样,除非您设置了某种事务性内存。变量通常存储在 RAM 中,但在硬件寄存器中进行修改,因此读取不仅仅是为了踢球。读取对于将值从 RAM 复制到可以修改(甚至与另一个值比较)的位置是必要的。当数据在硬件寄存器中被修改时,陈旧的值仍然在 RAM 中,以防其他人想要将它复制到另一个硬件寄存器中。当修改后的数据被写回 RAM 时,其他人可能正在将其复制到硬件寄存器中。
在 C++ 中,整数保证至少占用一个字节的空间。这意味着它们实际上可能具有非真或假的值,例如由于读取发生在写入过程中的竞争条件。
在.Net 上有一些静态数据和静态方法的自动同步。标准 C++ 中没有这样的保证。
如果您只查看整数、布尔值和(我认为)长整数,您可以选择原子读/写和加法/减法。 C++0x 有something。 Intel TBB 也是如此。我相信大多数操作系统也有实现这一点所需的钩子。
【讨论】:
优点:我没有考虑过相同内存数据的多个寄存器。 写完这篇文章后,我开始怀疑读取是否发生,因为您事先知道修改将是写入常量数据。所以我得到了 g++ 的汇编代码输出,读取发生在函数被调用之前(标志被设置为 this 指针的一部分)。【参考方案7】:虽然您可能害怕死锁,但我相信您会为您的代码完美运行而感到无比自豪。 所以我建议你把锁扔进去,你可能还想考虑信号量,一种更原始(也许更通用)的锁类型。
【讨论】:
看了几篇关于DDJ的文章后,我对尝试减少锁的依赖感兴趣的主要原因:ddj.com/cpp/184401930,ddj.com/cpp/204801163?pgno=2 是的,我总是说简单是关键。有时无锁程序是最简单的。但是如果它是一个复杂的程序,那么锁可能比找到绕过每个锁的方法更简单。这是一个判断电话,祝你好运。【参考方案8】:你可以用 bools 侥幸逃脱,但是如果被改变的静态对象是非常复杂的类型,就会发生可怕的事情。我的建议 - 如果您要从多个线程编写,请始终使用同步对象,否则您迟早会被咬。
【讨论】:
是什么让布尔值如此特别?数据就是数据。如果它不是原子操作,那么类型有多“复杂”都没有关系。【参考方案9】:这不是一个好主意。有很多变量会影响不同线程的时序。
如果没有某种锁,您将无法保证拥有正确的最后状态。
有可能两个状态更新被乱序写入数据库。
只要锁定代码设计得当,这样的简单过程就不会出现死锁问题。
【讨论】:
应用程序的这一部分很简单,但它只是其中很小的一部分,可能会从几个不同的线程调用,每个线程还与至少 2 个其他锁交互,因此我的死锁担心。 我同意你不应该使用不需要的锁。但是,您不应该仅仅为了减少锁而尝试消除它们。如果需要,应该使用它们。评估需求需要有关您的系统的更详细信息。【参考方案10】:正如其他人所指出的,这通常是一个非常糟糕的主意(有一些警告)。
仅仅因为您在测试特定机器时没有发现问题,并不能证明该算法是正确的。对于并发应用程序尤其如此。例如,当您切换到具有不同内核数量的机器时,交错可能会发生巨大变化。
警告:如果您所有的 setter 都在进行原子写入,并且您不关心它们的时间安排,那么您可能会没事。
根据您所说的,我认为您可以在设置器中设置一个脏标志。一个单独的数据库写入线程会经常轮询脏标志并将更新发送到数据库。如果某些项目需要额外的原子性,则它们的设置器需要锁定互斥锁。数据库写入线程必须始终锁定互斥体。
【讨论】:
即使是一个脏标志,您也可能需要某种形式的同步。 如果 (a) 您不关心状态更改在发送到数据库之前是否会延迟一个轮询周期,并且 (b) 所有状态更改都是原子的,那么您不需要同步。以上是关于从多个线程写入静态数据实际上是不是安全的主要内容,如果未能解决你的问题,请参考以下文章
java 局部静态变量在多线程环境下是不是有线程安全问题??