7

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

请注意,这与对底层对象本身的更改不同,这对于引用类型(类)当然是可能的,但可以通过该类中的适当锁定来轻松防止。但是,我们不能保护对外部客户端变量空间本身的更改!我们必须在方法顶部制作自己的参数副本并覆盖底部的参数(例如),但考虑到处理不安全的参考。

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

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

Edit for conclusion:
Having confirmed it with a multi-threaded code example (Thanks Marc!) and thinking about it further, it makes sense that it is indeed the not-automatically-threadsafe behavior which I was worred about. One point of "ref" is to pass large structs by reference rather than copy them. Another reason is that you might want to set up a long-term monitoring of a variable and need to pass a reference to it which will see changes to the variable (eg. changing between null and a live object), which an automatic copy-in/copy-out would not allow for.

So, to make our Register method robust against client insanity, we could implement it like:

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.
}

They'd still have their own threading issues as far as what they end up getting, but at least their insanity wouldn't break our own sanity-checking process and possibly slip a bad state past our checks.

4

2 回答 2

7

When you use ref, you are passing the address of the caller's field/variable. Therefore yes: two threads can compete over the field/variable - but only if they both are talking to that field/variable. If they have a different field/variable to the same instance, then things are sane (assuming it is immutable).

For example; in the code below, Register does see the changes that Mutate makes to the variable (each object instance is effectively immutable).

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);
        }
    }
}
于 2009-03-24T23:27:34.507 回答
7

No, it's not "copy in, copy out". Instead, the variable itself is effectively passed in. Not the value, but the variable itself. Changes made during the method are visible to anything else looking at the same variable.

You can see this without any threading being involved:

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

The output of this is First, Second, Second - the call to ShowFoo() within ChangeValue shows that the value of foo has already changed, which is exactly the situation you're concerned about.

The solution

Make Definition immutable if it wasn't before, and change your method signature to:

public static Definition Register(Definition newDefinition)

Then the caller can replace their variable if they want to, but your cache can't be polluted by a sly other thread. The caller would do something like:

myDefinition = Register(myDefinition);
于 2009-03-24T23:29:43.910 回答