Pull to refresh
0
2.7
Виктор Поморцев @SpiderEkb

Консультант направления по разработке

Send message

Я умею "неабстрактно рассуждать" когда вижу конкретную постановку конкретной задачи и понимаю типовые сценарии использования того, что предстоит разработать

Здесь все предельно понятно: если у вас в одном объекте два мутекса и N полей, для К из которых нужно захватывать первый мутекс, а для остальных M полей -- второй мутекс, то высока вероятность ошибиться.

Глубочайшая мысль. Несомненно, требующая отдельной статьи.

Вопрос - там действительно надо делать именно так? Вот я о чем спрашиваю. Иных вариантов нет? Те же lock-free алгоритмы там не подходят?

Когда вижу подобные вещи, первая мысль - а нельзя ли без этого обойтись? Это действительно критично? Потому что тут мало что ошибиться легко, так еще и оно может работать на умеренной нагрузке, а с повышением начнутся гонки и производительность начнет падать. Т.е. вот такой вариант синхронизации - это уже крайний случай.

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

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

А вокруг выдернутого из контекста куска кода можно рассуждать только абстрактно. И до бесконечности оптимизировать то, что (возможно!) можно вообще в данном случае реализовать совсем иначе - проще и эффективнее.

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

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

Вот конкретно сейчас в работе. Есть две БД. Одна содержит в пределе до 250млн строк, вторая (опять в пределе) до 1млн строк. Задача - найти "совпадения" по строкам из первой и второй БД. Совпадением считается вхождение всех уникальных элементов (слов) строки из второй БД в строку из первой БД. Т.е. строка из второй БД может быть короче строки в первой и порядок следования элементов там может отличаться. Например:

Строка 1: 'A B C C D E E'
Строка 2: 'E B C B'

Это совпадение т.к. все уникальные элементы 'B', 'C' и 'E' строки 2 содержатся в строке 1.

Каждое совпадение должно быть зафиксировано в таблице. У каждой строки в БД (1 и 2) есть некий идентификатор. В таблице нужно фиксировать что строка из БД2 с идентификатором ... совпадает со строкой из БД1 с идентификатором ...

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

Если просто в один поток - работать будет сутками (если что - 120 8-поточных ядер Power9 - не хрен собачий, извиняюсь). Так что распараллеливаем на несколько потоков (обычно от 5-ти до 10-ти - полностью загрузить сервер своей задачей никто не даст - там еще 100500 разных процессов крутится плюс загрузка сервера в нормальном режиме не должна превышать 50-60% т.к. бывают периоды пиковой нагрузки когда она доходит до 90% - должен быть запас).

Решается это распараллеливанием обработки. Есть головное задание - оно производит отбор элементов (идентификаторов строк), например из БД2, по заданным условиям (там не всегда надо отбирать все). Отобранные идентификаторы объединяются в пакеты (скажем, по 100 штук) и выкладываются в очередь для обработки. Параллельно головному заданию (мы работаем именно с заданиями - job - это удобнее для сопровождения и безопаснее - задания полностью изолированы и падение одного даже по самой страшной ошибке не затрагивают остальные) работает 5-10 задний-обработчиков. Обработчик берет из очереди очередной пакет и обрабатывает содержащиеся в нем элементы. После чего берет следующий и так пока очередь не опустеет.

Все время тут определяется временем обработки элемента. Все упирается в него. Затраты времени на транспорт вообще никак не влияют - будет это 1мкс или 1нс - не изменит ничего если время обработки одного пакета исчисляется десятками мс.

В качестве транспортной очереди используется то, что предоставляет система. Это может быть, например, pipe. Или, на нашей платформе, есть системный объект user queue простая в работе и быстрая очередь. Все блокировки чтение-запись, удаление прочитанного элемента - за все это отвечает система. На нашей стороне фактически две операции - enqueue (положить) и dequeue (взять). Все. Никаких мьютексов и прочего.

