Многопоточность
От: _NN_ www.nemerleweb.com
Дата: 21.12.20 18:23
Оценка: 7 (1)
Код Socket.cs имеет автомарную запись в Dispose:

            // Make sure we're the first call to Dispose
            if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 1)
            {
                return;
            }


Но само чтение _disposed происходит не автомарно


        private int _disposed; // 0 == false, anything else == true

        internal bool Disposed => _disposed != 0;

        private void ThrowIfDisposed()
        {
            if (Disposed)
            {
                ThrowObjectDisposedException();
            }
        }


Гарантируется ли в C# работоспособность этого кода ?
Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re: Многопоточность
От: vlp  
Дата: 21.12.20 18:32
Оценка: 7 (1) -1
Здравствуйте, _NN_, Вы писали:
?
_NN>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?

Атомарная тут только запись, чтение теоретически может приводить к stale read и в этом случае не будет брошено исключение когда dispose вызван. Это в принципе нестрашно, т.к. в этом случае все равно код нормально работать уже не будет.

Interlocked.Read не сделан осознанно, чтобы не замедлять код. Dispose вызывается редко, в отличие от остальных операций на сокетах.
Re[2]: Многопоточность
От: _NN_ www.nemerleweb.com
Дата: 21.12.20 18:40
Оценка:
Здравствуйте, vlp, Вы писали:

vlp>Здравствуйте, _NN_, Вы писали:

vlp> ?
_NN>>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?

vlp>Атомарная тут только запись, чтение теоретически может приводить к stale read и в этом случае не будет брошено исключение когда dispose вызван. Это в принципе нестрашно, т.к. в этом случае все равно код нормально работать уже не будет.

В каком смысле не страшно ?
Вот я вызвал Dispose, и вызываю Receive в цикле.
Компилятор заинлайнил/процессор положил в кеш переменную и теперь Receive будет вызываться, а _disposed всегда будет 0.
Или так не может быть ?

vlp>Interlocked.Read не сделан осознанно, чтобы не замедлять код. Dispose вызывается редко, в отличие от остальных операций на сокетах.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[3]: Многопоточность
От: vlp  
Дата: 21.12.20 19:02
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>Здравствуйте, vlp, Вы писали:


vlp>>Здравствуйте, _NN_, Вы писали:

vlp>> ?
_NN>>>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?

vlp>>Атомарная тут только запись, чтение теоретически может приводить к stale read и в этом случае не будет брошено исключение когда dispose вызван. Это в принципе нестрашно, т.к. в этом случае все равно код нормально работать уже не будет.

_NN>В каком смысле не страшно ?
В смысле, что если вы используете сокет после того, как его задиспоузили, код уже неверный и вопрос только в том, с какой ошибкой он будет падать.

_NN>Вот я вызвал Dispose, и вызываю Receive в цикле.

_NN>Компилятор заинлайнил/процессор положил в кеш переменную и теперь Receive будет вызываться, а _disposed всегда будет 0.
_NN>Или так не может быть ?
Наверное, может.
Если сам сокет уже disposed, read упадет в другом месте, а не с сообщением "object disposed"
Отредактировано 21.12.2020 20:09 vlp . Предыдущая версия .
Re: Многопоточность
От: VladCore  
Дата: 21.12.20 21:34
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>Код Socket.cs имеет автомарную запись в Dispose:


_NN>
_NN>            // Make sure we're the first call to Dispose
_NN>            if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 1)
_NN>            {
_NN>                return;
_NN>            }

_NN>


_NN>Но само чтение _disposed происходит не автомарно


_NN>

_NN>        private int _disposed; // 0 == false, anything else == true

_NN>        internal bool Disposed => _disposed != 0;

_NN>        private void ThrowIfDisposed()
_NN>        {
_NN>            if (Disposed)
_NN>            {
_NN>                ThrowObjectDisposedException();
_NN>            }
_NN>        }
_NN>


_NN>Гарантируется ли в C# работоспособность этого кода ?

_NN>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?

Прикольная задачка. Дело думаю в том что Disposed может переключится из false в true а обратно — нет.
Re[2]: Многопоточность
От: Sharov Россия  
Дата: 21.12.20 22:18
Оценка:
Здравствуйте, VladCore, Вы писали:

VC>Прикольная задачка. Дело думаю в том что Disposed может переключится из false в true а обратно — нет.


Ничего прикольного, и даже баг по-хорошему. Поскольку вызывать interlocked.read дороговато для когерентности кэшей, а это бы вызывалось для каждого вызова(проверка на dispose), то решили это убрать, один хрен будет исключение(скорее всего ObjectDisposedException). Но почему volatile не добавить :xz .
Кодом людям нужно помогать!
Re[3]: Многопоточность
От: vlp  
Дата: 21.12.20 23:14
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Здравствуйте, VladCore, Вы писали:


VC>>Прикольная задачка. Дело думаю в том что Disposed может переключится из false в true а обратно — нет.


S>Ничего прикольного, и даже баг по-хорошему. Поскольку вызывать interlocked.read дороговато для когерентности кэшей, а это бы вызывалось для каждого вызова(проверка на dispose), то решили это убрать, один хрен будет исключение(скорее всего ObjectDisposedException). Но почему volatile не добавить :xz .

