Являются ли параметры ссылки .NET потокобезопасными или уязвимы для небезопасного многопоточного доступа?

Правка для ознакомления
Мы знаем, что параметр 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 не будет изменена другими потоками (поскольку ссылка на объект была скопирована и будет скопирована, когда мы вернемся?), или этот другой поток может заменить объект на нас в середине нашего выполнения (если мы ссылаемся на указатель на исходное место хранения?? ?) и тем самым нарушить нашу проверку на вменяемость?

Обратите внимание, что это отличается от изменений в самом базовом объекте, которые, конечно, возможны для ссылочного типа (класса), но от которых можно легко защититься соответствующей блокировкой внутри этого класса. Однако мы не можем защитить изменения самого пространства переменных внешнего клиента! Нам пришлось бы сделать свою собственную копию параметра в верхней части метода и перезаписать параметр в нижней части (например), но компилятору кажется, что это имеет больше смысла делать за нас, учитывая безумие обработки небезопасная ссылка.

Итак, я склонен думать, что ссылка может быть скопирована и скопирована компилятором, так что метод обрабатывает непротиворечивую ссылку на исходный объект (пока он не изменит свою собственную ссылку, когда захочет) независимо от того, что может происходит с исходным местоположением в других потоках. Но у нас возникли проблемы с поиском окончательного ответа на этот вопрос в документации и обсуждении параметров ссылки.

Может ли кто-нибудь успокоить мою озабоченность окончательной цитатой?

Редактировать для заключения:
Подтвердив это на примере многопоточного кода (спасибо, Марк!) и подумав об этом, становится понятно, что это действительно не-автоматически- поведение threadsafe, о котором я беспокоился. Одним из пунктов «ref» является передача больших структур по ссылке, а не их копирование. Другая причина заключается в том, что вы можете захотеть настроить долгосрочный мониторинг переменной и передать ссылку на нее, которая будет видеть изменения в переменной (например, изменение между нулевым и активным объектом ), что невозможно при автоматическом копировании.

Итак, чтобы сделать наш метод 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.
}

У них по-прежнему будут свои проблемы с потоками в том, что они в конечном итоге получат, но, по крайней мере, их безумие не нарушит наш собственный процесс проверки работоспособности и, возможно, пропустит плохое состояние мимо наших проверок.


person Rob Parker    schedule 24.03.2009    source источник


Ответы (2)


Когда вы используете 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);
        }
    }
}
person Marc Gravell    schedule 24.03.2009
comment
Хорошо, этот пример действительно показывает, что параметры ref в C# не являются потокобезопасными копиями, поэтому клиент, делающий что-то глупое и патологическое, может изменить объект, который мы обрабатываем в середина нашего метода. Нам нужно будет сделать собственное копирование и копирование, если мы хотим надежно защититься от этого. Спасибо! - person Rob Parker; 25.03.2009

Нет, это не "копировать, копировать". Вместо этого эффективно передается сама переменная. Не значение, а сама переменная. Изменения, сделанные во время метода, видны всем, кто смотрит на ту же переменную.

Вы можете увидеть это без участия потоков:

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() внутри ChangeValue показывает, что значение foo уже изменилось, а это именно та ситуация, которая вас беспокоит.

Решение

Сделайте Definition неизменяемым, если это не было раньше, и измените сигнатуру метода на:

public static Definition Register(Definition newDefinition)

Затем вызывающий может заменить свою переменную, если захочет, но ваш кеш не может быть загрязнен каким-то хитрым другим потоком. Вызывающий сделает что-то вроде:

myDefinition = Register(myDefinition);
person Jon Skeet    schedule 24.03.2009
comment
Мы специально обратились к параметру ref, чтобы избежать требования, чтобы вызывающая сторона заменяла переменную результатами вызова, потому что они могут не сделать этого, и обычно это работает нормально (использует тот же объект), но иногда может их укусить. Но вы правы, что это позволит избежать этой проблемы. - person Rob Parker; 25.03.2009
comment
Они могут использовать локальную переменную вместо правильной общей переменной, что тоже их укусит. Вы не можете гарантировать, что вызывающий код будет разумным, но вы можете гарантировать, что ваш собственный код ведет себя разумно. - person Jon Skeet; 25.03.2009
comment
Да, но мы хотим, чтобы наш API был настолько надежным, насколько это возможно, а не заставлял их облажаться. ;-) Пока они используют локальную переменную для нового определения (как и должны), использование ref облегчает им правильное определение. Меня беспокоит паранойя по поводу того, что патологическое использование ломает нас. - person Rob Parker; 25.03.2009