Как стать автором
Обновить

Комментарии 27

обращаться с ней нужно осторожно. 

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

и какие легаси решения java вам мешают?

Мне лично ничего не мешает. А вот если вы почитаете скажем https://habr.com/ru/companies/ydb/articles/786550/ - то увидите, что проект Loom (который делали много лет) сталкивается с блокировкой виртуальных и реальных потоков, если где-то в коде используется syncronized и wait. А избежать этого на практике иногда совершенно нереально. В итоге одно легаси - syncronized и wait, сталкивается с другим легаси - используемыми библиотеками, которые еще не адаптированы.

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

Ну вот, все рассказывают про легаси, а выясняется что никому она не мешает особо и никто не может точно сказать, что было сделано не так. Тем более как асинхронщина и ее сложность control flow связана с легаси чего бы то ни было? Любой асинхронный код сложный, потому что он асинхронный. Примерно так же любой низкоуровневый код код не читаем. Собственно меня это и удивило в вашем комментарии.

Статью читал, так как раз Loom доказывает, что все воможно, разве нет? ;) можете назвать еще один язык-платформу, где обычное блокирующее api работает как ни в чем не бывало в неблокирующем режиме? (мне любопытно, я не сильно эрудирован касательно всего того, что есть за миром java). В python и ruby проблема просто запустить потоки в реально параллельном режиме работы из-за gil - вот это я понимаю легаси.

А избежать этого на практике иногда совершенно нереально.

В смысле не реально? В вашей же статье написано, что используйте все что хочется кроме synchronized, хоть Lock хоть семафор. Wait - и вовсе антипаттерн и признак кривого кода начиная с java 5, есть замена во все том же Lock, может вы подзабыли java core… В принципе чего не хватает - так это какой-то тулзы, которая просканирует class файлы и скажет, что в них используется synchronized и wait. К слову для реактивного api делался java agent, который кидал исключение при каждом вызове блокирующего api, есть и такой способ… В общем не вижу никакой проблемы для авторов переписать код с учетом api java 5, тем более что это все тривиально в случаи synchronized и wait.

В итоге одно легаси - syncronized и wait, сталкивается с другим легаси - используемыми библиотеками, которые еще не адаптированы.

так это одно и то же легаси ;)

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

вариант есть, другие вещи выпиливают(можно сказать что легаси правило в java не железное) типа finalize, но виртуальные потоки вещь новая и опциональная, никто не заставляет ее использовать, и каждый сам должен проверять код. ситуация несколько не однозная, но не критичная.

что все воможно, разве нет? ;) 

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

другие вещи выпиливают

А покажете еще хоть одну? Я вот за 20 с лишним лет не могу вспомнить, чтобы что-то выпилили из того, что было еще в 1.0. Потому что finalize еще не выпилили, так, между прочим.

и каждый сам должен проверять код

Разве что в стране розовых поней. В моем коде вообще нет wait. А вот фреймворки, которые я использую достаточно широко, все как на подбор по какой-то причине сильно больше кода моего проекта. Возможно потому, что у них на гитхабе сотни и тысячи контрибьюторов? Я и говорю о том, что когда ты сталкиваешься с легаси кодом в чужом коде фреймворка (который много больше чем твой проект) - то вот такой случай побороть очень сложно.

Это не невозможно разумеется - если исходники есть, так или иначе можно сесть и вникнуть. Но попробуйте скажем вникнуть в JDBC драйвер постгреса. Я пробовал. Он сравнительно небольшой. Но вы замучаетесь вносить в него исправления, а особенно - их тестировать, потому что они поддерживают все еще Java 7 (как минимум, если не более старые), и кучу разных версий сервера. У меня нет столько разных JDK и столько серверов, чтобы полноценно проверить, что мои правки ничего не сломали. А главное у меня нет столько времени, чтобы полноценно вникать во все то старье, что там годами наворотили :) У меня своего старья хватает.

То есть, если у вас в коде wait, то он не работает.

