Pull to refresh

Comments 27

программисты C++ должны вручную управлять памятью
Вот сейчас обидно было. Не должны, более того, «должны не вручную», в большинстве случаев, кроме тех, когда это действительно необходимо.
Вот про проверки обращения к неинициализированным переменным не очень понятно из статьи. Это компилятор проверяет во время компиляции? Если да, то как обеспечивается проверка, к примеру, обращения к переменной, которая была проинициализированна из другого потока?
Очень просто: общий доступ к изменяемым переменным из разных потоков запрещён.
А как тогда происходит общение между потоками?

Через примитивы синхронизации.
Я вас удивлю, но в С++ запись в переменную без примитивов синхронизации — UB по Стандарту. И если Rust ругается во время компиляции, то C++ делает rm -rf.

А я думал что вообще в принципе нельзя писать, то есть если обернуть мьютексом или если переменная атомик то можно использовать? Или там другая концепция синхронизации?
Я вас удивлю, но в С++ запись в переменную без примитивов синхронизации — UB по Стандарту.

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

Да, именно так. В систему типов компилятора встроено, что какой типаж можно шарить, а какой нет. И пользовательский код может объявлять свои типы, реализовывать типаж "ты можешь быть разделен между потоками", так что это не прибито гвоздями.
Если интересует больше, можете почитать https://doc.rust-lang.org/book/ch16-04-extensible-concurrency-sync-and-send.html?highlight=sync,send#extensible-concurrency-with-the--sync--and--send--traits

А что насчет дедлоков, может ли компилятор Rust их обнаруживать?
Тогда по другому задам вопрос. Какие еще типы ошибок в многопоточности может обнаружить компилятор Rust?

Я подозреваю, что все UB, присущие C++.
Дедлок — это отдельный разговор, дедлок в Rust считают ошибкой логики программы, а не корректности кода. То есть при наступлении дедлока максимум что случится в Rust программе — все повиснет. Но при этом вы можете быть уверены, что не будет UB, не будет веселых побочных эффектов в виде порчи данных.

Детекция UB это замечательтно. Другой вопрос, что UB, на конкретных платформах, в тонких местах, очень даже детерминировано и активно используется и благодаря этому достигается эффективность. Это, грубо 10% всех проблем, но это не самая большая проблема при работе с потоками. Мы можем получить дедлоки, гонки, любые другие ошибки логики, а это и есть самое сложное в многопоточном программировании. Получается что Rust не так-то и сильно помогает при работе с потоками.
Так переменные-с-мьютексом уже компилятор не может проверить на корректную инициализацию, верно?
Может. Мьютекс в Rust является контейнером, и хранимое внутри начальное значение должно быть указано при его создании.

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

Может. Этот подход не зависит от того, какой тип у переменной.

Судя по тому что ответил mayorovp — отличается. То есть просто переменная-с-мьютексом всегда обязанна быть инициализированна при создании на сколько я понял.
Вы как-то странно прочитали мой комментарий. Мьютекс точно так же может быть неинициализированным.
Я просто прочитал «начальное значение указано» == «проинициализировано». Видимо не верно вас понял. Но если инициализации нет, то не ясно как тогда гарантируется что переменная будет проинициализирована до момента обращения? Поток А создал переменную, поток Б ее должен проинициализировать(через лок, это понятно), но не успел создаться, поток А, Б, В, Г… обратился к ее значению, и вот как гарантируется что этого не произойдет?
Чтобы проинциализировать через лок — нужно чтобы сам лок был уже инициализированным. А лок и защищаемое им значение инициализируются одновременно.
Что значит «создал переменную»? Выделил неинициализированный кусок памяти и попросил другой поток туда что-то записать?
По сути да. Есть «болванка», в одном потоке. Инициализация ее идёт в другом потоке. Но третий поток может обратиться к переменной раньше чем она будет проинициализированна во втором потоке. Как это статически проверить? Или создание потоков строго детерминированно? Но тогда как?

Функции mem::zeroed() и mem::unitialized являются unsafe, так что раст тут ничего статически проверять и не будет, если нарушите инварианты — ССБЗ. Из цитаты разработчика кортимы:


let x: bool = mem::uninitialized();


This is UB. It has nothing to do with references.
On all currently-supported platforms bool has only two valid values, true (bit-pattern: 0x1) and false (bit-pattern: 0x0).

mem::uninitialized, however, produces a bit-pattern where all bits have the value uninitialized. This bit-pattern is neither 0x0 nor 0x1, therefore, the resulting bool is invalid, and the behavior is undefined.

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


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

Есть «болванка», в одном потоке. Инициализация ее идёт в другом потоке. Но третий поток может обратиться к переменной раньше чем она будет проинициализированна во втором потоке.

Так же. Будут проверены условия Sync и Send и компилятор запретит вам такую чудесную "оптимизацию".

Из первого же примера непонятно, каким образом s1 и s2 выходят за пределы области видимости

Это не совсем область видимости. В строке s2=s1 происходит не копирование, а перемещение. Теперь s1 как бы пустая, и компилятор не дает ей пользоваться. С другой стороны, мы можем присвоить ей какую-нибудь новую строчку, и тогда сможем с ней снова работать.

Теперь s1 как бы пустая

Не просто пустая, а s1 на самом деле уничтожается в пользу s2.


В Rust перемещение отличается от перемещения в C++. Если в C++ std::move тесно связан с конструкторами перемещения и оператором присваивания перемещением, то в Rust код вида a = b — это перемещение, если b не является Copy (Copy — тип, который можно скопировать бит за битом: числа, bool...) типом.


Таким образом, если в C++ вы делаете std::move(s1), то лексически время жизни s1 не завершилось, у неё некое неспецифицированное значение, работа с которым может привести к неопределенному поведению.


Тогда как в Rust перемещение ( s2=s1 ) лексически уничтожает переменную s1. И теперь s1 — это не "пустая строка", это строка, которой нет.

Статья неплохая, но какое-то перегруженное впечатление от неё. С одной стороны всё верно, с другой примеры с бч обычно более простые, а подзаголовки через каждый абзац создают впечатление, что я читаю речёвку в стиле «Фабрику — Рабочим».
Sign up to leave a comment.

Articles