.NET是ref参数是线程安全的,否则极易不安全的多线程访问?线程、不安全、极易、多线程

2023-09-03 01:01:23 作者:俗雅无味

修改的介绍: 我们知道,在C#中的ref参数传递一个的引用的一个变量,允许外部变量本身被一个名为方法中改变。但是处理很像C指针(读原始变量的当前内容与每一个访问该参数和改变与每次修改于该参数的原始变量)的参考,或可被调用的方法依靠对一个一致的参考呼叫的持续时间?前者带来了一些线程安全问题。特别是:

Edit for intro: We know that a ref parameter in C# passes a reference to a variable, allowing the external variable itself to be changed within a called method. But is the reference handled much like a C pointer (reading the current contents of the original variable with every access to that parameter and changing the original variable with every modification to the parameter), or can the called method rely on a consistent reference for the duration of the call? The former brings up some thread safety issues. In particular:

我已经写在C#中的静态方法,通过引用传递对象:

I've written a static method in C# which passes an object by reference:

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

调用者提供了一个完成,但还未注册定义对象,经过一番一致性检查,我们注册,他们提供的定义。但是,如果已经存在一个具有相同的密钥的定义,就不能注册新的,而不是他们引用更新为官定义该键。

The caller provides a completed but not-yet-registered Definition object, and after some consistency checking we "register" the definition they provided. However, if there is already a definition with the same key, it can't register the new one and instead their reference is updated to the "official" Definition for that key.

我们希望这是严格的线程安全的,但病理场景浮现在脑海。假设客户端(使用我们的库)的股票在非线程安全的方式引用,如使用静态成员,而不是一个局部变量:

We want this to be rigorously thread-safe, but a pathological scenario comes to mind. Suppose the client (using our library) shares the reference in an non-thread-safe manner, such as using a static member rather than a local variable:

private static Definition riskyReference = null;

如果一个线程组 riskyReference =新的定义(键1); ,填写的定义,并调用 Definition.Register(裁判riskyReference); ,而另一个线程还决定设置 riskyReference =新的定义(键2); ,是我们保证在我们注册方法 newDefinition 引用我们正在处理将不会被修改对我们被其他线程(因为引用的对象被复制,并且将被复制出来,当我们回来吗?) ,或可以在其他线程替换对象上我们在执行过程中(如果我们引用一个指针到原来的存储位置???),从而打破我们完整性检查?

If one thread sets riskyReference = new Definition("key 1");, fills out the definition, and calls our Definition.Register(ref riskyReference); while another thread also decides to set riskyReference = new Definition("key 2");, are we guaranteed that in our Register method the newDefinition reference we're handling will not be modified on us by other threads (because the reference to the object was copied in and will be copied out when we return?), or can that other thread replace the object on us in the middle of our execution (if we're referencing a pointer to the original storage location???) and thus break our sanity-checking?

请注意,这是从改变底层对象本身,这当然也是可能的参考类型(类)的不同,但可以由类内适当的锁定容易地防范。我们不能,不过,后卫更改外部客户端的可变空间本身!我们将不得不作出自己的参数拷贝在方法上并覆盖参数底部(例如),但似乎更有意义,让编译器为我们做特定的处理一个精神错乱不安全的参考。

Note that this is different from changes to the underlying object itself, which are of course possible for a reference type (class), but can be easily guarded against by appropriate locking within that class. We can't, however, guard changes to an external client's variable space itself! We would have to make our own copy of the parameter at the top of the method and overwrite the parameter at the bottom (for example), but that would seem to make more sense for the compiler to do for us given the insanity of handling an unsafe reference.

所以,我会倾向于认为基准可以被复制并复制出由编译器,以便该方法正在处理一个一致的参考原始对象(直到它改变其自己的参考当它要),而不管的可能发生的事,以对其他线程的原始位置。但是我们很难找到在documenation和ref参数讨论该点一个明确的答案。

So, I'd tend to think that the reference may be copied in and copied out by the compiler so that the method is handling a consistent reference to the original object (until it changes its own reference when it wants to) regardless of what might be happening to the original location on other threads. But we're having trouble finding a definitive answer on that point in documenation and discussion of ref parameters.

任何人都可以缓解我的关心最终引文?

Can anyone assuage my concern with a definitive citation?

修改的结论是: 经证实,它具有多线程code例子(感谢马克!),并考虑进一步,这是有道理的,这的确是的未自动线程安全的行为,我是worred约。一个点裁判是按引用传递大的结构,而不是复制它们。另一个原因是,你可能的需要的设立一个长期监测变量,并需要通过对它的引用,会看到改变的变量(如空和现场物体之间变化),其中自动拷入/拷出不会允许。

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.

推荐答案

在使用 REF ,要传递调用者的字段/变量的地址。因此是:两个线程可争用字段/变量 - 但前提是他们都在谈论到字段/变量。如果他们有不同的字段/变量相同的实例,那么事情是理智的(假设它是不可变的)。

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

例如,在code以下,注册 确实看到的变化是突变使得以在变量(每个对象实例实际上是不可变的)。

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