да, это другой момент почему, но опять починить это примитивно легко руками, а вот огромное количество другого api исправлено, и его менять вы бы померли, по этому я и написал, что это успешный пример, как забороли легаси, а могли бы сказать: вот вам пакет в котором есть разное апи, оно сходно по имени обычным, но не блокирующее. и тогда да, это был бы фейл.

А покажете еще хоть одну?

рефлекшен порезан. Ну и откровенно говоря уже давно не каждую старую java программу можно запустить на новой версии. Даже раньше были проблемы с совместимостью сериализации между 5 и 6 версией, если я ничего не путаю. 

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

Все-то вам легко... ведь другие примитивы (*Lock) - они не идентичная замена, у них другая семантика.

Ну то есть, опять же - разумеется это не является нерешаемой проблемой, но это время и деньги.

Ну да, если сравнивать с внедрением Java 9 и модулей - лум это успешная попытка, на первый взгляд. В тот раз поломали вообще все, maven, gradle, а Hadoop и его компоненты до сих пор совместимость с Java новых версий разгребают.

может я чего не понимаю, но чем семантика synchronized отличается от Lock? ну кроме того, что synchronized можно добавлять на методы, статические и т.д.? т.е.вам просто придется больше кода написать, с try-finally и т.д. с wait-notify на сколько помню все проще. в общем не вижу больших различий в семантике из-за которой надо будет ломать голову как все переписать. на сколько я понимаю loom не работает с synchronized из-за того, что при synchronized блокировка происходит иначе чем с ReentrantLock - разная реализация, при synchronized какие-то биты ставятся в хидер объекта. т.е. это все скорее уже какое-то внутренее именно что легаси, но не семантика.

PS: попробовал скормить таск в chatgpt 3.5 - в принципе переписывает правдиво ;) try-finally, отдельные локи на обычные и статик методы.

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

Вот если правда можно сделать автоматически такую конвертацию (а ее таки чисто интуитивно наверное можно сделать на базе какого-нибудь AST процессора или даже antlr) - это выглядит более реалистично. И я такой подход как-то даже применял с другими целями - парсил antlr-ом кучу кода на VBA, чтобы понять что она делает, и переписать в конечном счете на C#.

Со стеком опечатка? Stack это LIFO

Да, опечатка. Поправил, спасибо большое!

Есть одно маленькое жирное НО, по которому не следует использовать CompletableFuture - их невозможно РЕАЛЬНО отменить!!!

Из-за чего ваше ПО/service будет подвержена DDoS. Например у нас был какой-то сервис (написанный каким-то Васей), содержащий blocked обращение к базе данных (посредством JPA/Hibernate) и было наложено ограничение на макс ответ в 5 секунд... И чё-то начала БД подтормаживать (причины не помню), ответ от CompletableFuture просто отменили/проигнорировали и вернули ошибку... а запросы к базе и их комплексную обработку никто реально не отменил и в фоне оно продолжало работать, и жрать ресурсы CPU и БД... CompletableFuture cancel() просто не реализован и у вас нет возможности понять, что стоит остановиться... ((

Такой проблемы нет с RxJava (и наверное Spring Reactor, не помню проверял или нет), в RxJava отмена задача успешно распространяется и может быть проверена с помощью Thread.isInterrupted().

Проверял года 3 назад, может уже пофиксали, но сомневаюсь... Поэтому перед использованием CompletableFuture в реальном сложном проекте убедитесь, что cancel() работает, или используйте нормальные взрослые настраиваемые реактивные фреймворки.

похоже вы плохо разобрались в произошедшим. никакая база не знает про изменение флага isInterrupted в вашем клиентском приложении. подозревают, что в RxJava и реактивном драйвере базы просто есть возможность отменить дальнейшую выборку если она у вас очень большая (закрыть курсор, если так, то это круто), но наверняка база все равно будет пыхтеть пока не выполнит план и не зафетчит первые данные. по идее в этот же момент она и при обычном режиме работы должна проверить, что конекшен с клиентом не закрылся и ей есть куда отправлять данные. т.е. фактически схожий характер работы. если этого не происходит, то это скорее косяк базы.

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

"похоже вы плохо разобрались в произошедшим."

Похоже вы не умеет читать внимательно, но минусовать умеете )).
Смотрите "содержащий blocked обращение к базе данных (посредством JPA/Hibernate)"!
Где здесь "реактивный драйвер базы"?

