Многопоточность
От: _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 сторону от ошибочного действия.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.