а чем принципиально здесь поможет volatile?
Re[2]: Многопоточность
От: karbofos42 Россия  
Дата: 22.12.20 05:52
Оценка: +1 :)
Здравствуйте, vlp, Вы писали:

vlp>Здравствуйте, _NN_, Вы писали:

vlp> ?
_NN>>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?

vlp>Атомарная тут только запись, чтение теоретически может приводить к stale read и в этом случае не будет брошено исключение когда dispose вызван. Это в принципе нестрашно, т.к. в этом случае все равно код нормально работать уже не будет.


vlp>Interlocked.Read не сделан осознанно, чтобы не замедлять код. Dispose вызывается редко, в отличие от остальных операций на сокетах.


ну, и не делали бы вовсе тогда проверку и пусть во всех методах предсказуемо падает с хз каким исключением, а не так, что возможно упадёт в ThrowObjectDisposedException, а возможно дальше провалится.
Re[3]: Многопоточность
От: vlp  
Дата: 22.12.20 06:27
Оценка:
Здравствуйте, karbofos42, Вы писали:
.

K>ну, и не делали бы вовсе тогда проверку и пусть во всех методах предсказуемо падает с хз каким исключением, а не так, что возможно упадёт в ThrowObjectDisposedException, а возможно дальше провалится.


Dispose не должен бросать исключений при повторном вызове, для этого там и сделан lock xchg.

В остальных местах сделано good enough решение — в большинстве случаев оно работает именно как надо, потенциальные проблемы в основном теоретические.
Re: Многопоточность
От: Философ Ад http://vk.com/id10256428
Дата: 22.12.20 06:40
Оценка: :)
Здравствуйте, _NN_, Вы писали:

_NN> private int _disposed; // 0 == false, anything else == true

ad ?

private volatile int _disposed;

volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти.
Всё сказанное выше — личное мнение, если не указано обратное.
Re[4]: Многопоточность
От: Философ Ад http://vk.com/id10256428
Дата: 22.12.20 06:43
Оценка:
Здравствуйте, vlp, Вы писали:

S>>Ничего прикольного, и даже баг по-хорошему. Поскольку вызывать interlocked.read дороговато для когерентности кэшей, а это бы вызывалось для каждого вызова(проверка на dispose), то решили это убрать, один хрен будет исключение(скорее всего ObjectDisposedException). Но почему volatile не добавить :xz .

vlp>а чем принципиально здесь поможет volatile?

volatile это инструкция компилятору "не оптимизировать обращение к полю". Из-за таких оптимизаций чтение пожет быть "оптимизировано".
Всё сказанное выше — личное мнение, если не указано обратное.
Re[5]: Многопоточность
От: vlp  
Дата: 22.12.20 07:00
Оценка:
Здравствуйте, Философ, Вы писали:

Ф>volatile это инструкция компилятору "не оптимизировать обращение к полю". Из-за таких оптимизаций чтение пожет быть "оптимизировано".


Как именно нужно написать код, чтобы без volatile значение поля disposed было stale из-за "оптимизаций"? Этого и без volatile не происходит. Чтобы происходило нужна не одна проверка в начале функции (она точно из памяти читает), а хотя бы цикл с проверкой. Очень хотелось бы увидеть контрпример в виде кода и результаты дизассемблера.
Отредактировано 23.12.2020 2:22 VladD2 . Предыдущая версия .
Re[6]: Многопоточность
От: Философ Ад http://vk.com/id10256428
Дата: 22.12.20 07:40
Оценка:
Здравствуйте, vlp, Вы писали:

Ф>>volatile это инструкция компилятору "не оптимизировать обращение к полю". Из-за таких оптимизаций чтение пожет быть "оптимизировано".


vlp>Как именно нужно написать код, чтобы без volatile значение поля disposed было stale из-за "оптимизаций"? Этого и без volatile не происходит. Чтобы происходило нужна не одна проверка в начале функции (она точно из памяти читает), а хотя бы цикл с проверкой. Очень хотелось бы увидеть контрпример в виде кода и результаты дизассемблера.


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

Connect()
Read()
Send()

Даже теоретической возмозможности обычно хватает, чтобы ты раз в полгода застревал в дебаге надолго. Такие вещи стреляют редко, но если стреляют, то хоть вешайся.
Всё сказанное выше — личное мнение, если не указано обратное.
Re[6]: Многопоточность
От: Философ Ад http://vk.com/id10256428
Дата: 22.12.20 07:41
Оценка:
Здравствуйте, vlp, Вы писали:

vlp>Как именно нужно написать код, чтобы без volatile значение поля disposed было stale из-за "оптимизаций"?


Значение поля может быть и верным, но вот его фактического чтения может и не произойти.
Всё сказанное выше — личное мнение, если не указано обратное.
Re[2]: Многопоточность
От: MadHuman Россия  
Дата: 22.12.20 07:42
Оценка:
Здравствуйте, Философ, Вы писали:

Ф>volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти.

internal bool Disposed => _disposed != 0;


когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться.
собаку может подложить кэш процессора. теоретически в локальном кэше ядра, выполняющего геттер может лежать устаревшее значение.
хотя вроде в процессорах есть довольно продвинутая система инвалидации кэшей и возможно и такой проблемы нет.
Отредактировано 22.12.2020 7:47 MadHuman . Предыдущая версия . Еще …
Отредактировано 22.12.2020 7:46 MadHuman . Предыдущая версия .
Re[3]: Многопоточность
От: Философ Ад http://vk.com/id10256428
Дата: 22.12.20 07:45
Оценка:
Здравствуйте, MadHuman, Вы писали:

Ф>>volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти.

MH>
MH>internal bool Disposed => _disposed != 0;
MH>


MH>когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться


Такие гарантии может дать только volatile, т.е. запрет оптимизаций. В ином случае только на везение можно рассчитывать. В большинстве случае везёт. Учитывай, что геттер Disposed вполне может быть заинлайнен.
Всё сказанное выше — личное мнение, если не указано обратное.
Re[4]: Многопоточность
От: MadHuman Россия  
Дата: 22.12.20 07:55
Оценка:
Здравствуйте, Философ, Вы писали:

Ф>Здравствуйте, MadHuman, Вы писали:


Ф>>>volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти.

MH>>
MH>>internal bool Disposed => _disposed != 0;
MH>>


MH>>когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться


Ф>Такие гарантии может дать только volatile, т.е. запрет оптимизаций.

нет. нет других вариантов для реализации этого метода кроме как генерировать ассемблерную инструкцию чтения памяти.
volatile — актуальна, когда в рамках одного метода надо запретить компилятору оптимизации. вот в таком кейсе (в жирном методе с рядом обращений к филду) он вполне может после 1-го чтения, запомнить результат в регистре
и затем к нему обращаться.

Ф> Учитывай, что геттер Disposed вполне может быть заинлайнен.

это хороший аргумент. тут ты прав.
но опять, же проблема будет, если иналйниться внутри жирного метода в котором уже были вызовы Disposed, и да, тогда похоже jit может сгенерить код, который воспользуется результатом пред чтения _disposed
Отредактировано 22.12.2020 7:57 MadHuman . Предыдущая версия . Еще …
Отредактировано 22.12.2020 7:56 MadHuman . Предыдущая версия .
Re[5]: Многопоточность
От: Философ Ад http://vk.com/id10256428
Дата: 22.12.20 08:48
Оценка: 10 (2) +1
Здравствуйте, MadHuman, Вы писали:

MH>>>когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться


Ф>>Такие гарантии может дать только volatile, т.е. запрет оптимизаций.

MH>нет. нет других вариантов для реализации этого метода кроме как генерировать ассемблерную инструкцию чтения памяти.

Есть другие варианты. Сегодня генерируется
cmp byte ptr [rcx+8],0; в RCX указатель this
а завтра может быть пара из mov и cmp. Например потому, что это другой процессор, и там джиттер работает немного иначе. И очередной mov вполне может быть опущен.

MH>volatile — актуальна, когда в рамках одного метода надо запретить компилятору оптимизации. вот в таком кейсе (в жирном методе с рядом обращений к филду) он вполне может после 1-го чтения, запомнить результат в регистре

MH>и затем к нему обращаться.

Проблема как раз в том, что метод вполне себе может быть жирным, и ты это никак не можешь проконтролировать. Просто очередной программист напишет пяток Receive() и пару Send() — всё, приехали: вот он простор для компиляторных оптимизаций.
Всё сказанное выше — личное мнение, если не указано обратное.
Отредактировано 22.12.2020 9:04 Философ . Предыдущая версия .
Re[3]: Многопоточность
От: _NN_ www.nemerleweb.com
Дата: 22.12.20 08:55
Оценка:
Здравствуйте, MadHuman, Вы писали:

MH>Здравствуйте, Философ, Вы писали:


Ф>>volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти.

MH>
MH>internal bool Disposed => _disposed != 0;
MH>


MH>когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться.

А как же инлайнинг и оптимизация?
Разве есть гарантия, что компилятор и джит обязаны в случае свойства всегда читать из памяти?
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[6]: Многопоточность
От: MadHuman Россия  
Дата: 22.12.20 09:11
Оценка: +1
Ф>>>>>volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти.
MH>>>>
MH>>>>internal bool Disposed => _disposed != 0;
MH>>>>


MH>>>>когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться


Ф>>>Такие гарантии может дать только volatile, т.е. запрет оптимизаций.

MH>>нет. нет других вариантов для реализации этого метода кроме как генерировать ассемблерную инструкцию чтения памяти.

Ф>а завтра может быть пара из mov и cmp. Например потому, что это другой процессор, и там джиттер работает немного иначе.

mov — всё равно из памяти будет читать
то есть другого варианта нет
дело не в том как джиттер работает. дело в том что при передаче управления в метод-геттера в теле метода надо читать филд, из памяти.


MH>>volatile — актуальна, когда в рамках одного метода надо запретить компилятору оптимизации. вот в таком кейсе (в жирном методе с рядом обращений к филду) он вполне может после 1-го чтения, запомнить результат в регистре

MH>>и затем к нему обращаться.