Если начать упираться в транспорт через расшаренную память - да, транспорт станет быстрее. Но на это потратится изрядно сил, а общий прирост производительности составит 0.000001%

Второй вариант, с которым пришлось плотно работать - коммуникации "многие-ко-многим". Есть несколько десятков промконтроллеров и десяток (в пределе, чаще 2-3) "интерфейсных клиентов". Нужно реализовать обмент данными - от контроллеров идут "сигналы" к клиентам, от клиентов к контроллерам "команды". Любая посылка по дороге неким образом обрабатывается (в частности, контроллеры работают с физическими адресами устройств, клиенты - с их логическими идентификаторами). Ну и еще некоторая логика, связанная с состоянием контроллеров, обработкой всяких ситуаций типа контроллер начал посылать дубли и т.п.

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

Плохо что коммуникационные таймауты очень маленькие. Нужно быстро проверить полученное сообщение (формат совпал, CRC совпало) и отправить ответ что сообщение принято.

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

Тут тоже скорость обмена данными между потоками не является критичной. Не настолько чтобы заниматься расшариванеим памяти и всем вот этим вот. Поэтому была реализована система "почтовых ящиков". На винде это mailslot, в других системах - локальный именованный Unix Socket. И опять - вся синхронизация отдается системе. У каждого потока (или процесса) свой ящик, остальные, кому надо что-то передать, просто бросают туда "посылочку" - датаграмму. Получается конвейер - получил пакет, быстро провалидировал, отправил подтверждение, пакет уходит в другой поток на обработку. Обработался - уходит в третий на отправку. Практика показала что все это стабильно работает в тех объемах нагрузки, которые у нас были в реальной жизни. И при этом достаточно экономно по ресурсам (компы в диспетчерских были очень простенькие).

Если бы сейчас реализовывал подобное на той платформе, на которой работаю - сделал бы это на той же user queue - там есть возможность каждому пакету добавлять "ключ". Получилась бы единая "шина данных с адресами получателя" - каждый берет то, что имеет соответствующий, "его", ключ. Но там, с чем работал того, таких вещей нет, поэтому "почтовые ящики".

Были бы иные задачи - были бы иные подходы. Любое решение всегда идет "от задачи" и ее граничных условий. Первое что смотрим - где узкие места, требующие проработки и в первую очередь решаем именно эту проблему. Упираться и тратить 80% времени для повышение производительности в 0.1% в подавляющем большинстве случаев слишком большая роскошь (и да, бывают исключения).

Для прикладника важно чтобы конечный продукт работал максимально быстро и эффективно. И при этом требовал минимум времени на разработку и отладку.

Т.е. прикладник всегда должен видеть узкие места - где минимальными усилиями получить максимальный выигрыш в производительности. По всей задаче в целом.

Ну вообще-то так работает любая БД. Если вы открыли транзакцию на запись, то остальные не увидят ваших изменений до тех пор, пока вы не сделаете коммит. Правда, commitment control дале лишнюю нагрузку на сервер и снижает произвидительность.

Но тут речь идет о работе с памятью. Или вы про in-memory DB? Так есть альтернативы в виде lock-free алгоритмов. Те же конкурентные очереди, деревья, списки...

Как пример

Вопрос всегда в том - для вашей конкретной задачи это действительно нужно? Скорость доступа к данным для вас действительно является узким местом?

В моей практике таких задач было по пальцам пересчитать (за 30+ лет). В подавляющем большинстве случаев необходимость многопоточки была связана с большими затратам времени на обработку данных. Т.е. ускорив доступ к шаренным данным в 2-3 раза, я получу суммарный выигрыш в производительности всей системы в доли процента. Тут просто овчинка выделки не стоит - берем любой системный канал связи (пайпы, сокеты и т.п.) и легко и быстро реализуем на нем обмен данными между потоками/процессами. А конкурентность доступа уже обеспечивается системой.

Попытки рассуждать вслух вокруг да около.

А вы все задачи решаете однотипно? Без учета конкретики и граничных условий? Все копипастой старых решений? И никогда "на берегу" не задумываетесь "а что будет если..."?

