.NET ref参数是线程安全的,还是容易受到不安全的multithreading访问?

编辑介绍:
我们知道C#中的ref参数传递对变量的引用 ,允许在被调用的方法中更改外部变量本身。 但是引用的处理方式与C指针非常相似(通过对该参数的每次访问读取原始变量的当前内容,并在对参数进行每次修改时更改原始变量),或者被调用的方法是否依赖于对该参数的一致引用通话时间? 前者提出了一些线程安全问题。 特别是:

我在C#中编写了一个静态方法,它通过引用传递一个对象:

public static void Register(ref Definition newDefinition) { ... } 

调用者提供一个已完成但尚未注册的Definition对象,经过一些一致性检查后,我们“注册”他们提供的定义。 但是,如果已经有一个具有相同密钥的定义,则它无法注册新密钥,而是将其引用更新为该密钥的“官方” Definition

我们希望这是严格的线程安全,但我想到了一个病态场景。 假设客户端(使用我们的库)以非线程安全的方式共享引用,例如使用静态成员而不是局部变量:

 private static Definition riskyReference = null; 

如果一个线程设置riskyReference = new Definition("key 1"); ,填写定义,并调用我们的Definition.Register(ref riskyReference); 而另一个线程也决定设置riskyReference = new Definition("key 2"); ,我们保证在我们的Register方法中,我们正在处理的newDefinition引用不会被其他线程修改(因为对象的引用被复制并在我们返回时将被复制出来吗?),或者可以线程在执行过程中替换我们上面的对象(如果我们引用指向原始存储位置的指针???),从而打破我们的理智检查?

请注意,这与对底层对象本身的更改不同,这对于引用类型(类)当然是可能的,但可以通过该类中的适当锁定来轻松防范。 但是,我们不能保护对外部客户端变量空间本身的更改! 我们必须在方法的顶部创建我们自己的参数副本并覆盖底部的参数(例如),但这对于编译器来说似乎更有意义,因为处理的是精神错乱不安全的参考。

因此,我倾向于认为引用可以被编译器复制并复制出来,以便该方法处理对原始对象的一致引用(直到它在需要时更改自己的引用),而不管是什么发生在其他线程上的原始位置。 但是我们在文档和参考参数的讨论中找不到关于这一点的确定答案。

有人可以通过明确的引用来缓解我的担忧吗?

编辑结论:
在用multithreading代码示例(感谢Marc!)确认它并进一步思考之后,它确实是我自己所担心的非自动线程安全行为。 “ref”的一点是通过引用传递大结构而不是复制它们。 另一个原因是你可能想要设置一个变量的长期监控,并且需要传递对它的引用,这将看到变量的变化(例如,在null和活动对象之间进行更改),这是一个自动复制 – in / copy-out不允许。

因此,为了使我们的Register方法能够抵御客户端疯狂,我们可以像下面这样实现它:

 public static void Register(ref Definition newDefinition) { Definition theDefinition = newDefinition; // Copy in. //... Sanity checks, actual work... //...possibly changing theDefinition to a new Definition instance... newDefinition = theDefinition; // Copy out. } 

就他们最终得到的问题而言,他们仍然有自己的线程问题,但至少他们的精神错乱不会打破我们自己的理智检查过程,并可能使我们的检查失败。

使用ref ,您传递的是调用者字段/变量的地址。 因此是的:两个线程可以在字段/变量上竞争 – 但前提是它们都在与该字段/变量进行通信。 如果他们对同一个实例有不同的字段/变量,那么事情就是理智的(假设它是不可变的)。

例如; 在下面的代码中, Register 确实看到Mutate变量 所做的更改(每个对象实例实际上是不可变的)。

 using System; using System.Threading; class Foo { public string Bar { get; private set; } public Foo(string bar) { Bar = bar; } } static class Program { static Foo foo = new Foo("abc"); static void Main() { new Thread(() => { Register(ref foo); }).Start(); for (int i = 0; i < 20; i++) { Mutate(ref foo); Thread.Sleep(100); } Console.ReadLine(); } static void Mutate(ref Foo obj) { obj = new Foo(obj.Bar + "."); } static void Register(ref Foo obj) { while (obj.Bar.Length < 10) { Console.WriteLine(obj.Bar); Thread.Sleep(100); } } } 

不,它不是“复制,复制”。 相反,变量本身是有效传入的。不是值,而是变量本身。 在该方法期间所做的更改对于查看同一变量的任何其他内容都是可见的。

您可以在不涉及任何线程的情况下看到此信息:

 using System; public class Test { static string foo; static void Main(string[] args) { foo = "First"; ShowFoo(); ChangeValue(ref foo); ShowFoo(); } static void ShowFoo() { Console.WriteLine(foo); } static void ChangeValue(ref string x) { x = "Second"; ShowFoo(); } } 

它的输出是First,Second,Second – 在ShowFoo()调用ShowFoo()表明foo的值已经改变,这正是你所关注的情况。

解决方案

如果之前不是,则使Definition不可变,并将方法签名更改为:

 public static Definition Register(Definition newDefinition) 

然后调用者可以根据需要替换他们的变量,但是你的缓存不会被狡猾的其他线程污染。 调用者会做类似的事情:

 myDefinition = Register(myDefinition);