Ф>Проблема как раз в том, что метод вполне себе может быть жирным, и ты это никак не можешь проконтролировать. Просто очередной программист напишет пяток Receive() и пару Send() — всё, приехали: вот он простор для компиляторных оптимизаций.

дак а я о чём? конечно может быть какой-то метод жирным и в нём ряд обращений к полю компилятор (а затем джит) заоптимит так что 1-й раз прочитается в регистр а затем из него.
но мы рассматриваем ситуацию топик-стартера, то есть речь об методе
internal bool Disposed => _disposed != 0;

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

более того — если посмотреть в целом на ситуацию, то volatile не нужен. т.к. даже если мы гарантированно прочитали память, то в следующий момент ( может даже ещё до выхода из геттера) _disposed может оказаться выставленным,
и вызывающая сторона ошибочно посчитает что ещё не диспозед, хотя уже диспозед. то есть volatile никак не предотвратит на 100% вызывающую Disposed сторону от ошибочного действия.
Re[4]: Многопоточность
От: MadHuman Россия  
Дата: 22.12.20 09:13
Оценка:
Здравствуйте, _NN_, Вы писали:

Ф>>>volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти.

MH>>
MH>>internal bool Disposed => _disposed != 0;
MH>>


MH>>когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться.

_NN>А как же инлайнинг и оптимизация?
_NN>Разве есть гарантия, что компилятор и джит обязаны в случае свойства всегда читать из памяти?

здесь
Автор: MadHuman
Дата: 22.12.20
Re[7]: Многопоточность
От: Философ Ад http://vk.com/id10256428
Дата: 22.12.20 09:24
Оценка:
Здравствуйте, MadHuman, Вы писали:

Ф>>а завтра может быть пара из mov и cmp. Например потому, что это другой процессор, и там джиттер работает немного иначе.

MH>mov — всё равно из памяти будет читать
MH>то есть другого варианта нет
MH>дело не в том как джиттер работает. дело в том что при передаче управления в метод-геттера в теле метода надо читать филд, из памяти.

mov будет читать из памяти — других вариантов у него нет, но из ряда
mov -> cmp
mov -> cmp
mov -> cmp

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


MH>
MH>internal bool Disposed => _disposed != 0;
MH>

MH>этот метод не жирный. здесь единственный случай чтения поля, и его можно только прочитать из памяти. не тот случай когда нужен volatile.

Проблема не в этом методе, а в том, что для этого поля оптимизации никто не запрещал.

MH>более того — если посмотреть в целом на ситуацию, то volatile не нужен. т.к. даже если мы гарантированно прочитали память, то в следующий момент ( может даже ещё до выхода из геттера) _disposed может оказаться выставленным,

MH>и вызывающая сторона ошибочно посчитает что ещё не диспозед, хотя уже диспозед. то есть volatile никак не предотвратит на 100% вызывающую Disposed сторону от ошибочного действия.

Угу, а потом ты будешь удивляться, что по логам пы коммит для транзакции послали, а транзакция куда-то пропала, хотя на самом деле никуда мы ничего не послали, а в этот момент сокет кто-то задиспозил.
Понимаешь, вовремя кинутое исключение ObjectDisposedException может тебе сократить неделю отладки.
Всё сказанное выше — личное мнение, если не указано обратное.
Re[7]: Многопоточность
От: Sharov Россия  
Дата: 22.12.20 10:47
Оценка:
Здравствуйте, MadHuman, Вы писали:

MH>этот метод не жирный. здесь единственный случай чтения поля, и его можно только прочитать из памяти. не тот случай когда нужен volatile.


А почему только из памяти, какие гарантии?
Кодом людям нужно помогать!
Re[8]: Многопоточность
От: MadHuman Россия  
Дата: 22.12.20 11:04
Оценка: +1
Здравствуйте, Философ, Вы писали:


MH>>более того — если посмотреть в целом на ситуацию, то volatile не нужен. т.к. даже если мы гарантированно прочитали память, то в следующий момент ( может даже ещё до выхода из геттера) _disposed может оказаться выставленным,

MH>>и вызывающая сторона ошибочно посчитает что ещё не диспозед, хотя уже диспозед. то есть volatile никак не предотвратит на 100% вызывающую Disposed сторону от ошибочного действия.

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

Ф>Понимаешь, вовремя кинутое исключение ObjectDisposedException может тебе сократить неделю отладки.
согласен. но в том-то и дело, что в данном примере топик-стартера нет 100% гарантии, что после того как из Disposed получили false (даже не заинлайнёнными и/или с volatile для _disposed)
сокет всё ещё не задиспозен, и описанная вами ситуация всё равно может повториться.
то есть volatile возможно может уменьшить вероятность такой ситуации (и ещё вопрос насколько), но никак не исключит её.
Отредактировано 22.12.2020 11:07 MadHuman . Предыдущая версия .
Re[8]: Многопоточность
От: MadHuman Россия  
Дата: 22.12.20 11:06
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Здравствуйте, MadHuman, Вы писали:


MH>>этот метод не жирный. здесь единственный случай чтения поля, и его можно только прочитать из памяти. не тот случай когда нужен volatile.


S>А почему только из памяти, какие гарантии?

