【发布时间】:2019-07-18 10:22:18
【问题描述】:
这个问题具体是关于何时可以安全地调用 ref 的成员,该成员预计只能在监视器锁定下读取或写入。
在下面的示例中,仅希望在锁定时检查和设置类字段。私有实现只访问锁下的值,但是,它们实际上将成员作为ref,然后锁定锁并对给定的ref 执行工作。公共 Get 和 TrySet 方法实际上通过将请求的成员字段传递给 ref 来调用私有方法,并且它们不会在调用站点锁定锁 --- 问题是这实际上是安全的。
它应该是安全的,因为:虽然公共方法通过ref 引用成员字段而没有锁;在这个呼叫站点上,ref 将只是指针;并且直到在所需锁下的私有方法中才会取消引用实际的成员值。
以下情况不安全:
- 如果传递
ref的公共方法实际上读取了该值。如果是这样,那么私有方法将接收参数中的值,并作用于该值而不是当前字段值;这可能已被另一个线程更改(然后逻辑被破坏:然后该字段不会在与之比较的锁下被读取,并且具有现在陈旧的值)。 - 或者如果指针在公共调用站点和私有方法之间移动;但我很确定 CLR 确保不可能发生这样的事情。
请注意,我知道返回的对象 ITSELF 仍然不安全:我的问题仅在于 REF 内存的实际取消引用。
我已阅读 5.1.5 下的规范;并查看了生成的 IL 代码;我相信它是安全的。
这里是示例:公共方法线程安全吗?
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
namespace Test
{
public class TestRef
{
private readonly object syncLock = new object();
private ulong eventCounter;
private object objectValue;
private long longValue = 1L;
private T getValue<T>(ref T member)
{
lock (syncLock) {
return member;
}
}
private bool trySetValue<T>(ref T member, T value)
{
lock (syncLock) {
if (object.Equals(member, value))
return false;
member = value;
++eventCounter;
}
SomeValueChanged?.Invoke(this, EventArgs.Empty);
return true;
}
public object GetObjectValue()
=> getValue(ref objectValue);
public long GetLongValue()
=> getValue(ref longValue);
public bool TrySetObjectValue(object value)
=> trySetValue(ref objectValue, value);
public bool TrySetLongValue(long value)
=> trySetValue(ref longValue, value);
public ulong EventCounter
{
get {
lock (syncLock) {
return eventCounter;
}
}
}
public event EventHandler SomeValueChanged;
}
public static class Program
{
public static async Task<bool> ExampleTest(int taskCount)
{
TestRef testRef = new TestRef(); // longValue is 1L
List<Task> tasks = new List<Task>(taskCount);
for (int i = 0; i < taskCount; ++i) {
tasks.Add(Task.Run(Callback)); // All Tasks will try set 2L
}
await Task.WhenAll(tasks);
bool success = testRef.EventCounter == 1UL;
Console.WriteLine(
$@"Ran {taskCount} Tasks: Raised event count: {testRef.EventCounter} (success: {success}).");
return success;
async Task Callback()
{
await Task.Delay(taskCount); // Cheaply try to pile Tasks on top of each other
testRef.TrySetLongValue(2L);
}
// If not safe, then it is possible for MORE THAN ONE
// Task to raise the event: i.e. two may
// begin and the public method could read the
// current value outside the lock, and both
// would read 1L; and then BOTH would compare
// the argument in the private method AS 1L
// and both would then set the value and raise the event.
// If safe, then only the first Task in would change
// the value
}
public static void Main(string[] args)
{
int defaultTaskCount = Environment.ProcessorCount * 500;
Console.WriteLine($@"Hello World.");
Console.WriteLine(
$@"Specify how many parallel Tasks to run against {Environment.ProcessorCount} instances (each):");
Console.WriteLine(
$@"--- The default will be {
defaultTaskCount
} Tasks against each instance [just type enter for the default]:");
if (!int.TryParse(Console.ReadLine(), NumberStyles.Any, CultureInfo.CurrentCulture, out int taskCount))
taskCount = defaultTaskCount;
Console.WriteLine($@"Will Run {taskCount} Tasks against {Environment.ProcessorCount} instances (each) ...");
List<Task<bool>> tasks = new List<Task<bool>>(Environment.ProcessorCount);
for (int i = 0; i < Environment.ProcessorCount; ++i) {
tasks.Add(Program.ExampleTest(taskCount));
}
Task.WhenAll(tasks)
.Wait();
bool success = tasks.All(task => task.Result);
Console.WriteLine($@"Success = {success}.");
Console.WriteLine($@"Type a key to exit ...");
Console.ReadKey();
}
}
}
public方法不加锁,通过引用传递成员;并且私有方法在读写之前锁定锁。
我假设它是安全的:传递的引用只是调用站点的指针;并且私有方法体实际上取消引用指针;那里的锁下面。
在生成的 IL 代码中,它看起来是安全的:只有指针被传递,并且直到在私有方法中的锁定下才会取消引用。
规范确实说“在函数成员或匿名函数中,引用参数被认为是最初分配的。” --- 但它说“考虑最初分配”......这可能会增加更多问题,但让我认为指针在使用之前不会受到尊重,因此上述内容总是安全的。
【问题讨论】:
-
所有“考虑赋值”的意思是c#的规则要求refd变量在调用之前要赋值,这样被调用者才能知道它有值。它只是一个编译时错误检查器。
-
如果你有一个Test对象的集合,枚举也不被认为是线程安全的。在枚举时,有人可能会更改集合。最好使用 ICollection.SyncRoot 或 Array.SyncRoot。
-
@EricLippert 我理解:我认为这意味着随时获取
ref指针总是安全的,并且您必须确保在锁定下取消引用它...所以我确实认为一切都很安全。我不能让测试程序失败... -
我认为你的问题的答案是
ref在每次使用时都会被取消引用,而不仅仅是一次。 -
private T getValue<T>(ref T member) { lock (syncLock) { return member; } }为什么我需要lock?