Являются ли параметры .NET ref поточно-ориентированными или уязвимыми для небезопасного многопоточного доступа? - PullRequest
7 голосов
/ 25 марта 2009

Редактировать для вступления:
Мы знаем, что параметр ref в C # передает ссылку в переменную, что позволяет изменять внешнюю переменную внутри вызываемого метода. Но обрабатывается ли ссылка так же, как указатель C (чтение текущего содержимого исходной переменной при каждом доступе к этому параметру и изменение исходной переменной при каждой модификации параметра), или вызываемый метод может полагаться на непротиворечивую ссылку для длительность звонка? Первый поднимает некоторые проблемы безопасности потоков. В частности:

Я написал статический метод в C #, который передает объект по ссылке:

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

Вызывающий объект предоставляет завершенный, но еще не зарегистрированный объект Definition, и после некоторой проверки согласованности мы «регистрируем» предоставленное им определение. Однако, если уже существует определение с тем же ключом, оно не может зарегистрировать новое, и вместо этого их ссылка обновляется до «официального» * ​​1012 * для этого ключа.

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

private static Definition riskyReference = null;

Если один поток устанавливает riskyReference = new Definition("key 1");, заполняет определение и вызывает наш Definition.Register(ref riskyReference);, в то время как другой поток также решает установить riskyReference = new Definition("key 2");, мы гарантируем, что в нашем методе Register ссылка newDefinition, которую мы обрабатываем не будут изменены на нас другими потоками (потому что ссылка на объект была скопирована и будет скопирована, когда мы вернемся?), или этот другой поток может заменить объект на нас в середине нашего выполнения (если мы ' ссылаетесь на указатель на исходное место хранения ???) и таким образом нарушаете нашу проверку работоспособности?

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

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

Может кто-нибудь утешить мою озабоченность окончательным цитированием?

Редактировать для вывода:
Подтвердив это на примере многопоточного кода (спасибо Марку!) И подумав об этом, становится понятным, что это действительно поведение не автоматически-поточно-безопасное , о котором я беспокоился. Одна из точек «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.
}

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

Ответы [ 2 ]

7 голосов
/ 25 марта 2009

Когда вы используете 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);
        }
    }
}
6 голосов
/ 25 марта 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);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...