Подразумевалось, что используя Thread.currentThread().isInterrupted() в том проекте вы могли бы определить, что задача уже прервана и просто закончить ненужное уже выполнение (банально бросив Exception), а не делать уже ненужные новые запросы и нагружая систему, которая и так еле дышала. И это бы прекрасно сработало в случае RxJava, но невозможно в случае стандартного CompletableFuture.

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

Серьёзно? Не может быть!!! Вот это да, как неожиданно, просто глаза мне открыли!

И это бы прекрасно сработало в случае RxJava, но невозможно в случае стандартного CompletableFuture.

Да, тут проблема именно в использовании CompletableFuture - она работает не так, как RxJava, и cancel не приводит к интеррапту потока даже если передать флаг mayInterruptIfRunning=true.
Эти нюансы хорошо задокументированы, и если разработчик полагался на предполагаемое поведение, которого не существует - это ошибка самого разработчика.

"это ошибка самого разработчика"

Да, вы абсолютно правы )) Разработчики не понимали изначально этой возможной проблемы, сделали проект (по приколу заюзав новые технологии, то ли наворотив свою ex-либу на основе CompletableFuture, то ли заюзав чью-то) и пошли дальше писать новые поекты, а проект потом свалился к кому-то на support... :-(

И я там ошибся в top-комменте - сервис получился подвержен не DDoS, а подвержен логической DoS (так будет правильнее, но не хочу править ориг коммент)


Может действительно сумбурно/непонятно изначально написал... но и так уже много буковок получилось.

"нюансы хорошо задокументированы"

Это если вы полезете в документацию )) А если вы просто почитаете вводные статьи, то там об этом может быть ничего и не сказано.
Например я давно знал о CompletableFuture, но об этом нюансе "хорошо" узнал (и запомнил) когда пытался ремонтировать тот проект.


PS: Интересно за что минусов накидали (всё же по делу и инфа полезная)? Ну фиг с ним ))

Если честно уже пару раз прочитал вместе и все равно плохо понимаю проблему, описана она не очень внятно (ну и бог с ним ;) ), видимо за это минусы и летят, у меня кармы нет. Почему проблема скорее всего не в CompletableFuture я описал (как и то почему у вас примерно тоже самое будет и с rx), ну и если у вас есть блокирующее апи, то скорее всего есть конекшен пул, по этому я не очень понимаю как можно задосить базу и почему это никак не чиниться с CompletableFuture. может не хватает опыта работы с ним

Вот вам пример/эмуляция

fun testCompletableFutureCanceling(): Int {

val resultFuture = CompletableFuture.supplyAsync { println("task 1"); 1 }
    .thenApply { println("task 2"); it + 1 }
    .thenApply {
        var rr = it

        // to emulate unpredictable database workload
        val n = 8U // Random.Default.nextUInt(10U)
        println("N: $n")

        for (i in 0U..n) {

            // Emulation of long BLOCKING operation
            Thread.sleep(1000)

            println("Long task 3.$i => we request another portion of data from database using BLOCKING JPA call");
            rr++

            // probably our task is too long and we need to stop
            if (Thread.currentThread().isInterrupted) {

                // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                // Flag Thread.isInterrupted does NOT work there, but works properly if you use RXJava

                println("Seems the task is already canceled. We will cancel next steps.")
                throw IllegalStateException("Task is canceled.")
            }
        }

        println("Long task 3 => $rr")
        rr
    }
    .thenApply { println("task 4"); it + 1 }
    .thenApply { println("task 5"); it + 1 }
    .thenApply { println("task 6"); it + 1 }

return try { resultFuture.get(5, TimeUnit.SECONDS) }
       catch (ex: TimeoutException) {
           println("### Task works too long. We do not need it anymore. Lets cancel it.")

           // It does NOT work for CompletableFuture!!!
           resultFuture.cancel(true)

           -1
       }
       catch (ex: Exception) {
           println("### Task failed. Error: $ex")
           -1
       }

}