Да куда уж мне, я и программировать-то не умею.

У меня нет повода сомневаться в вашем уровне. Но программирование и разработка, все-таки, немножко разные вещи. Можно наизусть знать последний стандарт С++, все популярные библиотеки на уровне исходников, но при этом не уметь понять поставленную задачу во всех тонкостях, со всеми граничными условиями и типовыми сценариями использования создаваемого продукта. И, как следствие, выбрать не самое эффективное и стабильное решение в пользу более привычного и знакомого просто потому что "всегда так делал".

К сожалению, навидался за 30+ лет примеров использования таких вот "привычных решений" там, где они не являются оптимальными. Порой это приводило к необходимости все переделывать заново. Нежелание подумать заранее приводит к приобретению нежелательного опыта.

Поэтому в любой задаче, после первого решения всегда мысль - а можно это сделать иначе? А какие будут плюсы и минусы? Потому что "опыт" - это совсем не знание "как надо" (этому в школе учат), но знание как не дало в каждом конкретном случае.

И в данном конкретном случае (этой конкретной статьи) можно только абстрактно рассуждать. Потому нет понимания все конкретики задачи и все ее граничных условий. Обсуждать конкретное решение можно только имея перед глазами четкую постановку и зная архитектуру всей системы целиком. Может там все так, что вот это все вообще напрочь не вперлось и надо как-то иначе все делать. Всегда есть варианты.

Вот приходит человек - "нужно сделать то-то, можно так, можно этак - как лучше?". И вот тут зависит от условий. Если это отдельный процесс, то есть одно решение - более простое в реализации и более эффективное. Но в случае актора, вызываемого одновременно из 100500 мест 100млн раз в стуки это решение потащит за собой много накладных расходов от системы и не даст стабильной производительности. Поэтому лучше выбрать другое, которое чуть сложнее в реализации и менее производительное в одном потоке, но зато стабильное в условиях большой плотности параллельных вызовов. Вот то, что я хочу услышать от опытного разработчика. А не умение решать ликодовские задачки и зачитывания наизусть произвольного места из последнего стандарта языка или перечисления всех классов стандартной библиотеки.

Что именно вам кажется ерундой? Контроль над данными с которыми осуществляется совместная работа несколькими потоками? Идея изоляции шареных данных?

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

Я уже писал тут, что в зависимости от задачи можно выбирать иные подходы. И многопоточная обработка не всегда требует работы с одним массивом шареных данных. А если и требует - см. любую операционную систему где 10 программ могут работать с одним файлом, но все делают это через системное API (все ваши read/writeв конечном итоге приходят в одну точку в ядре системы). И все проблемы конкурентного доступа и блокировок решаются в одном месте - на уровне ОС. А системное API отдает в программы уже безопасную копию данных в их текущем состоянии.

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

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

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

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

Возвращаясь к теме геттеров. Если ваш геттер вернул вам указатель на шаренные денные, он должен его "залочить" - никому другому он больше этот указатель возвращать не должен. До тек пор, пока вы этот лок не снимете. И это опять возврат к механизму мьютексов. А если вы сняли лок и продолжаете пользоваться указателем - ну что ж... Вы сознательно (или по недомыслию) стреляете себе в ногу.

И все равно, когда потоков становится не 2, а 5, а мьютексов не 3, а 10, вы все равно рискуете за всем не уследить, а дальше чем выше плотность обращений к данным, тем выше вероятность попадания в дедлок. И тут возникает необходимость реализации механизма таймаутов доступа и разрешения коллизий. И вероятность провалов производительности на ровном месте. Что не всегда допустимо по условиям задачи (там может быть условие не столько быстродействия, сколько гарантированного времени реакции и отсутствие фризов).

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

И да, это в ущерб производительности. Но тут или так или этак - в каждом конкретном случае выбираем то, что более критично для данной задачи.

