11

我一直在阅读 Joe Duffy 关于并发编程的书。我有一个关于无锁线程的学术问题。

第一:我知道无锁线程充满危险(如果你不相信我,请阅读本书中关于内存模型的部分)

不过,我有一个问题:假设我有一个带有 int 属性的类。

该属性引用的值会被多个线程非常频繁地读取

值会发生变化是极其罕见的,当它发生变化时,将是一个单独的线程来改变它。

如果在另一个使用它的操作正在进行时它确实发生了变化,那么没有人会失去手指(任何人使用它的第一件事就是将它复制到局部变量)

我可以使用锁(或 readerwriterlockslim 来保持读取并发)。我可以将变量标记为 volatile (很多例子都是这样做的)

然而,即使是 volatile 也会对性能造成影响。

如果我在 VolatileWrite 更改时使用它,并让读取访问正常。像这样的东西:

public class MyClass
{
  private int _TheProperty;
  internal int TheProperty
  {
    get { return _TheProperty; }
    set { System.Threading.Thread.VolatileWrite(ref _TheProperty, value); }
  }
}

我不认为我会在现实生活中尝试这个,但我对答案很好奇(最重要的是,作为我是否理解我一直在阅读的内存模型内容的检查点)。

4

7 回答 7

6

Marking a variable as "volatile" has two effects.

1) Reads and writes have acquire and release semantics, so that reads and writes of other memory locations will not "move forwards and backwards in time" with respect to reads and writes of this memory location. (This is a simplification, but you take my point.)

2) The code generated by the jitter will not "cache" a value that seems to logically be unchanging.

Whether the former point is relevant in your scenario, I don't know; you've only described one memory location. Whether or not it is important that you have only volatile writes but not volatile reads is something that is up to you to decide.

But it seems to me that the latter point is quite relevant. If you have a spin lock on a non-volatile variable:

while(this.prop == 0) {}

the jitter is within its rights to generate this code as though you'd written

if (this.prop == 0) { while (true) {} }

Whether it actually does so or not, I don't know, but it has the right to. If what you want is for the code to actually re-check the property on each go round the loop, marking it as volatile is the right way to go.

于 2010-01-29T00:37:15.743 回答
4

The question is whether the reading thread will ever see the change. It's not just a matter of whether it sees it immediately.

Frankly I've given up on trying to understand volatility - I know it doesn't mean quite what I thought it used to... but I also know that with no kind of memory barrier on the reading thread, you could be reading the same old data forever.

于 2010-01-28T21:21:45.690 回答
2

The "performance hit" of volatile is because the compiler now generates code to actually check the value instead of optimizing that away - in other words, you'll have to take that performance hit regardless of what you do.

于 2010-01-28T21:22:57.477 回答
2

At the CPU level, yes every processor will eventually see the change to the memory address. Even without locks or memory barriers. Locks and barriers would just ensure that it all happened in a relative ordering (w.r.t other instructions) such that it appeared correct to your program.

The problem isn't cache-coherency (I hope Joe Duffy's book doesn't make that mistake). The caches stay conherent - it is just that this takes time, and the processors don't bother to wait for that to happen - unless you enforce it. So instead, the processor moves on to the next instruction, which may or may not end up happening before the previous one (because each memory read/write make take a different amount of time. Ironically because of the time for the processors to agree on coherency, etc. - this causes some cachelines to be conherent faster than others (ie depending on whether the line was Modified, Exclusive, Shared, or Invalid it takes more or less work to get into the necessary state).)

So a read may appear old or from an out of date cache, but really it just happened earlier than expected (typically because of look-ahead and branch prediction). When it really was read, the cache was coherent, it has just changed since then. So the value wasn't old when you read it, but it is now when you need it. You just read it too soon. :-(

Or equivalently, it was written later than the logic of your code thought it would be written.

Or both.

Anyhow, if this was C/C++, even without locks/barriers, you would eventually get the updated values. (within a few hundred cycles typically, as memory takes about that long). In C/C++ you could use volatile (the weak non-thread volatile) to ensure that the value wasn't read from a register. (Now there's a non-coherent cache! ie the registers)

In C# I don't know enough about CLR to know how long a value could stay in a register, nor how to ensure you get a real re-read from memory. You've lost the 'weak' volatile.

I would suspect as long as the variable access doesn't completely get compiled away, you will eventually run out of registers (x86 doesn't have many to start with) and get your re-read.

But no guarantees that I see. If you could limit your volatile-read to a particular point in your code that was often, but not too often (ie start of next task in a while(things_to_do) loop) then that might be the best you can do.

于 2010-01-29T02:23:23.550 回答
1

This is the pattern I use when the 'last writer wins' pattern is applicable to the situation. I had used the volatile keyword, but after seeing this pattern in a code example from Jeffery Richter, I started using it.

于 2010-01-28T21:18:23.323 回答
1

For normal things (like memory-mapped devices), the cache-coherency protocols going on within/between the CPU/CPUs is there to ensure that different threads sharing that memory get a consistent view of things (i.e., if I change the value of a memory location in one CPU, it will be seen by other CPUs that have the memory in their caches). In this regard volatile will help to ensure that the optimizer doesn't optimize away memory accesses (which are always going through cache anyway) by, say, reading the value cached in a register. The C# documentation seems pretty clear on this. Again, the application programmer doesn't generally have to deal with cache-coherency themselves.

I highly recommend reading the freely available paper "What Every Programmer Should Know About Memory". A lot of magic goes on under the hood that mostly prevents shooting oneself in the foot.

于 2010-01-28T21:49:05.730 回答
0

在 C# 中,int类型是线程安全的。

由于您说只有一个线程对其进行写入,因此您永远不应该争论什么是正确的值,并且只要您正在缓存本地副本,就永远不应该得到脏数据。

You may, however, want to declare it volatile if an OS thread will be doing the update.

Also keep in mind that some operations are not atomic, and can cause problems if you have more than one writer. For example, even though the bool type wont corrupt if you have more than one writer, a statement like this:

a = !a;

is not atomic. If two threads read at the same time, you have a race condition.

于 2010-01-28T21:18:04.640 回答