а откуда ещё (при условии что метод не заинлайнен)? есть какие-то предположения откуда, кроме как из памяти?
Re[4]: Многопоточность
От: Sharov Россия  
Дата: 22.12.20 11:07
Оценка:
Здравствуйте, vlp, Вы писали:

vlp>Здравствуйте, Sharov, Вы писали:


S>>Здравствуйте, VladCore, Вы писали:


VC>>>Прикольная задачка. Дело думаю в том что Disposed может переключится из false в true а обратно — нет.


S>>Ничего прикольного, и даже баг по-хорошему. Поскольку вызывать interlocked.read дороговато для когерентности кэшей, а это бы вызывалось для каждого вызова(проверка на dispose), то решили это убрать, один хрен будет исключение(скорее всего ObjectDisposedException). Но почему volatile не добавить :xz .

vlp>а чем принципиально здесь поможет volatile?

Судя по сообщением выше, действительно ничем. А вообще,если к переменной возможен доступ из нескольких потоков, то
сама же ms рекомендует этой переменной добавлять модификатор volatile.
Кодом людям нужно помогать!
Re[4]: Многопоточность
От: romangr Россия  
Дата: 22.12.20 11:12
Оценка: +2
Здравствуйте, Философ, Вы писали:

Ф>Такие гарантии может дать только volatile, т.е. запрет оптимизаций. В ином случае только на везение можно рассчитывать. В большинстве случае везёт. Учитывай, что геттер Disposed вполне может быть заинлайнен.


Гарантий, которые дает volatile недостаточно, т.к. между инструкциями cmp [ecx + offset], 1 и последующей за ней je/jne запросто может случиться context switch.
И пока этот поток спит, параллельный поток устанавливает _isdisposed=1. После чего первый поток просыпается и выбирает не ту ветку, т.к. в регистре флагов сохранено устаревшее значение.
... << RSDN@Home 1.3.110 alpha 5 rev. 62>>
Re[2]: Многопоточность
От: Sharov Россия  
Дата: 22.12.20 11:19
Оценка:
Здравствуйте, vlp, Вы писали:

vlp>Здравствуйте, _NN_, Вы писали:

vlp> ?
_NN>>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?

vlp>Атомарная тут только запись, чтение теоретически может приводить к stale read и в этом случае не будет брошено исключение когда dispose вызван. Это в принципе нестрашно, т.к. в этом случае все равно код нормально работать уже не будет.


vlp>Interlocked.Read не сделан осознанно, чтобы не замедлять код. Dispose вызывается редко, в отличие от остальных операций на сокетах.


А нет ли здесь экономии на спичках? Т.е. мы не гоняем лишний раз когерентность кэшей для n потоков,
но при этом когда мы действительно освободим сокет, то n-1 поток обломятся на стадии системных вызовов ОС,
потеряв при этом не малое кол-во времени на обращении к той же памяти, при учете возможный прерываний отсутствующих
страниц VM и т.п. накладных расходов?

PS: Блин, ладно, удачно описался, и сам себе на вопрос и ответил. Оставлю как есть.
Действительно, проблемы тут нету, ибо Interlocked.CompareExchange как раз когеретность кэшей и вызывает, т.е.
все disposed поля у всех потоков будут одинаковы. Я ошибся в том, что Interlocked.Read также инициирует
когерентность кэшей, чего по идее может и не быть, это же чтение, а не запись. Т.е. Interlocked.CompareExchange
сделает disposed поле в кэше для всех одинаковым.

PPS: Блин, все-таки и вправду ошибка.А если disposed читается из регистра, а не из, хотя бы кэша? Т.е. вроде бы volatile
должно хватить, не говоря о Interlocked.Read...Так что, кмк, мой первоначальный вопрос в силе.
Кодом людям нужно помогать!
Re[9]: Многопоточность
От: Sharov Россия  
Дата: 22.12.20 11:21
Оценка:
Здравствуйте, MadHuman, Вы писали:

MH>Здравствуйте, Sharov, Вы писали:


S>>Здравствуйте, MadHuman, Вы писали:


MH>>>этот метод не жирный. здесь единственный случай чтения поля, и его можно только прочитать из памяти. не тот случай когда нужен volatile.


S>>А почему только из памяти, какие гарантии?

MH>а откуда ещё (при условии что метод не заинлайнен)? есть какие-то предположения откуда, кроме как из памяти?

А если метод заинлайнен, то регистр, верно? А если нет, то регистр исключен? Я, честно, сам не до конца понимаю.\
Т.е.если инлайна нет, то почему не возможно чтение из регистра?
Кодом людям нужно помогать!
Re[10]: Многопоточность
От: Философ Ад http://vk.com/id10256428
Дата: 22.12.20 11:40
Оценка: 14 (1) +1
Здравствуйте, Sharov, Вы писали:

S>>>А почему только из памяти, какие гарантии?

MH>>а откуда ещё (при условии что метод не заинлайнен)? есть какие-то предположения откуда, кроме как из памяти?

S>А если метод заинлайнен, то регистр, верно? А если нет, то регистр исключен? Я, честно, сам не до конца понимаю.\

S>Т.е.если инлайна нет, то почему не возможно чтение из регистра?