Я бы (в каких-то ситуациях) делал бы вообще так - есть данные, не шареные, и к ним есть геттер-сеттер. Все это работает в отдельном потоке. Все остальные потоки обращаются к данным через единую точку - геттер-сеттер. Который уже сам разруливает конкурентность доступов. Получается что-то типа критической секции.

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

Т.е. можно будет относительно безболезненно отстреливать приболевшие части сервиса в виде его отдельных процессов, не пытаясь починить их погулявше поврежденные heap/stack и прочие local state.

Вот поэтому мне решение с параллельными процессами (заданиями - job) нравится больше чем решение с нитями-потоками (thread). Оно легче сопровождается и более устойчиво к сбоям в силу изолированности отдельного задания относительно всех остальных.

Ну если работаем в рамках одного процесса и нескольких нитей, то почему бы не критические секции?

Если в рамках нескольких процессов и расшареной памяти - то да, только системные объекты синхронизации (мьютексы, может быть где-то как-то семафоры)

Если все(!) потоки обращаются к данным только на чтение

Не совсем понятно тогда как это работает? Есть все только читают, то вообще никаких проблем нет.

А если чтение-запись, то, мне казалось, это решается через critical sections скорее, нежели через мьютексы...

Согласен. Посему лучше стараться выбирать те подходы, которые в принципе не предусматривают таких ситуаций. Если задача позволяет, конечно. Потому, как уже писал, стараюсь без нужды в такие блуды не лезть (в тех задачах, что приходится решать, в 99% случаев получается выбрать иные подходы).

Пример, кстати, называется "deadlock" - взаимная блокировка когда все по кругу ждут друг друга. Но для этого нужно несколько мутексов и несколько потоков с ними работающих. В общем случае там нужны таймауты на ожидание и механизм разрешения коллизий (как это делается в сетевых протоколах с захватом линии).

Да, верное решение. Геттер/сеттер которые работают с данными, защищенными мьютексом.
Как это будет реализовано уже не суть важно - класс, лямбда... Важно что вы не обращаетесь к данным напрямую, только через get/set, а те уже внутри используют мьютекс.

Ну тут что есть под рукой :-) Пайпы в принципе очень просты в работе и не грузят систему.

Но у нас на платформе есть User Queue - системный объект (т.е. никаких библиотек - поддерживается системными средствами). Преимущество в том, что его не надо каждый раз создавать-удалять. Один раз при развертывании поставки создал (с нужным именем) и оно есть. Только подключайся. Оно может быть FIFO, LIFO или KEYED - когда каждый пакет еще дополнительно снабжается "ключом" и можно этот ключ использовать в качестве условия для извлечения сообщения (равно, не равно, больше, меньше, больше или равно, меньше или равно) - извлекается первое сообщение, подходящее под условие. Основное преимущество перед пайпом - есть возможность "материализации" - получения состояния очереди (в т.ч. максимально возможное количество сообщений и текущее количество сообщений) что позволяет контролировать скорость раздачи и разбора и динамически балансировать систему (если очередь растет - добавить обработчик, если уменьшается - остановить какой-то из обработчиков). А поскольку это системный объект, то даже в случае падения задания (головного, обработчиков) содержимое очереди сохраняется в памяти системы.

Есть еще Data Queue - примерно тоже самое, но более тяжелая за счет того, что хранит все содержимое свое на диске.

И та и другая очереди доступны как через API, так и через SQL

Хотим посмотреть информацию об очерелди

select *
  from table(USER_QUEUE_INFO('TSTQUE'));

Получаем

Хотим посомтреть содержимое (без удаления из очереди - "материализация сообщений", peek)

select *
  from table(USER_QUEUE_ENTRIES('TSTQUE'));

Получаем

Для сопровождения очень полезно

Это так. У меня жена профессиональный переводчик. И много времени жила "там" (США, Канада, Британия) по работе. И свободно говорит на том языке, на котором думает. И думает на том языке, в какой среде находится. До смешного - иногда сначала в голове формируется на английском (потому что именно эта мысль на английском точнее и лаконичнее), потом уже на русском ("я знаю как это по-английски сказать, но что-то сходу в голову не приходит как на русском адекватно сформулировать").

