LINUX.ORG.RU

Thread safe in rust

 , ,


1

7

Приветствую, не холивара ради, а действительно интересно. Последнее время столкнулся с несколькими цпп либами, авторы которых не удосужились задокументировать как там ведут себя их поделки в многопотоке (thread safe ли api какой-то либы?). Приходится лезть в исходники и разбираться, открывать issue, писать в чаты и тп. Ну в общем больно это всё. Тут я вспоминаю, что растаманы козыряют тем, что у них там в расте нет data race.

Ну думаю - ок, может раст не так уж и плох, если избавит меня от боли - копаться в чьих-то там исходниках, автор которых забил на должные доки. Ну thread safe внутри моего модуля-кода меня интересует мало - я его знаю и понимаю + санитары + знаю как тестить, а вот что происходит в других либах и как с ними взаимодействывать - вот это действительно интересно. Давайте пример:

void library_function(void(*)(shared_ptr<int> i));

std::mutex s_mtx;
shred_ptr<Some_object> s;

void my_callback(shared_ptr<Some_object> i) {
   lock_guard l(s_mtx);
   s = i;
}

void my_thread() {
   while (true) {
      this_thread::sleep_for(1s);
      lock_guard l(s_mtx);
      if (s) {...}
   }
}

int main() {
   init_library(my_callback);
   thread t(my_thread);
   ...
   t.join();
   deinit_library();
}

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

Вопрос - не имея в доках инфы по поводы thread safe данной либы и её объектов, может ли раст дать какие-то гарантии в компал тайме, что если скомпилилось, то всё гуд? Some_object представляет из себя что-то вроде:

class Some_object {
public:
    do_this();
    do_that();
    ...
};

PS: не надо цепляться к тому, что раз передали shared_ptr - то объект должен быть thread safe, это вообще не факт, либа даёт объект - из глубин своего ливера в том виде, в котором автор счел нужным, никаких гарантий их этого не возникает


Ответ на: комментарий от kvpfs_2

поэтому у вас «крейты».

Если ты подразумеваешь под «крейтами» статическую сборку, то это происходит скорее по причине того, что стабильная версия компилятора выходит каждые 6 недель и в каждом релизе не гарантируется сохранение abi с предыдущим. В расте можно собирать динамические библиотеки, но придется делать для всех экспортируемых символов такую сигнатуру, чтобы она совпадала с сишным abi.

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

И да, у людей проблема с терминами.

то что лежит на crates.io – это пакеты. А вот внутри пакетов лежат крейты. Каждый крейт – это что-то типа таргета. Крейт может быть либо исполняемым файлом, либо разделяемой между другими крейтами библиотекой.

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

Darfin
()
Ответ на: комментарий от unC0Rr

Покажи, как ты собираешься ломать код на границе .so?

Где я сказал «ломать»? Я сказал, что статический анализатор не поможет и проверки надо делать в run time. Условно:

    let mut my_arc = Arc::new(String::from("Hello"));

    let a = &my_arc;
    println!("{}", a); // Hello

// start: code in shared library
    let val = Arc::get_mut(&mut my_arc);
    if val.is_some() {
        val.unwrap().push_str(" World");
    } else {
        println!("Cannot get_mut: There are multiple owners.");
    }
// end: code in shared library

    println!("{}", a);

Даже сейчас, если компилить всё в одной единице трансляции, то получается compile time error (т.к. есть живая ‘a’). А теперь представь, что код вынесен в либу, а в неё ты отдал ссылку на Arc, никакой возможности отработать у статического анализатора нет, будет оверхед в рантайме. Я ненастоящий растаман, но вопрос о том что будет после возврата из либы (как брать ссылки на Arc, ведь либа может припрятать ссылку и заюзать в любой момент), т.е. должно быть оказано влияние на вызывающий код (не в лучшую сторону, конечно же).

Допустим, что растоводы это предусмотрели, и не дают отправлять ссылки в «неизвестность», туда, где стат анализатор не может отработать (в либу, в нашем случае). Ок, тогде тебе нужно move’нуть объект в либу. Представь, что у нас в бинаре какая-то система с учетками клиентов, которых 100500, базы данных, прекрестные ссылки и всё такое. Мы хотим передать передать одну учетку клиента в какую-то либу, которая делает process_client(). Значит нам нужно вытащить учетку с данными о клиенте, убить во всех структурах данных, сделать сотню приседаний, вызвать process_client(), вернуться из него, всунуть учетку клиента обратно во все структуры данных нашего приложения. Чувствуешь, какой-здесь оверхед?

Допустим ты скажешь: «у нас есть rwlock, mutex, etc». Ok, но это опять оверхед в рантайме, всё это надо будет проверять и тратить время. А ц/ц++ может просто передать ссылку в process_client(), ничего не перемещать, без приседаний, без проверок, без обратных действий после возврата из process_client()

kvpfs_2
() автор топика
Последнее исправление: kvpfs_2 (всего исправлений: 1)
Ответ на: комментарий от kvpfs_2

let mut my_arc = Arc::new(String::from(«Hello»));

let a = &my_arc;

// start: code in shared library

let val = Arc::get_mut(&mut my_arc);

