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

时间:2009-03-24 23:24:33

标签: c# .net parameters thread-safety

编辑介绍:
我们知道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其他线程不会修改我们正在处理的引用(因为对象的引用被复制并在我们返回时将被复制出来?),或者其他线程可以替换我们中的对象我们执行的中间(如果我们引用一个指向原始存储位置的指针???),从而打破我们的理智检查?

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

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

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

编辑结论:
通过多线程代码示例(感谢Marc!)确认并进一步思考它之后,我确实认为它确实是 not-automatic-threadsafe 行为。 “ref”的一点是通过引用传递大结构而不是复制它们。另一个原因是您可能想要设置对变量的长期监控,并且需要传递对它的引用,这将看到变量的变化(例如,在null和活动对象之间切换) ),自动拷入/拷出不允许。

因此,为了使我们的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.
}

至于他们最终得到的问题,他们仍然有自己的线程问题,但至少他们的疯狂不会破坏我们自己的理智检查过程,并且可能会使我们的检查失败。

2 个答案:

答案 0 :(得分:7)

使用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);
        }
    }
}

答案 1 :(得分:6)

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

您可以在不涉及任何线程的情况下看到这一点:

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();
    }
}

这是第一,第二,第二的输出 - 在ShowFoo() 中对ChangeValue 的调用表明foo的值已经改变,正是你所关注的情况。

解决方案

如果以前不是Definition,请将其设置为不可变,并将方法签名更改为:

public static Definition Register(Definition newDefinition)

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

myDefinition = Register(myDefinition);