tag:blogger.com,1999:blog-8596733192274108952.post277478449984944621..comments2024-03-12T06:00:18.305+02:00Comments on Programming stuff: Небольшой трюк при работе с ConcurrentDictionarySergey Teplyakovhttp://www.blogger.com/profile/14300835272589262297noreply@blogger.comBlogger21125tag:blogger.com,1999:blog-8596733192274108952.post-65570937285672751402017-08-18T14:08:58.013+03:002017-08-18T14:08:58.013+03:00Есть вопрос. А почему MS вообще делают такую реали...Есть вопрос. А почему MS вообще делают такую реализацию ConcurrentDictionary?<br />Это баг или есть конкретное обоснование такой реализации?<br /><br />Т.е., если заглянуть внутрь класса ConcurrentDictionary, то (на первый взгляд всяком случае) кажется, что проблему можно было бы решить, если ввести перегрузку метода <br />TryAddInternal, которая могла бы принимать на вход делегат.<br />Тогда можно было бы вызывать эту перегрузку внутри перегрузки метода GetOrAdd, который принимает делегат, т.е. как-то так:<br /><br />public TValue GetOrAdd(TKey key, Func valueFactory)<br /> {<br /> if ((object) key == null)<br /> throw new ArgumentNullException("key");<br /> if (valueFactory == null)<br /> throw new ArgumentNullException("valueFactory");<br /> TValue resultingValue;<br /> if (this.TryGetValue(key, out resultingValue))<br /> return resultingValue;<br /> this.TryAddInternal(key, valueFactory, false, true, out resultingValue);<br /> return resultingValue;<br /> }<br />Anonymoushttps://www.blogger.com/profile/09699853766310467288noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-87221508125091512502015-06-19T03:59:10.699+03:002015-06-19T03:59:10.699+03:00Да, Lazy - это класс, так что будет две дополнител...Да, Lazy - это класс, так что будет две дополнительные аллокации.Sergey Teplyakovhttps://www.blogger.com/profile/14300835272589262297noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-9742444371381534532015-06-19T03:55:59.262+03:002015-06-19T03:55:59.262+03:00Конечно не надо! Ведь для меня это же тоже полезна...Конечно не надо! Ведь для меня это же тоже полезная обратная связь, которая подтвердила мое предположение, что писал я достаточно быстро и, значит, не всем будет понятно:)Sergey Teplyakovhttps://www.blogger.com/profile/14300835272589262297noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-59031284371228487202015-06-18T23:58:13.959+03:002015-06-18T23:58:13.959+03:00Всем спасибо за разъяснение и дискуссию. Еще раз у...Всем спасибо за разъяснение и дискуссию. Еще раз убедился в том, что не надо стесняться задавать "глупые" вопросы :)Anton Norkohttps://www.blogger.com/profile/11409713140969211320noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-2980850070482281642015-06-18T18:35:03.550+03:002015-06-18T18:35:03.550+03:00Из официальной документации:https://msdn.microsoft...Из официальной документации:https://msdn.microsoft.com/en-us/library/ee378677(v=vs.110).aspx<br /><br />> Return Value<br />Type: TValue<br />The value for the key. This will be either the existing value for the key if the key is already in the dictionary, or the new value for the key as returned by valueFactory if the key was not in the dictionary.<br /><br />И секция remarks там же:<br /><br />> If you call GetOrAdd simultaneously on different threads, addValueFactory may be called multiple times, but its key/value pair might not be added to the dictionary for every call.Sergey Teplyakovhttps://www.blogger.com/profile/14300835272589262297noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-27858741932689614102015-06-18T18:17:17.331+03:002015-06-18T18:17:17.331+03:00Возвращаясь к моему изначальному вопросу: подскажи...Возвращаясь к моему изначальному вопросу: подскажите, такое поведение GetOrAdd где-нибудь описано у МС? Для меня оно было не очевидным осшбенно в виду массы примеров в инете похожим на тот второй что я сослался.Beathttps://www.blogger.com/profile/15602820637577186548noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-3807266484248138732015-06-18T18:10:58.591+03:002015-06-18T18:10:58.591+03:00Из статьи:
> С точки зрения многопоточности эт...Из статьи:<br /><br />> С точки зрения многопоточности эта реализация совершенно корректна. Даже если метод RunOperationOrGetFromCache для одного и того же operationId будет вызван из двух потоков, то каждый из них получит один и тот же результат. Проблема же в том, что хоть результат и будет один и тот же, но запущено будет две операции. <br /><br />Хотя, возможно, на этом моменте стоило сильнее акцент сделать.Sergey Teplyakovhttps://www.blogger.com/profile/14300835272589262297noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-24607082904259822242015-06-18T18:05:40.062+03:002015-06-18T18:05:40.062+03:00О том что getoradd снова проверит наличие в кеше и...О том что getoradd снова проверит наличие в кеше и вернет значение из негр в случае если оно есть в статье не сказано. Меня интересовал именно этот момент.Beathttps://www.blogger.com/profile/15602820637577186548noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-19442911197747186592015-06-18T17:58:23.157+03:002015-06-18T17:58:23.157+03:00В заметке приведено три разных версии метода RunOp...В заметке приведено три разных версии метода RunOperationOrGetFromCache. Вы взяли, почему-то, вторую, которая вообще не использует GetOrAdd, и не использует Lazy. Нужно смотреть на последний пример.<br /><br />З.Ы. Код реального GetOrAdd: http://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentDictionary.cs,2f8bcdfbad10304f<br /><br />Реальное поведение описано в статье, потом в первом комменте Алексея и в моем предыдущем комменте.Sergey Teplyakovhttps://www.blogger.com/profile/14300835272589262297noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-42572068802874754122015-06-18T17:55:54.136+03:002015-06-18T17:55:54.136+03:00Lazy - это структура - это плюс, но есть вложенный...Lazy - это структура - это плюс, но есть вложенный делегат - это минус.Замыкание остается тем же, но дополнительная аллокация на делегат появится.<br /><br />Правда сам ConcurrentDictionary дает достаточно большой overhead. Мы в своем проекте были вынуждены от него отказаться в некоторых местах в пользу кастомного решения, поскольку overhead его составлял сотни мегабайт памяти в определенных сценариях. Но у нас сильно нетипичная задача.<br /><br />Да, при этом в других местах у нас используется подобный трюк, поскольку накладные расходы на лишнюю длительную операцию выше, чем дополнительные расходы на аллокацию одного делегата.Sergey Teplyakovhttps://www.blogger.com/profile/14300835272589262297noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-84890240080268855212015-06-18T17:44:15.013+03:002015-06-18T17:44:15.013+03:00Я ссылался на приведенный выше ко. RunOperstionOrG...Я ссылался на приведенный выше ко. RunOperstionOrGetFromCache. А можете привести кож реального GetOrAdd или описание такого поведения. Beathttps://www.blogger.com/profile/15602820637577186548noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-16296342335974375682015-06-18T17:39:52.664+03:002015-06-18T17:39:52.664+03:00> Тут надо оговорится, что такой хак срабатывае...> Тут надо оговорится, что такой хак срабатывает, так как по умолчанию при создании Lazy используется LazyThreadSafetyMode.ExecutionAndPublication настройка.<br />Да, это валидное замечание.<br /><br />> Если бы ConcurrentDictionary поддерживал LazyThreadSafetyMode или что-то подобное, то хак не потребовался бы.<br /><br />Пока не представляю, каким образом это можно решить без потери производительности на стороне ConcurentDictionary. Он так ведет себя by design, наличие блокировок на его уровне сделает его менее эффективным для других сценариев.<br />Sergey Teplyakovhttps://www.blogger.com/profile/14300835272589262297noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-34050006797877556362015-06-18T17:38:20.150+03:002015-06-18T17:38:20.150+03:00@Beat несколько агрессивно и нифига не понятно.
П...@Beat несколько агрессивно и нифига не понятно. <br />Приведенный пример во втором комментарии откуда взят? Я им объяснял как работает GetOrAdd.<br /><br />Теперь по поводу первого коммента: Алексей ответил совершенно правильно, сколько бы потоков не вызывали GetOrAdd ConcurrentDictionary гарантирует, что все они получат *одну и ту же ссылку*. Это значит, что в исходном примере без Lazy (проще понимать), будет запущено N операций, в кэш добавлена первая, а метод TryAdd внутри метода GetOrAdd снова проверит, а нет ли уже такого значения в кэше, и если есть, то вернет существующее значение, а новое выбросит.Sergey Teplyakovhttps://www.blogger.com/profile/14300835272589262297noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-25294347554946010612015-06-18T17:35:15.211+03:002015-06-18T17:35:15.211+03:00Если два потока попросят значение, то первый запус...Если два потока попросят значение, то первый запустит операцию, а второй будет ждать ее заверершения. Но, с другой стороны, ведь и без этого хака оба потока бы синхронно сидели бы и ждали окончания длительной операции. Но если она CPU-Intensive, то в старом случае мы бы тратили ресурсы на ее исполнение, а в новом - просто ждали бы результата, висев на синхронизации в объекте Lazy.Value.Sergey Teplyakovhttps://www.blogger.com/profile/14300835272589262297noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-64499558123929683402015-06-18T17:07:45.557+03:002015-06-18T17:07:45.557+03:00А внимательно посмотреть на код не судьба?
resul...А внимательно посмотреть на код не судьба?<br /><br /> result = RunLongRunningOperation(operationId);<br /> _cache.TryAdd(operationId, result);<br /> return result;<br /><br />result добавится единожды, но то, что вернет GetOrAdd будет ИПСПОЛЬЗОВАНО, а соответвенно вызвана длительная операция.Beathttps://www.blogger.com/profile/15602820637577186548noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-67212468917399786852015-06-18T17:04:50.180+03:002015-06-18T17:04:50.180+03:00Решение интересное, но есть вопрос к memory traffi...Решение интересное, но есть вопрос к memory traffik. (После .NEXT стараюсь замечать такие моменты :))<br /><br />На каждый элемент коллекции, дополнительно, создаются 3 объекта. <br /> Сам Lazy; <br /> Делегат, оборачивающий лямбду;<br /> Объект для замыкания по <b>id</b> для лямбды () => RunLongRunningOperation(<b>id</b>)<br /> <br />Получается не все гладко при большом размере такой коллекции.Vasiliyhttps://www.blogger.com/profile/00334336715354812316noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-87386328655351158362015-06-18T16:33:57.258+03:002015-06-18T16:33:57.258+03:00Тут надо оговорится, что такой хак срабатывает, та...Тут надо оговорится, что такой хак срабатывает, так как по умолчанию при создании Lazy используется LazyThreadSafetyMode.ExecutionAndPublication настройка.<br />Она определяет как будет инициализироваться Lazy конкурентно или с блокировкой.<br /><br />Если бы ConcurrentDictionary поддерживал LazyThreadSafetyMode или что-то подобное, то хак не потребовался бы.<br /><br />PS: Есть повод отписать тикет ребятам из BCL :)Vasiliyhttps://www.blogger.com/profile/00334336715354812316noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-55229436108194280382015-06-18T13:11:27.539+03:002015-06-18T13:11:27.539+03:00Оба потока быстро получат ссылку на один и тот же ...Оба потока быстро получат ссылку на один и тот же Lazy-объект и уже именно экземпляр Lazy<> будет блокировать вызывающие потоки, заставляя всех ждать одно и то же значение.Oleksiy Davidichhttps://www.blogger.com/profile/06798005643408616225noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-16690245611773777252015-06-18T12:53:34.897+03:002015-06-18T12:53:34.897+03:00Создание РАЗНЫХ lazy-объектов произойдёт, но возвр...Создание РАЗНЫХ lazy-объектов произойдёт, но возвращён будет всё равно ОДИН и тот же экземпляр, а новые lazy-объекты будут утеряны (вместо потери результатов долгой операции).Oleksiy Davidichhttps://www.blogger.com/profile/06798005643408616225noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-58089704855098370292015-06-18T12:32:53.494+03:002015-06-18T12:32:53.494+03:00Здесь также возможен двойной/тройной/etc вызов дли...Здесь также возможен двойной/тройной/etc вызов длительной операции, т.к. в случае _одновременного_ вызова GetOrAdd несколькими потоками каждый из них получит свой экземпляр lazy-объекта. Конечно, вероятность этого многократно ниже чем в классическом варианте.Beathttps://www.blogger.com/profile/15602820637577186548noreply@blogger.comtag:blogger.com,1999:blog-8596733192274108952.post-91900661509826411692015-06-18T09:42:54.279+03:002015-06-18T09:42:54.279+03:00Элегантно.
Т.е. технически мы вызвали конструктор,...Элегантно.<br />Т.е. технически мы вызвали конструктор, объект зашел в коллекцию (что отработает быстро) и только потом начнется выполнение RunLongRunningOperation при обращении к Value. Ок, а что будет если два потока попросят значение из кеша? Первый запустит RunLongRunningOperation, а второй будет ждать, пока отработает первый, чтобы тоже получить закешированное значение? Ведь первый поток все еще ждет, пока ему вернется значение. Можно чуть поподробнее про такой кейс?Anton Norkohttps://www.blogger.com/profile/11409713140969211320noreply@blogger.com