问题
How do variables captured by a closure interact with different threads? In the following example code I would want to declare totalEvents as volatile, but C# does not allow this.
(Yes I know this is bad code, it's just an example)
private void WaitFor10Events()
{
volatile int totalEvents = 0; // error CS0106:
_someEventGenerator.SomeEvent += (s, e) => totalEvents++;
while(totalEvents < 10)
Thread.Sleep(100);
}
EDIT: People seem to be missing the point of my question a bit. I know I can't use volatile on local vars. I also know that the example code code is bad and could be implemented in other ways, hence my "bad code" disclaimer. It was just to illustrate the problem.
Anyway, it would appear that there is no way to force volatile semantics onto captured local variables, so I will implement a different way. Thanks for the answers though, I have learned a couple of useful things anyway. :)
回答1:
Volatile.Write to the rescue:
private void WaitFor10Events()
{
int totalEvents = 0;
_someEventGenerator.SomeEvent += (s, e) => Volatile.Write(ref totalEvents, totalEvents+1);
while(totalEvents < 10)
Thread.Sleep(100);
}
That said, I would still use Interlocked.Incrementfor this particular case..
回答2:
It is not valid to have a local variable marked as volatile. A closure can capture volatile fields, the following is perfectly legal:
volatile int totalEvents = 0;
private void WaitFor10Events()
{
_someEventGenerator.SomeEvent += (s, e) => totalEvents++;
...
}
See here for information about the volatile keyword;
As an aside, you might consider using a reset event (auto, manual), the monitor class (pulse and wait methods) or a countdown event to have a thread sleep until an event is raised, it is far more efficient than sleeping in a loop.
Update
Following on from the edit on the question, a simple way to get thread-safe semantics is to use the Interlocked class. To re-write your example in this fashion (although as stated in other answers there are better ways of writing this example):
private void WaitFor10Events()
{
long totalEvents = 0;
_someEventGenerator.SomeEvent += (s, e) => Interlocked.Increment(ref totalEvents);
while(Interlocked.Read(ref totalEvents) < 10)
{
Thread.Sleep(100);
}
}
回答3:
You can't declare locals volatile. Besides, there are better ways to acheive your goal... Use System.Threading.CountdownEvent instead. It's going to be more efficient than your poll/sleep method.
using(CountdownEvent cde = new CountdownEvent(10))
{
_someEventGenerator.SomeEvent += (s, e) => cde.Signal();
cde.Wait();
}
回答4:
This won't work if events are fired in parallel. n++, unfortunately, is not an atomic operation in .NET, so you can't just expect multiple threads executing n++ 10 times to actually increase n by 10, it can be increased by less. Here's a little program that proves it (and in the process makes sure closures are properly handled when used in parallel):
class Program
{
static volatile int _outer = 0;
static void Main(string[] args)
{
int inner = 0;
Action act_outer1 = () => _outer++; // Interlocked.Increment(ref _outer);
Action act_inner1 = () => inner++; // Interlocked.Increment(ref inner);
Action<int> combined = (i) => { act_outer1(); act_inner1(); };
Console.WriteLine("None outer={0}, inner={1}", _outer, inner);
Parallel.For(0, 20000000, combined);
Console.WriteLine("Once outer={0}, inner={1}", _outer, inner);
Console.ReadKey();
}
}
The Interlocked.Increment variant works as expected.
回答5:
Local variables captured by closures get "hoisted" out to a different class generated by the compiler, which doesn't loosen the "locals can't be volatile" rule when doing so, even though the local "really" ends up as an instance field. Your best bet is to construct the closure class by hand ("function object") or use some IL manipulation tool, eg. ILDASM.
来源:https://stackoverflow.com/questions/9413080/making-variables-captured-by-a-closure-volatile