Comments 22
Double-check lock
АААААА МОИ ГЛАЗА!!!!11
Это совсем не упрек автору, наоборот, отличная демонстрация того, что старые паттерны из языков с беспорядочным доступом к переменным в Rust либо не работают совсем, либо выглядят как зебра в розовом смокинге.
Вот бы еще к примерам "как выглядит паттерн из Java в Rust" добавить "Как нужно писать". В случае синглтона это sync::Once
, например.
Согласен, данная статья скорее людей отпугнет, чем привлечет.
Перенос типичных шаблонов проектирования с одного языка на другой редко приводит к хорошим результатам, что и было сейчас с успехом продемонстрировано.
Тем не менее, спасибо за статью, есть некоторые моменты, которые я раньше боязливо обходил стороной.
Объясните, пожалуйста, для человека знакомого лишь с предпосылками, но не подробностями Rust: мне казалось, что основные идеи Rust — это "бесконтрольный доступ опасен, обложим всё compile-time проверками, максимально затруднив написание опасного кода". Не являются ли эти примеры попыткой выкосить все хорошие нововведения Раста, чтобы они не мешали писать код, как раньше. Я понимаю, что в низкоуровневых библиотеках будет что-то подобное, но, казалось бы, их должны один раз написать, очень тщательно проверить их интерфейсы на безопасность, а потом пользоваться этими интерфейсами (не реализуя их каждый раз у себя)?
Извините если я чего-то не понял, но у double check lock есть применения кроме глобальной инициализации?
Если нет — то первые 2 паттерна в Rust покрываются sync::Once, lazy_static, при необходимости — mut_static. Ещё есть scoped_tls. Советовать писать такую колбасу для задач глобальной или ленивой инициализации не надо.
кастомные низкоуровневые синхронизационные примитивы стоит писать полностью в unsafe, ради производительности
Всё-таки unsafe
о не только и не столько для производительности: он для снятия некоторых ограничений. Конечно, есть ряд unsafe
функций с отключенными проверками, но вообще далеко не всегда (и уж точно не сам по себе) unsafe
даст прибавку скорости.
Было бы куда проще и безопаснее написать просто с final. И можно шарить объект между потоками. final нам будет гарантировать видимость. Если вызвали конструктор, то значит это кому-нибудь нужно :) Лучше один раз выполнить работу и просто возвращать значение чем писать опасный код с double check lock.
public class Lazy<T> {
private final T val;
public Lazy(Supplier<T> supp) {
this.val = supp.get();
}
public T get() {
return val;
}
}
Например пусть supp достает данные из БД, а таких Lazy много и их кушает оптимизационный алгоритм, который нетривиально ходит по ним асинхронно и с возвращением. Проще написать алгоритм так, будто все данные находятся в памяти, в тоже время эффективнее читать из БД только то, что нужно. И тут тебя выручит ленивость.
Ну и зачем нужен такой класс Lazy?
Вот из-за таких вот статей по всему интернету у нас до сих пор ничерта надежно в многопоточном режиме не работает.
Rust получился страшным, сложным, но мне он нравится тем что data races на нем быстро вылезают там, где даже на второй взгляд абсолютно нормальный код (из личного опыта).
Тем не менее, я считаю Rust слишком усложненным.
p.s. double lock из примера на самом деле никакой не дабл лок, а пародия на lock free с двумя проверками, и должен выглядеть как пример с «безопасной гонкой» далее по тексту.
Наткнулся я тут снова на старый пост, и поскольку теперь я знаю Rust лучше чем раньше — не могу удержаться от запоздалой критики.
По первому паттерну — Double-check lock
Когда мы используем UnsafeCell<Option<T>>
— мы храним внутри признак инициализированности значения. Но он же самый хранится в поле init! Чтобы не хранить два раза одно и то же — лучше вместо Option использовать MaybeUninit (да, я в курсе что на момент написания поста этой штуки ещё не было):
pub struct Lazy<T, F: Fn() -> T> {
init: AtomicBool,
val: UnsafeCell<MaybeUninit<T>>,
supp: Mutex<F>
}
Теперь про второй вариант, Double-check lock с удалением Supplier
Помимо того же самого соображения про MaybeUninit, как-то странно выглядит тип Mutex<UnsafeCell<Option<Arc<F>>>>
. Во-первых, смысл дропать замыкание не только в занятой им памяти — но и в занятых им ресурсах! Во-вторых, Mutex уже является контейнером с внутренней мутабельностью и ему не требуется UnsafeCell. В-третьих, тут прямо-таки напрашивается использование FnOnce.
Так что структура должна выглядеть как-то так:
pub struct Lazy<T, F: FnOnce() -> T> {
init: AtomicBool,
val: UnsafeCell<MaybeUninit<T>>,
supp: Mutex<Option<F>>,
}
Наконец, ваш PoorMvcc попросту небезопасен, поскольку содержит гонку! Если один поток, исполняя get_read_copy
, застрянет между self.current_value.load
и Arc::clone
, в то время как другой поток сделает return_write_copy
— у вас будет повреждение кучи.
Хах! Приятно что к моим статьям возвращаются :-)
На самом деле я с тех пор перевел свою статью на английский и там код уже по чище.
Но про MaybeUninit
я не знал конечно.
На самом деле в итоге я забил на изучение Раста.
Concurrency паттерны в Rust из Java