Раст не позволит тебе этого сделать. Arc::get_mut требует мутабельную ссылку. Ты не можешь в нескольких потоках иметь мутабельную ссылку на один объект

Darfin
()
Ответ на: комментарий от kvpfs_2

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

Кроме того, у тебя посреди тела функции из ниоткуда всплыл код, который находится в библиотеке. Ты в библиотеку jmp делать собрался или всё же функцию вызывать? А как только ты вызываешь функцию, разница между библиотекой и соседней функцией в том же файле исчезает.

либа может припрятать ссылку

Не может, в сигнатуре функции прописано, может она припрятать ссылку или нет.

А ц/ц++ может просто передать ссылку в process_client(), ничего не перемещать, без приседаний, без проверок, без обратных действий после возврата из process_client()

Ты думаешь, в «ц/ц++» нет лайфтаймов у объектов? Есть, просто не прописанные явно. Use after free отсюда и возникает, когда делают «просто передать ссылку» без должного управления временем жизни. А если это многопоточный код, а ты ссылку передаёшь направо-налево? А если process_client меняет содержимое объекта, а у тебя синхронизации нет?

Ок, тогде тебе нужно move’нуть объект в либу.

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

unC0Rr ★★★★★
()
Ответ на: комментарий от unC0Rr

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

Эй, вообще-то я его написал в реальности и скомпилил. Причину «некомпилируется» я также назвал, решается комментом одной строки.

По расту выводы сделал, решаю одну проблему, он накидывает ворох других.

kvpfs_2
() автор топика
Ответ на: комментарий от Darfin

Ну подумай немного, в чем там смысл был. Есть Arc в бинаре, есть ссылка. Делаем вызов в so, передаем ссылку на Arc, пытаемся получить доступ к данным, должны получить ошибку в runtime.

Ну очевидно же.

PS: я заканчиваю, оверхед для меня очевиден, ваша безопасность выходит вам боком в либах. Вам нужны исходники и всё на расте. Поэтому вы всё переписываете. Раст как злокачественная опухоль, он не может существовать с другими языками и либами, ему нужно их убить

kvpfs_2
() автор топика
Ответ на: комментарий от kvpfs_2

Все проблемы в этом

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

Из-за непонимания основных концепций раста образуется каша в голове.

В расте есть главное правило: одна мутабельная ссылка или несколько немутабельных. И это правило не нарушается никогда на уровне синтаксиса.

В начале обучения расту во всех учебниках людей учат, что есть типа мутабельные объекты/ссылки, а есть немутабельные. У людей складывается понимание, что мутабельность в расте примерно тоже самое, что константность в плюсах. В плюсах константность – это всего лишь подсказка программисту, что этот объект не будет в будущем меняться, никакого большего смысла она не несет. Если взять плюсовый проект и регулярками вырезать из него все упоминания const, то проект вполне себе будет живой и рабочий (ну есть некоторые требования наличия константности у специальных методов, типа конструктора копирования, но это не принципиальные технические мелочи).

В расте все не так. По хорошему, мутабельные объекты/ссылки должны назваться «приватными», а немутабельные – «шаренными», потому что это их основное предназначение. Но это еще сильнее увеличит порог входа на первоначальных этапах.

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

Встает вопрос, что делать если все же хочется изменять «шаренные» объекты. В плюсах если тебе хочется менять состояние константного объекта, члены класса можно пометить словом mutable. Для плюсов это скорее экзотика, в расте этот прием используется постоянно. Если у тебя есть необходимость менять что-то на немутабельном («шаренном») объекте, те члены, которые ты хочешь менять сделай такими, чтобы они могли меняться даже на неизменчивом объекте.

Если немного упростить сишнатуру метода lock на том же растовском мьютексе, то получится что-то вроде этого

pub fn lock(&self) -> &mut T

Она немного сложнее, это связано с RAII, чтобы мьютекс автоматически отпускался. Но суть в том, что метод lock можно вызывать на немутабельном объекте (&self), а возвращается тебе так или иначе мутабельная ссылка на внутренний объект. Это делается под гарантии самого мьютекса, вся его суть, что он не допустит параллельного возвращения &mut T во всем приложении.

Благодаря этому можно писать такой код

use std::sync::Mutex;

struct SharObj {
    string1: Mutex<String>,
    string2: String,
}

fn main() {
    let shar_obj = SharObj {
        string1: Mutex::new("string 1".to_string()),
        string2: "string 2".to_string()
    };

    *shar_obj.string1.lock().unwrap() += " 2";

    dbg!(&shar_obj.string1); // data: "string 1 2"
}

Т.е. изначально немутабельный объект мы все равном модифицируем.

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

Если посмотреть на всю многопоточку растовскую: мьютексы, атомики, каналы, то у них у всех основные методы принимают &self, благодаря чему весь пазл складывается.

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

Darfin
()
Ответ на: комментарий от kvpfs_2

передаем ссылку на Arc

Передаем мутабельную ссылку на Arc. Это довольно сложно сделать.

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

Darfin
()
Ответ на: комментарий от kvpfs_2

безопасность выходит вам боком в либах

Разница между вызовом функции из библиотеки и вызовом функции в том же крейте/файле так и не продемонстрирована.

unC0Rr ★★★★★
()
Ответ на: комментарий от Darfin

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

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

rumgot ★★★★★
()