Язык - это не только слова, это образ мысли. Убогие мысли - убогий язык. Неумение четко сформулировать мысль выражается в "э-э-э..." в языке.

И языки разные. Русский - описательный. Много крутится вокруг прилагательных. Английский - язык действия. все вокруг глаголов - отсюда множество времен и "отглагольных" частей речи. И чтобы научится им свободно владеть, надо прежде всего научиться думать именно в этом ключе.

Я не зря сравнил с разработчиками - там ровно тоже самое. Когда обдумываешь задачу, в голове сразу возникает алгоритм и его реализация доступными средствами. И передать все это словами часто бывает долго и сложно - дольше и сложнее, чем написать кодом. Потому что в голове именно код, а не слова.

Из личной практики. Очень часто мне не требуется именно работа с какими-то общими данными из нескольких потоков. Чаще задачи двух классов -

  • "конвейерная обработка потока данных"

  • "параллельная обработка большого количества независимых элементов".

И в подавляющем большинстве случаев скорость транспорта данных между потоками ("поток" в данном случае понимается в широком смысле - это может быть как нить (thread), так и отдельный процесс или изолированное задание (job)) не является узким местом, все время уходит на обработку данных.

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

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

Параллельная обработка - когда есть много (десятки миллионов и более) однотипных элементов и каждый из них нужно независимо от остальных обработать по какому-то алгоритму (может быть достаточно сложный и долгий). Тут подход иной - есть "головное задание", которое формирует пакеты элементов (например, делает выборку из БД по заданным условием, результаты выборки объединяются в пакет, скажем, по 100 элементов) и выкладывает их в очередь. Параллельно работают несколько экземпляров обработчиков, каждый из которых берет из очереди очередной пакет и обрабатывает содержащиеся в нем элементы.

В обоих случаях нет нужды связываться с разделяемой памятью и синхронизацией доступа к ней. Можно воспользоваться системными средствами (и за всю синхронизацию будет отвечать система). Для конвейерной обработки используется принцип "почтовых ящиков" - у каждого потока есть свой ящик (в Windows можно использовать mailslot, в иных системах - локальный именованный Unix socket) куда любой может писать блоки данных для этого потока.

При параллельной обработке можно использовать pipe в который головное задание пишет пакеты, а обработчики читают их оттуда. Ну или если истема поддерживатье что-то еще подходящее (сейчас вот с IBM i работаю - там есть очереди - data queue и user queue - ккк раз для такого удобно).

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

Свободно говорить на языке вы начнете только после того, как начнете на нем думать. Причем, думать именно "на языке", а на "иностранными словами" (это две большее разницы - разное строение языка, например, русский язык более "описательный", английский - "язык действия"). А не переводить в голове каждую фразу прежде чем ее сказать.

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

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

Т.е. тут работает точно тот же механизм, но с некоторой задержкой. За счет которой можно надувать пузыри, но они рано-поздно лопаются.

Да ну? А в заголовке вижу про "FAR - центр вселенной".

Любите понятия подменять? Придумать якобы аргумент оппонента и потом победоносно его опровергнуть? Ну тоже такой прием с дискуссиях есть, бывает...

И еще раз - почему именно FAR (а не DC, TC и т.п.) лучше чем cd+ls? Потому что вы другого не пробовали?

Т.е. разумных аргументов нет?

Еще раз. Почему именно FAR? Почему не любой другой двухпанельный ФМ? В чем преимущество именно FAR перед остальными?

Во всей ветке из без малого 500 камментов я вижу лишь два -

  • работа с консолью

  • (субъективно) больше нравится текстовый интерфейс

Все. Остальные упираются в "FAR лучше чем cd+ls" или "FAR лучше стандартного проводника".

Так кто тут тупит?

1
23 ...

Information

Rating
1,049-th
Location
Екатеринбург, Свердловская обл., Россия
Works in
Date of birth
Registered
Activity