Здравствуйте, _NN_, Вы писали:
? _NN>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?
Атомарная тут только запись, чтение теоретически может приводить к stale read и в этом случае не будет брошено исключение когда dispose вызван. Это в принципе нестрашно, т.к. в этом случае все равно код нормально работать уже не будет.
Interlocked.Read не сделан осознанно, чтобы не замедлять код. Dispose вызывается редко, в отличие от остальных операций на сокетах.
Здравствуйте, vlp, Вы писали:
vlp>Здравствуйте, _NN_, Вы писали: vlp> ? _NN>>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?
vlp>Атомарная тут только запись, чтение теоретически может приводить к stale read и в этом случае не будет брошено исключение когда dispose вызван. Это в принципе нестрашно, т.к. в этом случае все равно код нормально работать уже не будет.
В каком смысле не страшно ?
Вот я вызвал Dispose, и вызываю Receive в цикле.
Компилятор заинлайнил/процессор положил в кеш переменную и теперь Receive будет вызываться, а _disposed всегда будет 0.
Или так не может быть ?
vlp>Interlocked.Read не сделан осознанно, чтобы не замедлять код. Dispose вызывается редко, в отличие от остальных операций на сокетах.
Здравствуйте, _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"
Здравствуйте, VladCore, Вы писали:
VC>Прикольная задачка. Дело думаю в том что Disposed может переключится из false в true а обратно — нет.
Ничего прикольного, и даже баг по-хорошему. Поскольку вызывать interlocked.read дороговато для когерентности кэшей, а это бы вызывалось для каждого вызова(проверка на dispose), то решили это убрать, один хрен будет исключение(скорее всего ObjectDisposedException). Но почему volatile не добавить :xz .
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, VladCore, Вы писали:
VC>>Прикольная задачка. Дело думаю в том что Disposed может переключится из false в true а обратно — нет.
S>Ничего прикольного, и даже баг по-хорошему. Поскольку вызывать interlocked.read дороговато для когерентности кэшей, а это бы вызывалось для каждого вызова(проверка на dispose), то решили это убрать, один хрен будет исключение(скорее всего ObjectDisposedException). Но почему volatile не добавить :xz .
а чем принципиально здесь поможет volatile?
Здравствуйте, vlp, Вы писали:
vlp>Здравствуйте, _NN_, Вы писали: vlp> ? _NN>>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?
vlp>Атомарная тут только запись, чтение теоретически может приводить к stale read и в этом случае не будет брошено исключение когда dispose вызван. Это в принципе нестрашно, т.к. в этом случае все равно код нормально работать уже не будет.
vlp>Interlocked.Read не сделан осознанно, чтобы не замедлять код. Dispose вызывается редко, в отличие от остальных операций на сокетах.
ну, и не делали бы вовсе тогда проверку и пусть во всех методах предсказуемо падает с хз каким исключением, а не так, что возможно упадёт в ThrowObjectDisposedException, а возможно дальше провалится.
Здравствуйте, karbofos42, Вы писали:
.
K>ну, и не делали бы вовсе тогда проверку и пусть во всех методах предсказуемо падает с хз каким исключением, а не так, что возможно упадёт в ThrowObjectDisposedException, а возможно дальше провалится.
Dispose не должен бросать исключений при повторном вызове, для этого там и сделан lock xchg.
В остальных местах сделано good enough решение — в большинстве случаев оно работает именно как надо, потенциальные проблемы в основном теоретические.
Здравствуйте, _NN_, Вы писали:
_NN> private int _disposed; // 0 == false, anything else == true
ad ?
private volatile int _disposed;
volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, vlp, Вы писали:
S>>Ничего прикольного, и даже баг по-хорошему. Поскольку вызывать interlocked.read дороговато для когерентности кэшей, а это бы вызывалось для каждого вызова(проверка на dispose), то решили это убрать, один хрен будет исключение(скорее всего ObjectDisposedException). Но почему volatile не добавить :xz . vlp>а чем принципиально здесь поможет volatile?
volatile это инструкция компилятору "не оптимизировать обращение к полю". Из-за таких оптимизаций чтение пожет быть "оптимизировано".
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, Философ, Вы писали:
Ф>volatile это инструкция компилятору "не оптимизировать обращение к полю". Из-за таких оптимизаций чтение пожет быть "оптимизировано".
Как именно нужно написать код, чтобы без volatile значение поля disposed было stale из-за "оптимизаций"? Этого и без volatile не происходит. Чтобы происходило нужна не одна проверка в начале функции (она точно из памяти читает), а хотя бы цикл с проверкой. Очень хотелось бы увидеть контрпример в виде кода и результаты дизассемблера.
Здравствуйте, vlp, Вы писали:
Ф>>volatile это инструкция компилятору "не оптимизировать обращение к полю". Из-за таких оптимизаций чтение пожет быть "оптимизировано".
vlp>Как именно нужно написать код, чтобы без volatile значение поля disposed было stale из-за "оптимизаций"? Этого и без volatile не происходит. Чтобы происходило нужна не одна проверка в начале функции (она точно из памяти читает), а хотя бы цикл с проверкой. Очень хотелось бы увидеть контрпример в виде кода и результаты дизассемблера.
Примеры дизассемблера тебе ничего не дадут: сегодня это один пример, а завтра версия джиттера поменяется. Для того, чтобы зачитанное значение могло застрять в регистре, достаточно несколько подряд вызовов, дёргающих ThrowIfDisposed(), например:
Connect()
Read()
Send()
Даже теоретической возмозможности обычно хватает, чтобы ты раз в полгода застревал в дебаге надолго. Такие вещи стреляют редко, но если стреляют, то хоть вешайся.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, Философ, Вы писали:
Ф>volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти.
internal bool Disposed => _disposed != 0;
когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться.
собаку может подложить кэш процессора. теоретически в локальном кэше ядра, выполняющего геттер может лежать устаревшее значение.
хотя вроде в процессорах есть довольно продвинутая система инвалидации кэшей и возможно и такой проблемы нет.
Здравствуйте, MadHuman, Вы писали:
Ф>>volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти. MH>
MH>internal bool Disposed => _disposed != 0;
MH>
MH>когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться
Такие гарантии может дать только volatile, т.е. запрет оптимизаций. В ином случае только на везение можно рассчитывать. В большинстве случае везёт. Учитывай, что геттер Disposed вполне может быть заинлайнен.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, Философ, Вы писали:
Ф>Здравствуйте, MadHuman, Вы писали:
Ф>>>volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти. MH>>
MH>>когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться
Ф>Такие гарантии может дать только volatile, т.е. запрет оптимизаций.
нет. нет других вариантов для реализации этого метода кроме как генерировать ассемблерную инструкцию чтения памяти.
volatile — актуальна, когда в рамках одного метода надо запретить компилятору оптимизации. вот в таком кейсе (в жирном методе с рядом обращений к филду) он вполне может после 1-го чтения, запомнить результат в регистре
и затем к нему обращаться.
Ф> Учитывай, что геттер Disposed вполне может быть заинлайнен.
это хороший аргумент. тут ты прав.
но опять, же проблема будет, если иналйниться внутри жирного метода в котором уже были вызовы Disposed, и да, тогда похоже jit может сгенерить код, который воспользуется результатом пред чтения _disposed
Здравствуйте, MadHuman, Вы писали:
MH>>>когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться
Ф>>Такие гарантии может дать только volatile, т.е. запрет оптимизаций. MH>нет. нет других вариантов для реализации этого метода кроме как генерировать ассемблерную инструкцию чтения памяти.
Есть другие варианты. Сегодня генерируется
cmp byte ptr [rcx+8],0; в RCX указатель this
а завтра может быть пара из mov и cmp. Например потому, что это другой процессор, и там джиттер работает немного иначе. И очередной mov вполне может быть опущен.
MH>volatile — актуальна, когда в рамках одного метода надо запретить компилятору оптимизации. вот в таком кейсе (в жирном методе с рядом обращений к филду) он вполне может после 1-го чтения, запомнить результат в регистре MH>и затем к нему обращаться.
Проблема как раз в том, что метод вполне себе может быть жирным, и ты это никак не можешь проконтролировать. Просто очередной программист напишет пяток Receive() и пару Send() — всё, приехали: вот он простор для компиляторных оптимизаций.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, MadHuman, Вы писали:
MH>Здравствуйте, Философ, Вы писали:
Ф>>volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти. MH>
MH>internal bool Disposed => _disposed != 0;
MH>
MH>когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться.
А как же инлайнинг и оптимизация?
Разве есть гарантия, что компилятор и джит обязаны в случае свойства всегда читать из памяти?
Ф>>>>>volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти. 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 сторону от ошибочного действия.