Потому что функция генериутеся так, чтобы не делать никаких предположений о состоянии регистров. В случае экземплярного метода первый неявный параметр — this. Он передаётся в регистре rcx или ecx (x64 и x86 соответственно). Обращение к полям внутри метода — [rcx+FieldOffset], например:
cmp     byte ptr [rcx+8],0


Если функция заинлайнена, то в коде могут быть предположения о том, что находится в регистрах.
Всё сказанное выше — личное мнение, если не указано обратное.
Re[10]: Многопоточность
От: MadHuman Россия  
Дата: 22.12.20 11:42
Оценка: +1
Здравствуйте, Sharov, Вы писали:

S>Здравствуйте, MadHuman, Вы писали:


MH>>Здравствуйте, Sharov, Вы писали:


S>>>Здравствуйте, MadHuman, Вы писали:


MH>>>>этот метод не жирный. здесь единственный случай чтения поля, и его можно только прочитать из памяти. не тот случай когда нужен volatile.


S>>>А почему только из памяти, какие гарантии?

MH>>а откуда ещё (при условии что метод не заинлайнен)? есть какие-то предположения откуда, кроме как из памяти?

S>А если метод заинлайнен, то регистр, верно?

да, если метод заинлайнен и ранее в нём было чтение этого филда, то вот в этом случае возможна оптимизация, что прочитанное его значение
запомнится в переменной-регистре и последующие чтения будут оттуда.
но в отдельно взятом методе, 1-е чтение филда всегда будет из памяти. потому что других вариантов нет)
Re[3]: Многопоточность
От: vlp  
Дата: 22.12.20 18:39
Оценка:
Здравствуйте, karbofos42, Вы писали:

K>ну, и не делали бы вовсе тогда проверку

проверка работает хорошо за исключением весьма и весьма редких случаев. На практике эти случаи возникают очень редко. Попробуйте на практике добиться того, чем тут всех пугают в этом треде — я уверен, что у вас не получится, а ObjectDisposedException вы будете получать стабильно. Профит для разработчика очевиден.
Re[7]: Многопоточность
От: vlp  
Дата: 22.12.20 18:48
Оценка:
Здравствуйте, Философ, Вы писали:

Ф>Здравствуйте, vlp, Вы писали:


Ф>>>volatile это инструкция компилятору "не оптимизировать обращение к полю". Из-за таких оптимизаций чтение пожет быть "оптимизировано".


vlp>>Как именно нужно написать код, чтобы без volatile значение поля disposed было stale из-за "оптимизаций"? Этого и без volatile не происходит. Чтобы происходило нужна не одна проверка в начале функции (она точно из памяти читает), а хотя бы цикл с проверкой. Очень хотелось бы увидеть контрпример в виде кода и результаты дизассемблера.


Ф>Примеры дизассемблера тебе ничего не дадут: сегодня это один пример, а завтра версия джиттера поменяется.

Я просил пример того, как прямо сейчас, с текущей версией компилятора и джиттера, возможна ситуация, которой тут всех пугают и как volatile в ней помогает.
Тут, судя по ответам, в треде одни философы-теоретики, примеров нет.

>Для того, чтобы зачитанное значение могло застрять в регистре, достаточно несколько подряд вызовов, дёргающих ThrowIfDisposed(), например:


Ф>Connect()

Ф>Read()
Ф>Send()

Покажите скомпилированный код + дизассемблер кода, который в этом случае будет зачем-то записывать значение disposed в регистр и его потом переиспользовать. На практике и без volatile этого не делается, вместо этого значение всегда читается из памяти (почему-объяснять относительно долго)/

Ф>Такие вещи стреляют редко, но если стреляют, то хоть вешайся.

Чтобы это выстрелило, надо использовать disposed сокет. А race condition дебагать действительно тяжело, да.
Re[5]: Многопоточность
От: vlp  
Дата: 22.12.20 18:53
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Судя по сообщением выше, действительно ничем. А вообще,если к переменной возможен доступ из нескольких потоков, то

S>сама же ms рекомендует этой переменной добавлять модификатор volatile.
Ну так не надо слепо следовать всем рекомендациям, не понимая их сути. В ряде контекстов эта рекомендация не имеет смысла, а иногда даже вредит.
Re[8]: Многопоточность
От: Философ Ад http://vk.com/id10256428
Дата: 22.12.20 19:52
Оценка:
Здравствуйте, vlp, Вы писали:

Ф>>Примеры дизассемблера тебе ничего не дадут: сегодня это один пример, а завтра версия джиттера поменяется.

vlp>Я просил пример того, как прямо сейчас, с текущей версией компилятора и джиттера, возможна ситуация, которой тут всех пугают и как volatile в ней помогает.
vlp>Тут, судя по ответам, в треде одни философы-теоретики, примеров нет.