fun main() {
val message = testCompletableFutureCanceling()
println("Task is completed => result = $message")

// to make sure other thread are not interrupted
// (to emulate execution on server)
Thread.sleep(10_000)

}

Тестил сейчас на Java 17.

Output:

task 1
task 2
N: 8
Long task 3.0 => we request another portion of data from database using BLOCKING JPA call
Long task 3.1 => we request another portion of data from database using BLOCKING JPA call
Long task 3.2 => we request another portion of data from database using BLOCKING JPA call
Long task 3.3 => we request another portion of data from database using BLOCKING JPA call
### Task works too long. We do not need it anymore. Lets cancel it.
Task is completed => result = -1
Long task 3.4 => we request another portion of data from database using BLOCKING JPA call
Long task 3.5 => we request another portion of data from database using BLOCKING JPA call
Long task 3.6 => we request another portion of data from database using BLOCKING JPA call
Long task 3.7 => we request another portion of data from database using BLOCKING JPA call
Long task 3.8 => we request another portion of data from database using BLOCKING JPA call
Long task 3 => 11
task 4
task 5

Как видите после прерывания финальной CompletableFuture, все остальные продолжают выполняться (и даже task 4 и 5), потому что флаг cancel не передается по цепочке (и не "преобразуется" в Thread.isInterrupted()). А вот RXJava умеет нормально передавать флаг cancel (и прерывать) и перобразует его в Thread.isInterrupted() (что удобно в случае БЛОКИРУЮЩЕГО кода).

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

А здесь непонятный вообще output (почему вообще не закенселилось??? хз :-) ).
Если кто-то объснит, то буду благодарен

task 1
task 2
N: 8
Long task 3.0 => we request another portion of data from database using BLOCKING JPA call
Long task 3.1 => we request another portion of data from database using BLOCKING JPA call
Long task 3.2 => we request another portion of data from database using BLOCKING JPA call
Long task 3.3 => we request another portion of data from database using BLOCKING JPA call
Long task 3.4 => we request another portion of data from database using BLOCKING JPA call
Long task 3.5 => we request another portion of data from database using BLOCKING JPA call
Long task 3.6 => we request another portion of data from database using BLOCKING JPA call
Long task 3.7 => we request another portion of data from database using BLOCKING JPA call
Long task 3.8 => we request another portion of data from database using BLOCKING JPA call
Long task 3 => 11
task 4
task 5
task 6
Task is completed => result = 14

сел колупаться и действительно, флаг Thread.currentThread().isInterrupted() в тру не ставится и InterruptedException не прилетает, понять штатными средствами внутри лямбы что ей пришел сигнал прерваться - невозможно никак. в этом плане обычный Future работает образцово

Это же и в документации сказано:

    /**
     * If not already completed, completes this CompletableFuture with
     * a {@link CancellationException}. Dependent CompletableFutures
     * that have not already completed will also complete
     * exceptionally, with a {@link CompletionException} caused by
     * this {@code CancellationException}.
     *
     * @param mayInterruptIfRunning this value has no effect in this
     * implementation because interrupts are not used to control
     * processing.
     *
     * @return {@code true} if this task is now cancelled
     */
    public boolean cancel(boolean mayInterruptIfRunning) {

а есть идеи почему так сделано? у Future нет же проблем и как тогда быть? Ничего лучше внешнего AtomicBoolean пока не нашел

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

флаг Thread.currentThread().isInterrupted() в тру не ставится и InterruptedException

Это же только пол беды - вся цепочка продолжает выполнятся.
Для серьезных server-side проектов (с Circuit Barrier) это неприемлемо.

Это же и в документации сказано:

Извините, но это звучит это как оправдание "Ну да фигово я реализовал, НО в документации же в таком-то файле на такой-то строчке я об этом написал"

а есть идеи почему так сделано?

По срокам наверное не успевали. Сделать это КАЧЕСТВЕННО наверное нелегко. У той же RxJava уже несколько версий ПОЛНОГО переписывания было.
А если потом пофиксать - потеряешь совместимость.
Но открытый вопрос: если вы не реализовали БАЗОВОЙ функциональности по прерыванию цепочки (это не про InterruptedException), зачем вы развиваете этот API для улучшения возможности написания ДЛИННЫХ цепочек? хз :-)

