如何在此模型中使用 volatile 关键字?
我有一个数据类,其中包含大量数据(电视节目表数据)。 数据从一侧查询并从另一侧定期更新。 有两个线程:第一个线程根据请求查询数据,第二个线程定期更新数据。 为了防止锁定,我使用数据类的两个实例(副本):实时实例和备份实例。 最初,两个实例都填充了相同的数据。第一个线程仅从活动实例中读取。 第二个线程定期更新两个实例,如下所示:
- 更新备份实例。
- 交换备份和活动实例(即备份实例变为活动实例)。
- 更新备份实例。
- 备份实例和实时实例现在都是最新的。
我的问题是:这里应该如何使用 volatile 关键字?
public class data
{
// Lots of fields here.
// Should these fields also be declared volatile?
}
我已经将参考文献设为易失性:
public volatile data live
public volatile data backup
I have a data class with lots of data in it (TV schedule data).
The data is queried from one side and periodically updated from the other side.
There are two threads: the first thread queries the data on request and the second thread updates the data on regular intervals.
To prevent locking, I use two instances (copies) of the data class: the live instance and the backup instance.
Initially, both instances are filled with the same data. The first thread only reads from the live instance.
The second thread periodically updates both instances as follows:
- Update the backup instance.
- Swap the backup and live instance (i.e. the backup instance becomes the live instance).
- Update the backup instance.
- Both backup instance and live instance are now up-to-date.
My questions is: how should I use the volatile keyword here?
public class data
{
// Lots of fields here.
// Should these fields also be declared volatile?
}
I have already made the references volatile:
public volatile data live
public volatile data backup
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
我认为用
易失性
标记东西不会得到你想要的效果。考虑这段代码。在上面的示例中,如果第二个线程在第一个线程进入的时间之间交换了
live
和backup
引用,则可以将false
写入控制台。if
并调用Console.WriteLine
。如果您不关心这个问题,那么您真正需要做的就是将
live
变量标记为易失性
。您无需将data
中的各个字段标记为易失性
。原因是因为易失性读取会创建获取栅栏内存屏障,易失性写入会创建释放栅栏内存屏障。这意味着当线程 2 交换引用时,对data
的各个字段的所有写入都必须首先提交,并且当线程 1 想要读取live
的各个字段时实例中,必须首先从主内存重新获取live
变量。您无需将backup
变量标记为易失性
,因为线程 1 从未使用过它。Joe Albahari 电子书中的高级线程部分 详细介绍了
易失性
的语义,并且应该解释为什么您只需要来标记您的实时
参考。I do not think you are going to get the effect you want by marking things with
volatile
. Consider this code.In the example above
false
could be written to the console if the second thread swapped thelive
andbackup
references between the time the first thread entered theif
and calledConsole.WriteLine
.If that problem does not concern you then all you really need to do is mark the
live
variable asvolatile
. You do not need to mark the individual fields indata
asvolatile
. The reason is because volatile reads create acquire fence memory barriers and volatile writes create release fence memory barriers. What that means is that when thread 2 swaps the references then all writes to the individual fields ofdata
must commit first and when thread 1 wants to read the individual fields of thelive
instance thelive
variable must be reacquired from main memory first. You do not need to mark thebackup
variable asvolatile
because it is never used by thread 1.The advanced threading section in Joe Albahari's ebook goes into a great deal of detail on the semantics of
volatile
and should explain why you only need to mark yourlive
reference as such.如果您打算在
lock
之外修改字段,或者不使用Interlocked
,则应将字段声明为易失性
。这是深入解释易失性
的最佳文章:http://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/fields should be declared
volatile
if you plan to modify them outsidelock
s, or withoutInterlocked
. Here is the best article that explainvolatile
deeply: http://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/说实话,我只会锁定它。正确性更容易检查,并且无需备份。
根据您的计划,这些字段也必须是不稳定的。考虑一下这种情况:
为了简单起见,这里我们只有一个公共字段,这同样适用于更现实的结构。 (顺便说一句,类名的大写是 C# 中更常见的约定)。
现在考虑线程A所看到的
live.SimpleInt
。因为live
可以被缓存,所以我们需要将其设置为易失性的。但是,请考虑一下,当对象与backup
交换,然后交换回live
时,live
将具有与其相同的内存位置之前做过(除非 GC 已移动它)。因此,live.SimpleInt
将具有与之前相同的内存位置,因此,如果它不是易失性的,则线程 A 可能正在使用live.SimpleInt
的缓存版本。但是,如果您创建了一个新的 Data 对象,而不是进行换入换出,则 live.SimpleInt 的新值将不会出现在线程的缓存中,并且它可以安全地保持非易失性。
考虑字段的字段也必须是易失性的这一点也很重要。
实际上,现在您只需要一个存储的 Data 对象。新的对象将被创建为仅由一个线程引用的对象(因此它不会被另一个线程损坏或对其造成损坏),并且其创建将基于从
live
读取的值,即同样安全,因为另一个线程只是读取(除非某些记忆技术,这意味着“读取”实际上是在幕后写入,读取不会损害其他读取,尽管它们可能会受到写入的损害)在仅对单个线程可见的情况下进行更改线程,因此只有最后的写入才需要担心同步,这确实应该安全,只有 volatile 或用于保护的 MemoryBarrier ,因为分配引用是原子的,并且因为您不再关心旧值。To be honest, I would just lock on it. The correctness is so much easier to check, and the need for the backup is removed.
With your plan here, the fields would also have to be volatile. Consider the case otherwise:
Here we have just a single public field for simplicity, the same applies to more realistic structures. (Incidentally, captials for class names is a more common convention in C#).
Now consider
live.SimpleInt
as seen by thread A. Becauselive
could be cached, we need to have it as volatile. However, consider that when the object is swapped withbackup
, and then swapped back tolive
, thenlive
will have the same memory location as it did before (unless the GC has moved it). Thereforelive.SimpleInt
will have the same memory location as it did before, and therefore if it was not volatile, thread A may be using a cached version oflive.SimpleInt
.However, if you created a new Data object, rather than swapping in and out, then the new value of
live.SimpleInt
will not be in the thread's cache, and it could be safely non-volatile.It's also important to consider that the fields of the fields will have to be volatile too.
Indeed now you need just one stored Data object. The new one will be created as an object referenced only by one thread (hence it cannot be damaged by or do damage to another thread), and its creation will be based on values read from
live
, which is also safe as the other thread is only reading (barring some memoisation techniques that mean that "reads" are really writes behind the scenes, reads can't harm other reads, though they can be harmed by writes) altered while visible to just a single thread, and hence only the final write requires any concern about synchronisation which should indeed be safe with only volatile or a MemoryBarrier used for protection, since assigning a reference is atomic, and since you don't care about the old value anymore.