Зачем тебе реальный пример? Тебя теоритическая возможность засесть на пару недель дебага не достаточно?
На x86 cейчас там обычный cmp с ячейкой, реально генерируемый код выглядит вот так:
00007ff9`6153096b 80790900        cmp     byte ptr [rcx+9],0
00007ff9`6153096f 7549            jne     00007ff9`615309ba


Можешь теоретически объяснить, почему так? Почему не
mov rax, byte ptr [rcx+9]
cmp rax,0
jne SomeOffset

???
Полагаю, что потому что в системе команд присутствует cmp с ячейкой памяти? Ну ещё это может быть быстрее.
А если нет, если через регистр окажется быстрее!?


vlp>На практике и без volatile этого не делается, вместо этого значение всегда читается из памяти (почему-объяснять относительно долго)/


Даже на практике в диассемлерных листнингах встречается вот такое:
00007ff9`615508fb 488b4808        mov     rcx,qword ptr [rax+8]
00007ff9`615508ff 83790c02        cmp     dword ptr [rcx+0Ch],2


Т.е. в этом случае действуют через промежуточный регистр. Почему ты думаешь, что в случае ThrowIfDisposed() не может произойти тоже самое?
Всё сказанное выше — личное мнение, если не указано обратное.
Re[9]: Многопоточность
От: vlp  
Дата: 22.12.20 20:01
Оценка:
Здравствуйте, Философ, Вы писали:

Ф>Здравствуйте, vlp, Вы писали:


Ф>>>Примеры дизассемблера тебе ничего не дадут: сегодня это один пример, а завтра версия джиттера поменяется.

vlp>>Я просил пример того, как прямо сейчас, с текущей версией компилятора и джиттера, возможна ситуация, которой тут всех пугают и как volatile в ней помогает.
vlp>>Тут, судя по ответам, в треде одни философы-теоретики, примеров нет.

Ф>Зачем тебе реальный пример? Т

ну как зачем — чтобы спровоцировать кого-то подумать

>Тебя теоритическая возможность засесть на пару недель дебага не достаточно?

неа

Ф>Даже на практике в диассемлерных листнингах встречается вот такое:

Ф>
Ф>00007ff9`615508fb 488b4808        mov     rcx,qword ptr [rax+8]
Ф>00007ff9`615508ff 83790c02        cmp     dword ptr [rcx+0Ch],2
Ф>


Ф>Т.е. в этом случае действуют через промежуточный регистр.

в этом случае сначала из памяти читается адрес, а потом опять из памяти сравнивается значние по другому адресу. Это чтение поля класса, когда где-то есть указатель на инстанс (он грузится в ecx, а сам лежит в [rax+8]). Какое отношение это имеет к поведению volatile?

> Почему ты думаешь, что в случае ThrowIfDisposed() не может произойти тоже самое?

произойти что именно? Покажите пример того, что именно произойдет и от чего вас убережет volatile.
Re[6]: Многопоточность
От: Философ Ад http://vk.com/id10256428
Дата: 22.12.20 20:25
Оценка:
Здравствуйте, vlp, Вы писали:

S>>сама же ms рекомендует этой переменной добавлять модификатор volatile.

vlp>Ну так не надо слепо следовать всем рекомендациям, не понимая их сути. В ряде контекстов эта рекомендация не имеет смысла, а иногда даже вредит.

Ну так я, например, реально натыкался на примеры того, что софтина зависает из-за кэширования значений в регистре. Т.е. там был выход из цикла по условию, а условие закэшировалось в регистре и реального чтения не происходило, т.е. вместо

mov r6, [rcx + myVarOffset]
cmp r7, [rcx + myVar2Offset]
jne ..

осталось только две последних строчки, и чтения в регистр не происходило. Запрет на оптимизацию (volatile) решило эту проблему. И было реально тяжело догадаться, что там вот такая засада. У меня там пара седых волос прибавилась. С тех пор и пишу volatile в таких местах — следую рекомендации ms. А учитывая, что бывают вещи и похитрее, я стараюсь исключать вообще даже вероятность таких ошибок, иначе на всю жизнь останешься в дебаге...
Всё сказанное выше — личное мнение, если не указано обратное.
Re[7]: Многопоточность
От: vlp  
Дата: 22.12.20 20:44
Оценка:
Здравствуйте, Философ, Вы писали:


Ф>Ну так я, например, реально натыкался на примеры того, что софтина зависает из-за кэширования значений в регистре. Т.е. там был выход из цикла по условию, а условие закэшировалось в регистре и реального чтения не происходило, т.е. вместо


Ф>mov r6, [rcx + myVarOffset]

Ф>cmp r7, [rcx + myVar2Offset]
Ф>jne ..

Ф>осталось только две последних строчки, и чтения в регистр не происходило. Запрет на оптимизацию (volatile) решило эту проблему.

я знаю, что происходит в случае, когда есть цикл и сохранение значния в регистре. Я просил пример именно с кодом из socket.cs. Там проверка ровно один раз в начале каждого метода. Циклов нет.
Re[6]: Многопоточность
От: Sharov Россия  
Дата: 22.12.20 21:40
Оценка:
Здравствуйте, vlp, Вы писали:

vlp>Ну так не надо слепо следовать всем рекомендациям, не понимая их сути. В ряде контекстов эта рекомендация не имеет смысла, а иногда даже вредит.


Я бы не сказал, что не понимаю сути-- запретить компилятору любые оптимизации с данным полем, чтобы где-то чего-то не переставить, закэшировать и тп. Просто этот конкретный случай хак ms -- не использовать volatile там, где в оф. документах и книжках использовать советуют.
Кодом людям нужно помогать!
Re: Многопоточность
От: VladD2 Российская Империя www.nemerle.org
Дата: 23.12.20 02:20
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>Код Socket.cs имеет автомарную запись в Dispose:

_NN> if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 1)

Это не атомарная запись. А атомарный обмен. Бул всегда атомарно читается и пишется.

_NN>Гарантируется ли в C# работоспособность этого кода ?

_NN>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?

В общем, случае — нет. Но в CompareExchange происходит мемори борьер и если читать после него в том же потоке, то все будет ОК. Дучше добавить к _disposed volatile. Только это не про атомарность, а про сброс кэшей процессоров.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: Многопоточность
От: MadHuman Россия  
Дата: 24.12.20 08:59
Оценка: 7 (1)
Здравствуйте, VladD2, Вы писали:

_NN>>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?


VD>В общем, случае — нет. Но в CompareExchange происходит мемори борьер и если читать после него в том же потоке, то все будет ОК. Дучше добавить к _disposed volatile. Только это не про атомарность, а про сброс кэшей процессоров.

дак volatile вынуждает компилятор при генерации кода только всегда читать филд из памяти (запрещая генерировть код так чтоб результат 1-го чтения закэшировать в регистре например и затем реюзать) или ещё добавляет какую-то спец инструкцию перед чтением из памяти чтоб кэши процессора сбрасывать?
разве когерентность кэшей не обеспечивается аппаратно?
иначе существует вероятность при чтении из памяти получать не актуальное значение (которое из локального кэша ядра прочтётся), это действительно так?
Re[3]: Многопоточность
От: Sharov Россия  
Дата: 24.12.20 10:12
Оценка:
Здравствуйте, MadHuman, Вы писали:

MH>Здравствуйте, VladD2, Вы писали:


_NN>>>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?


VD>>В общем, случае — нет. Но в CompareExchange происходит мемори борьер и если читать после него в том же потоке, то все будет ОК. Дучше добавить к _disposed volatile. Только это не про атомарность, а про сброс кэшей процессоров.

MH>дак volatile вынуждает компилятор при генерации кода только всегда читать филд из памяти (запрещая генерировть код так чтоб результат 1-го чтения закэшировать в регистре например и затем реюзать) или ещё добавляет какую-то спец инструкцию перед чтением из памяти чтоб кэши процессора сбрасывать?

А в чем проблема запретить любые манипуляции с этим полем (перестановки в целях оптимизации) и выставлять инструкции
чтения из памяти, а не из регистра? Т.е. вообще не генерировать код чтения поля из регистра.

MH>разве когерентность кэшей не обеспечивается аппаратно?


Именно железо.

MH>иначе существует вероятность при чтении из памяти получать не актуальное значение (которое из локального кэша ядра прочтётся), это действительно так?


По идее нет, но у процессора столько оптимизаций на счет работы с памятью, что сразу и не скажешь. На этот счет
надо читать Memory model соотв. runtime'а и смотреть какие гарантии он дает. Т.е. ответ на этот вопрос должен дать
этот документ.

Хотя стоп, если кто-то записал новое значение, но его еще не отправил -- процессор попридержал в целях оптимизации, то
возможно, с другой стороны, новое значение должно отразиться на кэше, соотв. если кто-то будет читать из кэша, то
прочитает новое значение, тот кто отправился в RAM -- не судьба...
Кодом людям нужно помогать!
Re[3]: Многопоточность
От: Ночной Смотрящий Россия  
Дата: 24.12.20 12:06
Оценка: +1
Здравствуйте, MadHuman, Вы писали:

MH>разве когерентность кэшей не обеспечивается аппаратно?


В х86 — да. С ARM все несколько сложнее.

volatile, если говорить формально, просто говорит о том что переменную будут читать и писать из разных потоков. А что сделает компилятор, рантайм и железо — зависит от конкретной реализации. И там на любом уровне может быть отличие — и запрет кешировать переменную в регистре, и запрет перестановки инструкций, и добавление барьера, и т.п.
... << RSDN@Home 1.3.17 alpha 5 rev. 62>>
Re[3]: Многопоточность
От: VladD2 Российская Империя www.nemerle.org
Дата: 24.12.20 12:25
Оценка:
Здравствуйте, MadHuman, Вы писали:

MH>дак volatile вынуждает компилятор при генерации кода только всегда читать филд из памяти (запрещая генерировть код так чтоб результат 1-го чтения закэшировать в регистре например и затем реюзать) или ещё добавляет какую-то спец инструкцию перед чтением из памяти чтоб кэши процессора сбрасывать?

MH>разве когерентность кэшей не обеспечивается аппаратно?

Да, он запрещает переупорядочивание инструкций как компилятором, так и процессором.

MH>иначе существует вероятность при чтении из памяти получать не актуальное значение (которое из локального кэша ядра прочтётся), это действительно так?


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

Тут вот приводили реальный пример приводящий к неожиданному поведению.

using System;
using System.Threading;

class Program
{
    static int a;
    static void Main()
    {
        bool b = true;

        new Thread(() =>
            {
                Thread.Sleep(1000);
                b = false;
            })
        {
            IsBackground = true
        }.Start();

        while (b)
        {
            a++;
            //Thread.MemoryBarrier();
        }

        Console.WriteLine($"b={b} a={a}");
    }
}
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.