свой статус операции намного универсальнее

Как по мне это может быть надежнее, если вдруг какая-то thirdparty функция (которую вы вызываете, и которая написана каким-то Васей) съест Thread Interrupted флаг, но точно не универсальнее.
Эта идея с флажком очень сильно пиарилась на ранних этапах разработки на Java, когда учебные книжки писались "экспертами" пришедшими из Си, и которые даже не знали (и не писали в своих книгах) как правильно обработать InterruptedException.
Не пишу свои флажочки уже более 10 лет, так как стандартный inerruption механизм прерывает блокирующие socket вызовы и тд, а свой/ваш флажочек - нет. Написание своего флажочка наверное имеет смысл, если вы не хотите держать ссылку на задачу/thread, но предпочитаете держать ссылку на свой флажочек... не вижу смысла в этом, всё равно какую-то ссылку вам придется держать.

Извините, но это звучит это как оправдание "Ну да фигово я реализовал, НО в документации же в таком-то файле на такой-то строчке я об этом написал"

Кажется вы от класса, который заявляет A, ожидаете, что он будет делать B. CompletableFuture - это всего-лишь инструмент. Он написан в точности таким, каким он был задуман, никакого вопроса про "не успевали" тут быть не может. Если для вашей задачи он не подходит, то ничего страшного - возьмите другой. Если вы по какой-то причине считаете, что этот инструмент должен делать что-то иначе, т.е. концептуально быть другим инструментом, то может быть это вопрос именно ваших ожиданий.

Если кто-то объснит, то буду благодарен

Это из другого вашего сообщения. В статье есть ответ, в документации тоже. Не нужно ожидать, что CompletableFuture делает то же самое, что RxJava, даже не прочитав документацию, тут именно вы совершаете ошибку, а не разработчики Java.

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

Если вы разработчик этого API и точно знаете, то я с вами соглашусь, а иначе это такое же голословное утверждение как и моё, ни чем неподтвержденное ))

то может быть это вопрос именно ваших ожиданий.

Этот инструмент имеет метод cancel, который просто не кенселит реальное выполнение.
Заявлять, что мои ожидания завышены - ну такое себе.
Если вы не читали мои другие коменты, то я напомню - я сердит, потому что видел проект написанный на этих CompletableFuture, и как его фиксать (добавить РЕАЛЬНЫЙ cancel) я не знал. Хорошо, что не пришлось..., просто забили на него :)).
Так что эта проблема человека читающего рекламу CompletableFuture в разных докладах и статьях, и не ищущего ложка дегтя в реальной документации, относится не только ко мне.

то может быть это вопрос именно ваших ожиданий.

Нормальное ожидание от метода cancel, что он будет отменять ВСЁ, включая ещё не выполненные задачи и ожидание результата. Иначе он должен называться по другому, например cancelWaiting/cancelResult. Возможно не стоило наследоваться от Future, дабы не нарушать ожидаемый контракт от предыдущей работы с Future.
Можно, конечно, ткнуть меня носом в javadoc "ATTEMPTS to cancel execution of this task. ... or could not be cancelled for SOME other reason". Повторюсь, считаю это или плохим дизайном, или bad naming.
Переотсылать разработчика на документацию к десяткам реализаций Future (особенно в старых проектах), чтобы понять реализуют они контракт или нет - такое себе.

Формально в споре (или в суде) вы победите, так как в документации на Future сказано: хотим отменяем, хотим - не отменяем :-) Это проблемы/обязанности девелопера читать внимательно документацию и/или сырцы. А понятие плохого/хорошего дизайна - это субъективно.

PS: Я надеюсь вы понимаете, что мы просто холиварим?

Возможно не стоило наследоваться от Future, дабы не нарушать ожидаемый контракт от предыдущей работы с Future.

С этом не могу не согласиться, это действительно сомнительное решение.

PS: Я надеюсь вы понимаете, что мы просто холиварим?

Конечно!

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории