1

Прочитав так много о том, как это сделать, я совершенно смущен.C# Обновление ссылок на объекты и многопоточность

Итак, вот что я хочу сделать: У меня есть структура данных/объект, который содержит все виды информации. Я надавливаю на структуру данных, как если бы она была неизменной. Всякий раз, когда мне нужно обновлять информацию, я делаю DeepCopy и вношу изменения в нее. Затем я заменю старый и вновь созданный объект.

Теперь я не знаю, как все делается правильно.

Давайте рассмотрим его со стороны читательских/потребительских потоков.

MyObj temp = dataSource; 
var a = temp.a; 
... // many instructions 
var b = temp.b; 
.... 

Как я понимаю, чтение ссылок является атомарным. Поэтому мне не нужна волатильность или блокировка, чтобы назначить текущую ссылку источника данных на temp. Но как насчет коллекции мусора. Насколько я понимаю, GC имеет своего рода счетчик ссылок, чтобы знать, когда освободить память. Поэтому, когда другой поток обновляет dataSource точно в тот момент, когда dataSource назначается temp, делает ли GC увеличение счетчика на правом блоке памяти? Другое дело в оптимизации компилятора/CLR. Я назначаю dataSource temp и использую temp для доступа к элементам данных. Что делает CLR? Это действительно делает копию dataSource или оптимизатор просто использует dataSource для доступа к .a и .b? Предположим, что между temp.a и temp.b указаны инструкции, так что ссылка на temp/dataSource не может храниться в регистре CPU. Так temp.b действительно temp.b или он оптимизирован для dataSource.b, потому что копирование в temp можно оптимизировать. Это особенно важно, если другой поток обновляет dataSource, чтобы указать на новый объект.

Действительно ли мне нужна изменчивость, блокировка, ReadWriterLockSlim, Thread.MemoryBarrier или что-то еще? Для меня важно то, что я хочу убедиться, что temp.a и temp.b обращаются к старой структуре данных, даже когда другой поток обновляет dataSource в другой вновь созданной структуре данных. Я никогда не изменяю данные внутри существующей структуры. Обновления всегда выполняются путем создания копии, обновления данных, а затем обновления ссылки на новую копию документа.


Возможно, еще один вопрос. Если я не использую volatile, сколько времени потребуется, пока все ядра всех процессоров не увидели обновленную ссылку?


Когда дело доходит до неустойчивого, пожалуйста, посмотри здесь: When should the volatile keyword be used in C#?


Я сделал небольшой тест программки:

namespace test1 { 
    public partial class Form1 : Form { 
    public Form1() { InitializeComponent(); } 

    Something sharedObj = new Something(); 

    private void button1_Click(object sender, EventArgs e) { 
     Thread t = new Thread(Do);   // Kick off a new thread 
     t.Start();        // running WriteY() 

     for (int i = 0; i < 1000; i++) { 
     Something reference = sharedObj; 

     int x = reference.x; // sharedObj.x; 
     System.Threading.Thread.Sleep(1); 
     int y = reference.y; // sharedObj.y; 

     if (x != y) { 
      button1.Text = x.ToString() + "/" + y.ToString(); 
      Update(); 
     } 
     } 
    } 

    private void Do() { 
     for (int i = 0; i < 1000000; i++) { 
     Something someNewObject = sharedObj.Clone(); // clone from immutable 
     someNewObject.Do(); 
     sharedObj = someNewObject; // atomic 
     } 
    } 
    } 

    class Something { 
    public Something Clone() { return (Something)MemberwiseClone(); } 
    public void Do() { x++; System.Threading.Thread.Sleep(0); y++; } 
    public int x = 0; 
    public int y = 0; 
    } 
} 

В button1_Click есть для цикла и внутри цикла for я обращаюсь к объекту/объекту один раз с использованием прямого «shareObj» и после использования временно созданной «ссылки». Использовать ссылку достаточно, чтобы убедиться, что «var a» и «var b» инициализируются значениями из одного и того же объекта.

Единственное, что я не понимаю, это почему-то «Что-то ссылка = sharedObj;» не оптимизированы и «int x = reference.x;» не заменяется на "int x = sharedObj.x;"?

Как компилятор, дрожание знает, чтобы не оптимизировать это? Или временно объекты, никогда не оптимизированные на C#?

Но самое главное: мой пример работает по назначению, потому что он правильный или работает как только случайно?

+0

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

+0

С новой информацией я не вижу никаких проблем. Когда вы меняете ссылку на объект и получаете доступ к ней до и после этого изменения/назначения, вы получите два разных результата. Volatile/MemoryBarrier - это совершенно другая вещь. Если вы используете локальную переменную temp или reference или что бы вы ни были хорошими. Но я не вижу причин для SO-сообщения. –

+0

Причина проста. Я боюсь, что мой «экземпляр» ссылочного указателя может быть оптимизирован компилятором, даже если он теперь работает в режиме отладки и выпуска, я не знаю, будет ли он работать в будущем, когда джиттер может улучшиться и оптимизируется более агрессивно. Есть ли какая-либо документация о том, что C# Compiler и Jitter будет оптимизировать и что никогда не будет затронуто? Если у меня есть b = a; с = Ь; что мешает компилятору сделать это c = a и полностью удалить b? Вот почему я спрашиваю, я просто не знаю, как работают оптимизаторы. – bebo

ответ

0

Как я понимаю, чтение ссылок является атомарным.

Исправить. Однако это очень ограниченное свойство. Это означает, что чтение ссылки будет работать; вы никогда не получите бит из половины старой ссылки, смешанной с битами половины новой ссылки, в результате чего ссылка не работает. Если есть одновременное изменение не обещает ничего, получаете ли вы старый или новую ссылку (что бы такое обещание, даже в виду?)

Так что я не нужен какие-либо летучий или блокировок, чтобы присвоить текущее обращение dataSource для temp.

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

А как насчет коллекции мусора. Насколько я понимаю, GC имеет своего рода счетчик ссылок, чтобы знать, когда освободить память.

Неверный. В сборке мусора .NET нет подсчета ссылок.

Если есть статическая ссылка на объект, то он не подлежит возврату.

Если есть активная локальная ссылка на объект, то это не значит, что оно требуется для рекультивации.

Если ссылка на объект в поле объекта, который не подходит для рекультивации, то он также не может быть рекультивирован для рекультивации.

Здесь нет счета. Либо есть активная сильная ссылка, запрещающая рекультивацию, либо ее нет.

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

Другое дело в оптимизации компилятора/CLR. Я назначаю dataSource temp и использую temp для доступа к элементам данных. Что делает CLR? Это действительно делает копию dataSource или оптимизатор просто использует dataSource для доступа к .a и .b?

Это зависит от того, что dataSource и temp являются, насколько являются ли они локальными или нет, и как они используются.

Если dataSource и temp являются локальными, то вполне возможно, что либо компилятор, либо дрожание оптимизировали бы назначение. Если они оба локальны, они оба локальны для одного потока, поэтому это не повлияет на многопоточное использование.

Если dataSource является полем (статическим или экземпляром), то, поскольку temp определенно является локальным в показанном коде (поскольку его инициализировано в показанном фрагменте кода), назначение не может быть оптимизировано.Во-первых, захват локальной копии поля сам по себе является возможной оптимизацией, поскольку быстрее выполнять несколько операций с локальной ссылкой, чем постоянно обращаться к полю или статическому. Там не так много смысла, что компилятор или «оптимизация» дрожания просто замедляют работу.

Рассмотрим, что происходит на самом деле, если вы не использовать temp:

var a = dataSource.a; 
... // many instructions 
var b = dataSource.b; 

dataSource.a Для доступа код должен сначала получить ссылку на dataSource, а затем получить доступ к a. Затем он получает ссылку на dataSource и затем обращается к b.

Оптимизация, не использующая локальный, не имеет смысла, поскольку в любом случае будет неявным локальным.

И есть тот простой факт, что страх у вас есть что-то считается:. После того, как temp = dataSource нет никакого предположения, что temp == dataSource потому что может быть другие потоки изменения dataSource, так что это не действует, чтобы сделать оптимизации основываются на temp == dataSource *

На самом деле оптимизация, о которой вы беспокоитесь, либо не актуальна, либо недействительна и, следовательно, не произойдет.

Существует случай, который может вызвать проблемы. Это почти возможно для потока, работающего на одном ядре, чтобы не видеть изменения до dataSource, сделанных потоком, изменяющимся на другом ядре. Таким образом, если у вас есть:

/* Thread A */ 
dataSource = null; 

/* Some time has passed */ 

/* Thread B */ 
var isNull = dataSource == null; 

Тогда нет никакой гарантии, что только потому, что Поток А закончили установку dataSource к нулю, что Thread B увидит это. Когда-либо.

Модели памяти, используемые в самой .NET и в процессорах .NET, как правило, работают (x86 и x86-64), предотвратили бы это, но с точки зрения возможных будущих оптимизаций это возможно. Вам нужны барьеры памяти, чтобы гарантировать, что публикация Thread A определенно влияет на чтение Thread B. lock и volatile оба способа обеспечить это.

* Для этого не требуется многопоточность, но в некоторых случаях можно доказать, что нет ничейных изменений, которые нарушили бы это предположение. Это не имеет большого значения, потому что многопоточный случай все еще применяется.

+0

Вы заявляете >>> И есть тот простой факт, что у вас есть страх: после temp = dataSource нет предположения, что temp == dataSource, потому что могут быть другие потоки, изменяющие dataSource, поэтому он недействителен, чтобы сделать оптимизацию on temp == dataSource <<< --- Почему бы и нет? Насколько я понимаю, оптимизаторам разрешено делать все возможное до тех пор, пока смысл кода не изменится с точки зрения текущего потока. Оптимизаторы ничего не знают о других потоках. В противном случае барьеры памяти и неустойчивые были излишними. – bebo

+0

Компилятор и джиттер, безусловно, рассматривают другие темы, когда дело доходит до оптимизации. Операции переупорядочения могут игнорировать проблемы других потоков, и барьеры памяти влияют на это, но вид переписывания, о котором вы говорите, просто не имеет смысла для компилятора .NET или джиттера. –

Смежные вопросы