>Да, я гораздо терпимее к тому, что вам не нравится, и терминов типа «говнокод» не использую.
Приведите мне, где я использовал «говнокод»? Не надо приписывать мне свои фантазии.
По вашему тексту создается ощущение, что как раз к плохому коду вы более терпимы, чем к хорошему. По вашему описанию я вижу, что вы готовы писать так себе качества код просто потому, что нет причин писать его лучше. Мне же кажется, что нужны веские причины не писать код хорошего качества. Да, это разные приоритеты. Я готов идти на компромиссы когда есть веская причина — но я не готов идти на компромиссы просто по традиции. И я считаю полезным разделять компромиссное решение от чистого решения — даже если реализовано было компромиссное.
Скажите, а вам действительно есть что с чем сравнивать? Т.е. я-то много работал с кодом, написанным в вашем стиле — вы совершенно правы, такого кода полным-полно даже в очень уважаемых библиотеках. А вам довелось поработать с кодом, написанным в «моем» стиле?
> потому что работу делают, а не маятся чесотошным перфекционизмом.
Некоторые люди считают тесты излишеством — потому что работу надо делать, а не маяться перфекционизмом. Действительно, зачем мне способы убедиться, что я написал правильный код? Все сомнения — это только от неуверенности в себе.
А ведь тесты гораздо дороже в написании и поддержке, чем прямо в текущей точке кода понять, что я закладываюсь на такие-то свойства аргументов/текущего состояния, и вставить прямо сюда checkArgument/checkState. Но написание тестов сейчас является общепринятой практикой. Не наводит ли это вас на мысль, что то, что вы называли чесоточным перфекционизмом, на самом деле является просто более зрелым профессионализмом? ;)
>Отвечу на 3., если вам ещё не понятна моя позиция: нет, не странно. Логика выбора NullPointerException — очень простая и прямолинейная, и следует из названия — ошибка вследствие попытки использования нулевого указателя.
В использовании нулевого указателя нет никакой ошибки. Я могу использовать его так: ref != null — это разве ошибка? Я могу использовать так: HashMap.put(key, null) — здесь тоже не будет ошибки. И даже так: HashMap.put(null, null) — это определенно использование нулевого указателя, но оно не является ошибкой. Дело не в том, что указатель нулевой — дело в том, что для вашего метода такой аргумент недопустим. Об этом надо и говорить — «аргумент недопустим». А вы говорите — «указатель нулевой». Ну да, он нулевой — и что с того?
Пример: вы отправили документы на рассмотрение. Вам их возвращают с комментарием. Какой комментарий более ясен: «документ заполнен черной ручкой». или «неверное заполнение: документ нельзя заполнять черной ручкой»?
> Если последняя возникает, то врядли на пути распространения её последствий окажется нужная проверка.
Почему, собственно, вряд ли? Если у вас хотя бы половина методов проверяет свои аргументы — статистически, ошибка уйдет не далее, чем на пару вызовов от места ее возникновения. В моем реальном мире заставить половину методов делать хотя бы простейшие проверки входных данных/согласованности внутри алгоритма — вполне выполнимая задача. В моем опыте это сильно упрощает локализацию ошибок — как раз потому, что ошибка теперь совсем недалеко уходит от точки своего возникновения, и не успевает испортить какие-то важные данные. В моем опыте мне важно знать, что разработчик метода осознанно бросает IAE, избегая работы с некорректными данными — а не случайно бросает NPE, разыменовывая указатель где-то, возможно, уже после того, как он использовал этот указатель чтобы испортить какие-то разделяемые данные. В моем опыте assert используется только в performance-critical участках, где лишняя проверка может что-то измеримо замедлить — и мой опыт говорит мне, что таких участков очень немного даже в высокопроизводительном коде.
Это вот мой опыт, и мой реальный мир. Видимо, ваш опыт и ваш мир — другой.
Во-первых, заменять NPE на IAE нужно во всем коде classlib — иначе будет несогласованное поведение. Представляете себе масштаб изменений? И ради чего — ради чуть более логичного поведения? Не убедили.
Во-вторых — я терпеть не могу эти вот визги насчет строк на разделенном буфере. Люди, которые об этом говорят — мне кажется, они вообще головой не думают. Изменения в производительности, в алгоритмах GC — они происходят в каждом релизе VM. И хотя в среднем они производительность улучшают, конкретно для вашего кода что-то может и ухудшится. Чем это отличается от ситуации со строками? Контракт метода остался тем же — вы получаете в точности ту же подстроку, что и раньше. Детали реализации изменились? — а вы задумывались когда-нибудь, сколько деталей реализации изменилось под капотом, пока вы просто не видите? Хотите совсем постоянства — сидите на одной версии VM.
>А ведь кто-то мог закладываться на скорость substring и на расход памяти при использовании разделённого буфера.
Вы таких людей знаете лично? Я вот сталкивался со string-intensive приложениями — они используют (сюрприз!) свои «строки» и свои механизмы их переиспользования.
>причина наверняка не там, где это исключение возникло, и даже не в стеке вызова — она где-то выше по коду.
Абсолютно то же самое можно сказать о любом невалидном значении параметра — оно возникло где-то выше по коду. Но IAE вам точно говорит, что это именно параметр невалиден. А NPE как раз может возникать при вполне валидных параметрах — из-за ошибки в самом коде функции.
>то да, использую StackOverflowError.
И вас не будет смущать то, что StackOverflowError extends VirtualMachineError?
Насчет пункта 3 вы не ответили — правильно ли я понял, что вы отдельно бросите NPE если параметр null, и отдельно IAE, если параметр некорректен по другим соображениям?
>А вообще предпочитаю использовать assert param != null
То есть вам комфортно с тем, что в debug ваш код будет бросать AssertionError, а в продакшене NPE — возможно, уже после того, как он испортит реальные данные? Действительно, кому нужны эти реальные данные… Вот отладочные данные должны быть защищены, это да.
>Я считаю использование NPE логичным и полезным для ситуации с null. И плевать, если кто-то считает это неправильным (логично, но неправильно — это вообще как?).
По-моему, это вы сами для себя решили, что NPE бросать логично. Логика бросания IAE в один шаг: получил null -> null не подходит -> throw НедопустимыйАргумент. Логика бросания NPE: получил null -> скорее всего придется его дальше разыменовывать -> получим NPE -> надо кинуть NPE пораньше -> throw NPE. А потом еще программист, читая логи, должен понять, что «NPE» означает, что «null является недопустимым аргументом у этого метода».
Лично для меня странно не то, что вы выбираете использовать в этой ситуации NPE, сколь важно то, как вы его выбираете — вы говорите «я всегда так делал, и думать над другим я не хочу — это слишком сложно для меня». На мой взгляд, для инженера такой способ выбора категорически не приемлем. Условно говоря, если бы этот диалог случился на собеседовании — я бы вас не рекомендовал бы брать. Не потому, что вы используете соглашения отличные от моих, а именно потому, что вы толком не можете их обосновать, а на подсказываемые мной проблемы закрываете глаза.
Во-первых, увы, но стандартная библиотека все-таки не скрижали завета. Ее авторы тоже люди, и могут делать ошибки. И эти ошибки — в отличие от ваших и моих — очень сложно исправлять, ибо обратная совместимость же. Если однажды было в контракте метода, что null -> NPE, значит кто-то мог на это заложиться — все, хана, фиг вы так просто теперь протащите изменение контракта. Вон, кто-то из корифеев явы давно уже считает, и прямо об этом пишет, что проверяемые исключения были ошибкой, надо было обойтись непроверяемыми — и что, стандартную библиотеку бросились переписывать?
Во-вторых, стандартная библиотека на то и стандартная, что ее вызовы зачастую воспринимаются чуть ли не как примитивы языка. То есть вызов метода classlib где-то близок по низкоуровневости операции к разыменованию ссылки. И поэтому получить оттуда NPE может быть естественнее, чем IAE.
Короче говоря, common practices это штука хорошая, но и своей головой думать тоже можно.
Хорошо, давайте посмотрим на ситуацию с разных сторон:
1. Абстракция. Вызов метода — это пересечение границ абстракции. Чем меньше вы как клиент знаете о деталях реализации абстракции — тем лучше. Предположение, что код метода разыменовывает переданный указатель — это, очевидно, предположение о реализации. Конечно, это очень логичное предположение, но если его можно не делать — зачем его делать?
2. Тестирование. Если в контракте метода написано, что значение null приводит к NPE, то должен же быть тест, который это проверяет, верно? Ок, он у вас есть. Но новый джуниор в вашей команде изменил реализацию метода — проверки в начале больше нет, аргумент сразу передается в DB. Но тест проходит — потому что после того, как некорректная запись попадает в DB он-таки ссылку разыменовывает, и искомый NPE таки вылетает. То есть вы не можете реально понять, NPE вылетает ожидаемо, или случайно (возможно, оставляя неустранимые побочные эффекты)
3. Несогласованность. У вас есть метод, который принимает строку. Строка должна быть не-null, и не пустая. Не странно ли, что в первом случае клиентский код получит NPE, а во втором — IAE?
4. Аналогия. У вас есть веб-страница с полями ввода. Какое действие более логично для ситуации, когда введены ошибочные значения — 500 Internal Server Error, или нормальная страница с описанием ошибки?
5. Еще одна аналогия. Пусть у вас есть рекурсивный алгоритм, и вам известно заранее, что при каких-то параметрах вам стека точно не хватит (скажем, глубина рекурсии растет экспоненциально). Будете ли вы бросать StackOverflowException заранее, когда только обнаружите, что параметры попадают в этот диапазон?
Что касается
>NPE в логе гораздо информативнее IAE, особенно если это IAE не содержит толкового объяснения что не так.
То простите меня — за выброс исключений без вменяемого сообщения в 99% случаев надо сразу программисту руки отрывать по локоть. Потому что количество времени, которое могло бы быть сэкономлено при отладке одним-единственным таким сообщением — может измеряться днями. И как раз гораздо вероятнее, что без сообщения выбросят NPE — по вашей же аналогии, что когда вы ссылку-то разыменовываете, NPЕ же бросается безо всякого сообощения.
NullPointerException — это исключение, которое означает, что программа разыменовала нулевой указатель. Т.е. программист не предусмотрел, что указатель будет нулевым.
А IllegalArgumentException — означает, что передаваемый аргумент функция считает некорректным. Т.е программист предполагал, что в метод могут передать всякое гавно, и застраховался от этого.
Другими словами: NPE означает, что ошибка в коде самого метода (недоопределенная семантика для null-аргументов), а IAE — что ошибка в вызывающем коде, который нарушает контракт вызова метода.
Мне кажется, это все же разные вещи. Мартин, конечно, тот еще волюнтарист и безбожник, но .lazySet() — это не store, это store:memory_order_release, и замена memory_order_seq_con на release в случае single writer вполне легальна. Да, строгая семантика .lazySet не прописана в JMM, но она такая потому что именно для этого lazySet и вводился. А что JMM уже который год обновить политической воли нет — так это грустно, конечно, но что ж теперь, сидеть и куковать? То есть lazySet — это инструмент, предназначенный для того, для чего его использует Мартин, но формально недоспецифицированный.
В случае с park/unpark же ситуация иная — их назначение это взаимодействие с системным планировщиком задач. То, что у них есть еще какая-то memory visibility семантика — это деталь реализации, к их назначению никакого отношения не имеющая, в общем-то — случайность. Закладываться на нее может только тот, кто имеет возможность держать руку на пульсе деталей реализации classlib на разных платформах, и вмешиваться в принимаемые по ним решения. То есть Даг Ли :)
Когда Даг пишет SequenceLock на неспецифицированных особенностях Unsafe — я апплодирую, потому что для меня появляется отличный примитив синхронизации, и это ответственность Дага сменить его реализацию, если у нужных методов Unsafe сменится семантика. Когда я делаю то же самое — это моя ответственность. Вот только моим мнением при этих изменениях вряд ли кто поинтересуется, и вряд ли меня предупредят о них — в отличие от Дага.
Преимущества в том, что Future «рекомендованный» способ работы с результатами асинхронных операций в яве (хотя лично я от него не сильно в восторге, но рекомендованный — значит рекомендованный, стандарты все же много значат).
То есть вам, по-сути, предлагают двухэтапный способ решения вашей задачи: на первом этапе вы оборачиваете Callback-based интерфейс библиотеки в нечто, предоставляющее взамен Future-based интерфейс. А Future уже можно использовать и для синхронного вычисления — просто вызывать .get(). Но можно и для асинхронного — есть методы типа .get(timeout), и прочее.
Но вы правы в том, что реализовывать обертку Callback -> Future действительно не фунт изюма. В качестве упражнения на способность делать непростые но корректные и эффективные реализации многопоточных примитивов — подойдет. Для однократного решения вашей задачи — я бы не стал возиться.
Разве park()/unpark() имеют какую-то memory visibility semantics? В javadoc я такого не вижу. Понятно, что в реализации скорее всего такая семантика есть неявно, но раз она в спецификации не прописана, то этот код не корректен для написания кем угодно, кроме Дага Ли.
Про многопоточность в яве у меня целый блог, который я начал вести задолго до того, как мне дали сюда инвайт. Написать отдельный цикл статей на хабр по модели памяти хочу давно, но написать в личный блог небольшую статью по той теме, что меня прямо сейчас интересует гораздо естественнее, чем поднимать большой пласт информации, и пытаться его как-то понятно организовать. Руки пока не доходят.
Кроме того, как правильно заметил предыдущий оратор, на эту тему и без меня много всего написано.
>Из этого следует, что queue.size() != 0 не может быть до инициализации объекта «о».
Если вы правда так считаете, то вы совершенно не понимаете модель исполнения многопоточного кода. Очень рекомендую почитать литературу на тему JMM, чтобы примерно быть в курсе того, как один поток может выполнять действия в одном порядке, а другой можеть видеть соответствующие изменения в памяти совсем в другом порядке. И каким образом расстановка действий синхронизации позволяет договариваться о глобально видимом порядке исполнения. Я уверен, что пока человек этого не понимает — его к написанию многопоточного кода на пушечный выстрел нельзя подпускать.
Чтобы понимать зачем while в идиомах ожидания состояния — нужно понимать откуда они выросли, и какой смысл у каждой операции в этом цикле. Но поскольку это довольно сложно для новичков, им обычно предлагают версию о spurious wakeups. Я же предлагаю просто подумать, сколько времени вы будете искать причину бага, если когда-нибудь .wait() проснется не по той причине, что вы задумали.
>Он инициализируется еще до вызова метода onFinish.
В каком смысле вы здесь пишете «до»? Это он в потоке 2 инициализируется «до» — в program order того потока. Первому потоку на этот program order плевать с высокой колокольни. С точки зрения первого потока вы можете увидеть полу-инициализированный объект, или увидеть queue в несогласованном состоянии.
Вот этот код корректен. Я очень рекомендую вам разобраться, в чем разница, и почему это важно — до того, как начнете выкладывать свой многопоточный код в продакшн.
— этот код некорректен. Пройдя по ветке queue.size != 0 вы вернете из списка объект, который может быть не до конца инициализирован. Ведь между сохранением объекта в список в другом потоке, и возвратом из списка в этом потоке у вас нет никаких ребер hb. Это типичный DCL
При чем здесь теории заговора? Вы считаете недоверие к системе, которая не пожила хотя бы пару лет — теорией заговора? Я полагаю это реализмом.
Я не знаю о багах в gcc. Но давайте прикинем — каков объем кода, созданного с с использованием утвержденной С11 модели памяти, и который работает в высоконагруженном продакшене? Огромная часть программ на С — либо однопоточные, либо используют элементарщину вроде pthread, либо (как ядра ОС) используют свои собственные низкоуровневые примитивы для работы с синхронизацией. Как вы оцениваете объем кода, который использует именно С11 ММ — актуальной версии которой, я напомню, всего полтора года? Сколько по времени этот код проработал в продакшене?
А теперь сравните это с явой, в которой большинство приложений многопоточные. В которой просто нет других примитивов синхронизации, кроме предусмотренных JMM — и поэтому каждая многопоточная программа использует только их. В которой актуальная версия JMM актуальна уже 8 лет и пережила 3-й мажорный релиз JVM. И в которой до сих пор находятся баги.
Вы можете придумать разумную теорию, которая объяснит, как это так вышло, что при реализации гораздо более сложной модели памяти, в гораздо более сложном языке, с на порядок меньшим временем на обкатку, и на порядок меньшим объемом обкатываемого кода — не было допущено ни одной ошибки? С точки зрения тервера это крайне маловероятно. Скорее кажется нерациональной ваша вера в безгрешность разработчиков gcc.
>Под гарантиями отзывчивости я в первую очередь имел ввиду то, что в C/C++ чётко определено, когда объект будет удалён
И smart-pointer-ы тоже четко определяют, когда объект будет удален? Не может такого случиться, что вот так сейчас звезды легли, что именно в этом маленьком методе завершили свою жизнь SP на 150 объектов сразу, и потому этот маленький метод вдруг стал в 150 раз тяжелее, чем обычно? И чем это отличается от GC в момент вызова того же метода?
И накладные расходы на аллокацию так уж четко определены? Разве чтобы это все действительно было четко определено, вам не придется действительно написать свои собственные аллокаторы, и использовать только стековую, или явную аллокацию/деаллокацию, без всяких smart-p? А это еще увеличит стоимость разработки.
Ну и потом — настраивая GC можно адаптировать приложение к разным объемам доступной памяти, к количеству свободных ядер на сервере, пытаться балансировать между пропускной способностью и временем отклика — в С вам все это придется делать, скорее всего, через изменения в коде.
В общем, мне кажется, что «преимущество» С здесь преувеличено. GC писали не дураки. Обойти их с явной схемой аллокации не так-то просто — это возможно, конечно, но требует инженеров уровня заметно выше среднего. А когда речь идет о соревновании экспертов, то важен уже не инструмент, а люди. Две экспертные команды, разрабатывающие на С и яве дадут результаты, пропорциональные мастерству команд, а не языку. Я так считаю :)
> программа была как минимум lock-free,
Вот как раз для lock-free алгоритмов GC часто является спасением — многопоточная аллокация/освобождение памяти + ABA делают многие алгоритмы, простые на managed-языках, очень сложными на unmanaged.
>Примеры багов, связанных с неправильной реализацией модели памяти в gcc или clang — в студию.
А их нет? И вас это не тревожит? При том, что модель памяти С++ сложнее явовской, при том, что модель памяти явы за 8 лет реального (а не в черновиках) использования все еще не освободилась от багов, при том, что ява принципиально многопоточный язык (в вашей программе будет 5-6 потоков даже если вы ни одного не запускали — в отличие от С), при том, что ява раза в полтора популярнее С…
… вы правда верите в то, что в реализации С11 модели памяти нет ошибок?
Лично я реально подхожу к оценке: многопоточность — очень сложная тема. Реализовывать модели памяти (тем более кроссплатформенно) — очень сложно. Делать это без ошибок — невозможно. Если в реализации такой сложности системы не замечено ни одной ошибки — это просто значит, что их никто не заметил. И именно поэтому модель памяти С11 — сырая. Вот когда ей будет лет 5-7, и в багтрекере будет 250 заведенных багов в ММ, и 150 исправленных — тогда можно будет говорить о зрелости. А пока это, фактически, черновик — хоть формально и чистовик
Приведите мне, где я использовал «говнокод»? Не надо приписывать мне свои фантазии.
По вашему тексту создается ощущение, что как раз к плохому коду вы более терпимы, чем к хорошему. По вашему описанию я вижу, что вы готовы писать так себе качества код просто потому, что нет причин писать его лучше. Мне же кажется, что нужны веские причины не писать код хорошего качества. Да, это разные приоритеты. Я готов идти на компромиссы когда есть веская причина — но я не готов идти на компромиссы просто по традиции. И я считаю полезным разделять компромиссное решение от чистого решения — даже если реализовано было компромиссное.
Скажите, а вам действительно есть что с чем сравнивать? Т.е. я-то много работал с кодом, написанным в вашем стиле — вы совершенно правы, такого кода полным-полно даже в очень уважаемых библиотеках. А вам довелось поработать с кодом, написанным в «моем» стиле?
Некоторые люди считают тесты излишеством — потому что работу надо делать, а не маяться перфекционизмом. Действительно, зачем мне способы убедиться, что я написал правильный код? Все сомнения — это только от неуверенности в себе.
А ведь тесты гораздо дороже в написании и поддержке, чем прямо в текущей точке кода понять, что я закладываюсь на такие-то свойства аргументов/текущего состояния, и вставить прямо сюда checkArgument/checkState. Но написание тестов сейчас является общепринятой практикой. Не наводит ли это вас на мысль, что то, что вы называли чесоточным перфекционизмом, на самом деле является просто более зрелым профессионализмом? ;)
В использовании нулевого указателя нет никакой ошибки. Я могу использовать его так: ref != null — это разве ошибка? Я могу использовать так: HashMap.put(key, null) — здесь тоже не будет ошибки. И даже так: HashMap.put(null, null) — это определенно использование нулевого указателя, но оно не является ошибкой. Дело не в том, что указатель нулевой — дело в том, что для вашего метода такой аргумент недопустим. Об этом надо и говорить — «аргумент недопустим». А вы говорите — «указатель нулевой». Ну да, он нулевой — и что с того?
Пример: вы отправили документы на рассмотрение. Вам их возвращают с комментарием. Какой комментарий более ясен: «документ заполнен черной ручкой». или «неверное заполнение: документ нельзя заполнять черной ручкой»?
> Если последняя возникает, то врядли на пути распространения её последствий окажется нужная проверка.
Почему, собственно, вряд ли? Если у вас хотя бы половина методов проверяет свои аргументы — статистически, ошибка уйдет не далее, чем на пару вызовов от места ее возникновения. В моем реальном мире заставить половину методов делать хотя бы простейшие проверки входных данных/согласованности внутри алгоритма — вполне выполнимая задача. В моем опыте это сильно упрощает локализацию ошибок — как раз потому, что ошибка теперь совсем недалеко уходит от точки своего возникновения, и не успевает испортить какие-то важные данные. В моем опыте мне важно знать, что разработчик метода осознанно бросает IAE, избегая работы с некорректными данными — а не случайно бросает NPE, разыменовывая указатель где-то, возможно, уже после того, как он использовал этот указатель чтобы испортить какие-то разделяемые данные. В моем опыте assert используется только в performance-critical участках, где лишняя проверка может что-то измеримо замедлить — и мой опыт говорит мне, что таких участков очень немного даже в высокопроизводительном коде.
Это вот мой опыт, и мой реальный мир. Видимо, ваш опыт и ваш мир — другой.
Во-вторых — я терпеть не могу эти вот визги насчет строк на разделенном буфере. Люди, которые об этом говорят — мне кажется, они вообще головой не думают. Изменения в производительности, в алгоритмах GC — они происходят в каждом релизе VM. И хотя в среднем они производительность улучшают, конкретно для вашего кода что-то может и ухудшится. Чем это отличается от ситуации со строками? Контракт метода остался тем же — вы получаете в точности ту же подстроку, что и раньше. Детали реализации изменились? — а вы задумывались когда-нибудь, сколько деталей реализации изменилось под капотом, пока вы просто не видите? Хотите совсем постоянства — сидите на одной версии VM.
>А ведь кто-то мог закладываться на скорость substring и на расход памяти при использовании разделённого буфера.
Вы таких людей знаете лично? Я вот сталкивался со string-intensive приложениями — они используют (сюрприз!) свои «строки» и свои механизмы их переиспользования.
Абсолютно то же самое можно сказать о любом невалидном значении параметра — оно возникло где-то выше по коду. Но IAE вам точно говорит, что это именно параметр невалиден. А NPE как раз может возникать при вполне валидных параметрах — из-за ошибки в самом коде функции.
>то да, использую StackOverflowError.
И вас не будет смущать то, что StackOverflowError extends VirtualMachineError?
Насчет пункта 3 вы не ответили — правильно ли я понял, что вы отдельно бросите NPE если параметр null, и отдельно IAE, если параметр некорректен по другим соображениям?
>А вообще предпочитаю использовать assert param != null
То есть вам комфортно с тем, что в debug ваш код будет бросать AssertionError, а в продакшене NPE — возможно, уже после того, как он испортит реальные данные? Действительно, кому нужны эти реальные данные… Вот отладочные данные должны быть защищены, это да.
>Я считаю использование NPE логичным и полезным для ситуации с null. И плевать, если кто-то считает это неправильным (логично, но неправильно — это вообще как?).
По-моему, это вы сами для себя решили, что NPE бросать логично. Логика бросания IAE в один шаг: получил null -> null не подходит -> throw НедопустимыйАргумент. Логика бросания NPE: получил null -> скорее всего придется его дальше разыменовывать -> получим NPE -> надо кинуть NPE пораньше -> throw NPE. А потом еще программист, читая логи, должен понять, что «NPE» означает, что «null является недопустимым аргументом у этого метода».
Лично для меня странно не то, что вы выбираете использовать в этой ситуации NPE, сколь важно то, как вы его выбираете — вы говорите «я всегда так делал, и думать над другим я не хочу — это слишком сложно для меня». На мой взгляд, для инженера такой способ выбора категорически не приемлем. Условно говоря, если бы этот диалог случился на собеседовании — я бы вас не рекомендовал бы брать. Не потому, что вы используете соглашения отличные от моих, а именно потому, что вы толком не можете их обосновать, а на подсказываемые мной проблемы закрываете глаза.
Во-вторых, стандартная библиотека на то и стандартная, что ее вызовы зачастую воспринимаются чуть ли не как примитивы языка. То есть вызов метода classlib где-то близок по низкоуровневости операции к разыменованию ссылки. И поэтому получить оттуда NPE может быть естественнее, чем IAE.
Короче говоря, common practices это штука хорошая, но и своей головой думать тоже можно.
1. Абстракция. Вызов метода — это пересечение границ абстракции. Чем меньше вы как клиент знаете о деталях реализации абстракции — тем лучше. Предположение, что код метода разыменовывает переданный указатель — это, очевидно, предположение о реализации. Конечно, это очень логичное предположение, но если его можно не делать — зачем его делать?
2. Тестирование. Если в контракте метода написано, что значение null приводит к NPE, то должен же быть тест, который это проверяет, верно? Ок, он у вас есть. Но новый джуниор в вашей команде изменил реализацию метода — проверки в начале больше нет, аргумент сразу передается в DB. Но тест проходит — потому что после того, как некорректная запись попадает в DB он-таки ссылку разыменовывает, и искомый NPE таки вылетает. То есть вы не можете реально понять, NPE вылетает ожидаемо, или случайно (возможно, оставляя неустранимые побочные эффекты)
3. Несогласованность. У вас есть метод, который принимает строку. Строка должна быть не-null, и не пустая. Не странно ли, что в первом случае клиентский код получит NPE, а во втором — IAE?
4. Аналогия. У вас есть веб-страница с полями ввода. Какое действие более логично для ситуации, когда введены ошибочные значения — 500 Internal Server Error, или нормальная страница с описанием ошибки?
5. Еще одна аналогия. Пусть у вас есть рекурсивный алгоритм, и вам известно заранее, что при каких-то параметрах вам стека точно не хватит (скажем, глубина рекурсии растет экспоненциально). Будете ли вы бросать StackOverflowException заранее, когда только обнаружите, что параметры попадают в этот диапазон?
Что касается
>NPE в логе гораздо информативнее IAE, особенно если это IAE не содержит толкового объяснения что не так.
То простите меня — за выброс исключений без вменяемого сообщения в 99% случаев надо сразу программисту руки отрывать по локоть. Потому что количество времени, которое могло бы быть сэкономлено при отладке одним-единственным таким сообщением — может измеряться днями. И как раз гораздо вероятнее, что без сообщения выбросят NPE — по вашей же аналогии, что когда вы ссылку-то разыменовываете, NPЕ же бросается безо всякого сообощения.
А IllegalArgumentException — означает, что передаваемый аргумент функция считает некорректным. Т.е программист предполагал, что в метод могут передать всякое гавно, и застраховался от этого.
Другими словами: NPE означает, что ошибка в коде самого метода (недоопределенная семантика для null-аргументов), а IAE — что ошибка в вызывающем коде, который нарушает контракт вызова метода.
В случае с park/unpark же ситуация иная — их назначение это взаимодействие с системным планировщиком задач. То, что у них есть еще какая-то memory visibility семантика — это деталь реализации, к их назначению никакого отношения не имеющая, в общем-то — случайность. Закладываться на нее может только тот, кто имеет возможность держать руку на пульсе деталей реализации classlib на разных платформах, и вмешиваться в принимаемые по ним решения. То есть Даг Ли :)
Когда Даг пишет SequenceLock на неспецифицированных особенностях Unsafe — я апплодирую, потому что для меня появляется отличный примитив синхронизации, и это ответственность Дага сменить его реализацию, если у нужных методов Unsafe сменится семантика. Когда я делаю то же самое — это моя ответственность. Вот только моим мнением при этих изменениях вряд ли кто поинтересуется, и вряд ли меня предупредят о них — в отличие от Дага.
То есть вам, по-сути, предлагают двухэтапный способ решения вашей задачи: на первом этапе вы оборачиваете Callback-based интерфейс библиотеки в нечто, предоставляющее взамен Future-based интерфейс. А Future уже можно использовать и для синхронного вычисления — просто вызывать .get(). Но можно и для асинхронного — есть методы типа .get(timeout), и прочее.
Но вы правы в том, что реализовывать обертку Callback -> Future действительно не фунт изюма. В качестве упражнения на способность делать непростые но корректные и эффективные реализации многопоточных примитивов — подойдет. Для однократного решения вашей задачи — я бы не стал возиться.
Кроме того, как правильно заметил предыдущий оратор, на эту тему и без меня много всего написано.
Если вы правда так считаете, то вы совершенно не понимаете модель исполнения многопоточного кода. Очень рекомендую почитать литературу на тему JMM, чтобы примерно быть в курсе того, как один поток может выполнять действия в одном порядке, а другой можеть видеть соответствующие изменения в памяти совсем в другом порядке. И каким образом расстановка действий синхронизации позволяет договариваться о глобально видимом порядке исполнения. Я уверен, что пока человек этого не понимает — его к написанию многопоточного кода на пушечный выстрел нельзя подпускать.
Чтобы понимать зачем while в идиомах ожидания состояния — нужно понимать откуда они выросли, и какой смысл у каждой операции в этом цикле. Но поскольку это довольно сложно для новичков, им обычно предлагают версию о spurious wakeups. Я же предлагаю просто подумать, сколько времени вы будете искать причину бага, если когда-нибудь .wait() проснется не по той причине, что вы задумали.
В каком смысле вы здесь пишете «до»? Это он в потоке 2 инициализируется «до» — в program order того потока. Первому потоку на этот program order плевать с высокой колокольни. С точки зрения первого потока вы можете увидеть полу-инициализированный объект, или увидеть queue в несогласованном состоянии.
Вот этот код корректен. Я очень рекомендую вам разобраться, в чем разница, и почему это важно — до того, как начнете выкладывать свой многопоточный код в продакшн.
Я не знаю о багах в gcc. Но давайте прикинем — каков объем кода, созданного с с использованием утвержденной С11 модели памяти, и который работает в высоконагруженном продакшене? Огромная часть программ на С — либо однопоточные, либо используют элементарщину вроде pthread, либо (как ядра ОС) используют свои собственные низкоуровневые примитивы для работы с синхронизацией. Как вы оцениваете объем кода, который использует именно С11 ММ — актуальной версии которой, я напомню, всего полтора года? Сколько по времени этот код проработал в продакшене?
А теперь сравните это с явой, в которой большинство приложений многопоточные. В которой просто нет других примитивов синхронизации, кроме предусмотренных JMM — и поэтому каждая многопоточная программа использует только их. В которой актуальная версия JMM актуальна уже 8 лет и пережила 3-й мажорный релиз JVM. И в которой до сих пор находятся баги.
Вы можете придумать разумную теорию, которая объяснит, как это так вышло, что при реализации гораздо более сложной модели памяти, в гораздо более сложном языке, с на порядок меньшим временем на обкатку, и на порядок меньшим объемом обкатываемого кода — не было допущено ни одной ошибки? С точки зрения тервера это крайне маловероятно. Скорее кажется нерациональной ваша вера в безгрешность разработчиков gcc.
И smart-pointer-ы тоже четко определяют, когда объект будет удален? Не может такого случиться, что вот так сейчас звезды легли, что именно в этом маленьком методе завершили свою жизнь SP на 150 объектов сразу, и потому этот маленький метод вдруг стал в 150 раз тяжелее, чем обычно? И чем это отличается от GC в момент вызова того же метода?
И накладные расходы на аллокацию так уж четко определены? Разве чтобы это все действительно было четко определено, вам не придется действительно написать свои собственные аллокаторы, и использовать только стековую, или явную аллокацию/деаллокацию, без всяких smart-p? А это еще увеличит стоимость разработки.
Ну и потом — настраивая GC можно адаптировать приложение к разным объемам доступной памяти, к количеству свободных ядер на сервере, пытаться балансировать между пропускной способностью и временем отклика — в С вам все это придется делать, скорее всего, через изменения в коде.
В общем, мне кажется, что «преимущество» С здесь преувеличено. GC писали не дураки. Обойти их с явной схемой аллокации не так-то просто — это возможно, конечно, но требует инженеров уровня заметно выше среднего. А когда речь идет о соревновании экспертов, то важен уже не инструмент, а люди. Две экспертные команды, разрабатывающие на С и яве дадут результаты, пропорциональные мастерству команд, а не языку. Я так считаю :)
> программа была как минимум lock-free,
Вот как раз для lock-free алгоритмов GC часто является спасением — многопоточная аллокация/освобождение памяти + ABA делают многие алгоритмы, простые на managed-языках, очень сложными на unmanaged.
А их нет? И вас это не тревожит? При том, что модель памяти С++ сложнее явовской, при том, что модель памяти явы за 8 лет реального (а не в черновиках) использования все еще не освободилась от багов, при том, что ява принципиально многопоточный язык (в вашей программе будет 5-6 потоков даже если вы ни одного не запускали — в отличие от С), при том, что ява раза в полтора популярнее С…
… вы правда верите в то, что в реализации С11 модели памяти нет ошибок?
Лично я реально подхожу к оценке: многопоточность — очень сложная тема. Реализовывать модели памяти (тем более кроссплатформенно) — очень сложно. Делать это без ошибок — невозможно. Если в реализации такой сложности системы не замечено ни одной ошибки — это просто значит, что их никто не заметил. И именно поэтому модель памяти С11 — сырая. Вот когда ей будет лет 5-7, и в багтрекере будет 250 заведенных багов в ММ, и 150 исправленных — тогда можно будет говорить о зрелости. А пока это, фактически, черновик — хоть формально и чистовик