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, это вообще не факт, либа даёт объект - из глубин своего ливера в том виде, в котором автор счел нужным, никаких гарантий их этого не возникает


Я вот пока на это так смотрю - ну вот получает растаман в колбэке arc<Some_object> сохраняет его в глобальный mutex<arc<Some_objcct>>, никакой синхронизации между потоками либы и приложения нет, имеем потенциальный data race, всё компилится без ошибок.

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

Ты не сможешь в этом случае получить mut ссылку на объект. Поэтому как раз обычно заворачивают наоборот Arc<Mutex<SomeObject>>. В этом случае Mutex обеспечивает внутреннюю мутабельность по разделяемой ссылке

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

Погоди, Some_object может хотеть брать несколько мьютексов, передавать Arc<Mutex> в колбэк в таком случае не выйдет. Например do_this() хочет mtx_1, do_that() хочет mtx_2, а do_those() хочет их оба. Либо Arc без мьютекса таки можно, либо api на расте будет сильно усложнено этим ограничением (ну либо оверхед за взятие лишних мьютексов во всех кейсах, или один глобальный мьютекс во внешней либе на все случае - очевидно горлышко)

PS: и мы ведь можем сохранить immutable, пусть будет чтение в моем потоке, а внутри либы запись - в итоге data race

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

Последнее время столкнулся с несколькими цпп либами, авторы которых не удосужились задокументировать как там ведут себя их поделки в многопотоке (thread safe ли api какой-то либы?)

По-моему, всё очевидно — если не задокументировали, значит нет никаких гарантий

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

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

namespace Thread_safe {
...
}

namespace Thread_unsafe {
...
}

Если дергаешь апи из safe спейса, то не парься и не штудируй доки, где слова о thread safe могут быть спрятаны в самом дальнем углу

kvpfs_2
() автор топика

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

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

Что под этим понимается? Что в такой ситуации, когда мне кто-то даёт мутабльную ссылку на объект, то это будет чем-то вроде arc<Mutex<>>? Но ведь мне могут дань немутабельную ссылку без мьютекса? я сделаю read, либа write -> data race на выходе. И чт по поводу того, что объект имеет данные под разными мьютексами:

calss Q {
   mutex mt0;
   int mt0_i;
   mutex mt1;
   int mt1_i;
public:
   void f0() {
      lock_guard(){
          scoped_lock lck(mt0);
          ...
      }
   }
   void f1() {
      lock_guard(){
          scoped_lock lck(mt1);
          ...
      }
   }
   void f2() {
      lock_guard(){
          scoped_lock lck(mt0, mt1);
          ...
      }
   }
};
kvpfs_2
() автор топика
Последнее исправление: kvpfs_2 (всего исправлений: 2)
Ответ на: комментарий от kvpfs_2

Что в такой ситуации, когда мне кто-то даёт мутабльную ссылку на объект, то это будет чем-то вроде arc<Mutex<>>? Но ведь мне могут дань немутабельную ссылку без мьютекса? я сделаю read, либа write -> data race на выходе.

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

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

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

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

а ты мне советуешь проштудировать букварь

Ты хочешь прочитать Войну и Мир, не осилив сначала букварь. Потому и советую.

ссылку на объект без мьютекса в другой поток я перекинуть могу

Формально можешь, но с кучей ограничений. Ты должен обеспечить, что время жизни объекта будет больше, чем время жизни твоего потока. Фактически это сработает только для &'static. Мутабельную ссылку ты тоже не сможешь просто так передать, тебе borrow checker по рукам даст.

У тебя не получится создать ситуацию, когда в разных потоках есть обычная и мутабельная ссылка на объект.

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

По поводу data race уже расписали выше. По поводу дедлоков - сам раст не решает эту проблему, но есть несколько библиотек для выражения отношений между локами в типах, которые запретят брать их в неправильном порядке прямо в компайл-тайме.

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

У тебя не получится создать ситуацию, когда в разных потоках есть обычная и мутабельная ссылка на объект.

Звучит странно, тогда не получится откуда-то вернуть mutable объект как immutable (ведь это подобно обычному const?). Верится мне в это слабо, и быстрый гуглинг говорит, mutable->mutable - можно. Придется набросать какой-то тест, похоже

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

тогда не получится откуда-то вернуть mutable объект как immutable

Во-первых, не путай объект и ссылку. Тема объектов и ссылок обмазана плотно borrow checker'ом. У объекта всегда есть только один владелец, передавая объект в поток или просто в функцию, ты меняешь ему владельца. Создавая ссылку, ты не меняешь владение, но даешь как бы попользоваться другим. Ты можешь создать много обычных ссылок, но мутабельная может быть только одна. При этом нельзя создать мутабельную ссылку, если уже есть живая обычная ссылка и наоборот.

Во-вторых, давай посмотрим примеры. Ты выше говорил, что нейросетка тебе там что-то предлагала, давай посмотрим, что она накодит?

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

Речь про ссылки, конечно. А когда говорил об объекте, то в уме держал arc<> (ведь это аналог shared_ptr объекта, котороый хранит ссылку).

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

Звучит взаимоисключающе. Сначала ты допускаешь существование мутабельной и немутабльной в один момент времени, а потом отрицаешь это. Невозможность создавать немутабельную ссылку на мутабельный объект (который является данными какого-то объекта) - звучит абсурдно, если в расте так, то это конкретный жирный изъян

Во-вторых, давай посмотрим примеры. Ты выше говорил, что нейросетка тебе там что-то предлагала, давай посмотрим, что она накодит?

ок, несколько позже я приду с кодом на расте

kvpfs_2
() автор топика

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

Конечно. Есть трейты Send и Sync, не дающие шарить между потоками непотокобезопасные вещи (и дающие, если они обёрнуты в потокобезопасные вещи типа Mutex и Arc).

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

Ок, давай с примерами, а то мы кажется о разном говорим

let mut obj = Arc::new(String::new()); // создаем объект, пусть будет Arc, объявляем его как мутабельный

let a = &obj; // берем обычную ссылку, всё ок
let b = &obj; // берем еще одну обычную ссылку, всё еще ок
let с = &mut obj; // пытаемся взять мутабельную ссылку, получаем ошибку cannot borrow `obj` as mutable because it is also borrowed as immutable

Попробуем в обратном порядке

let mut obj = Arc::new(String::new()); // создаем объект, пусть будет Arc, объявляем его как мутабельный

let a = &mut obj; // берем мутабельную ссылку, всё ок
let b = &obj; // пытаемся взять обычную ссылку, получаем ошибку cannot borrow `obj` as immutable because it is also borrowed as mutable

Давай пример посложнее

let mut obj = Arc::new(String::from("Hello"));
    
thread::spawn(move || { // обрати внимание на move - мы передали владение объектом в поток
  println!("{}", obj);
});
    
println!("{}", obj); // Упс, ошибка borrow of moved value: `obj`

Попробуем передать в поток не объект, а ссылку на него?

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

let obj_ref = &mut obj; // И снова упс, `obj` does not live long enough - нельзя чтобы ссылка жила дольше, чем объект
thread::spawn(move || { // тут мы пытаемся передать ссылку на объект на стеке в поток, но поток может жить дольше, чем вызывающая функция. Нет гарантий - компиляция не проходит
  println!("{}", obj_ref);
});
    
println!("{}", obj);

А что можно сделать?

let mut obj = Arc::new(String::from("Hello"));
    
let obj_ref_clone = Arc::clone(&obj); // мы клонируем "умный указатель" и  передаем его
thread::spawn(move || {
  println!("{}", obj_ref_clone);
}).join().unwrap();
    
println!("{}", obj);

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

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

Вопрос по-другому стоит - можно ли на rust написать не thread safe (ну если там нет явного unsafe?). У меня пока ясного ответа нет, но подозреваю, что должно быть можно. Чуть позже напишу тест. Если я прав, то никаких плюшек раст не дает и нужно точно также лезть в исходники/доки/чаты

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

Сначала ты допускаешь существование мутабельной и немутабльной в один момент времени, а потом отрицаешь это.

Нет. Разбирай русскую фразу правильно: либо «создать много обычных ссылок» либо «мутабельная может быть только одна». Одновременно нельзя. Не читай как «мутабельная (из них) может …».

Невозможность создавать немутабельную ссылку на мутабельный объект (который является данными какого-то объекта) - звучит абсурдно

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

monk ★★★★★
()

Никаких гарантий по многопоточности Rust тебе не дает, это тебе не Erlang. Точка.

А вот «Если скомпилировалось значит работает» - вот это вообще выкидывай из головы, пока она не сгнила.

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

Разбирай русскую фразу правильно: либо «создать много обычных ссылок» либо «мутабельная может быть только одна»

Я это слышу как «если кто-то в мутабельной ссылке меняет значения, то у меня в немутабельной данные будут меняться». И исходя из этого у меня возникает вопрос, а даётся ли гарантия, что в случае чтения в момент изменения, что операция чтения данных будет атомарной?

PPP328 ★★★★★
()

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

Там где невозможно посчитать статикой, во всяких interior mutability проверяющихся в рантайме, грамотно расставлены трейты Sync/Send, чтобы все эти Rc/Arc/Mutex и прочие товарищи не давали делать множество пишущих ссылок без обвязки с синхронизацией.

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

С этим всем получается немного злая штука: много библиотек обмазывают код Arc вместо Rc, на всякий случай делая возможным многопоток, но замедляя при этом однопоток. В плюсах судя по всему культурная норма обратная: предпочитаем по умолчанию быстрый однопоток.

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

Я это слышу как «если кто-то в мутабельной ссылке меняет значения, то у меня в немутабельной данные будут меняться».

Если бы было возможно их создать одновременно, но в Rust это невозможно.

И исходя из этого у меня возникает вопрос, а даётся ли гарантия, что в случае чтения в момент изменения, что операция чтения данных будет атомарной?

В Rust не получишь чтение в момент изменения. Либо читаешь заранее сделанную копию, либо ссылки на чтение и на запись получаешь поочерёдно.

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

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

Это да, но у того же std::shared_ptr же нет (или уже есть?) вариантов без atomic, он же всегда аналог Arc получается.

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

Я и в плюсах могу написать безопасно, сделать спец обертки

Ну да, ну да. Каждый в чьём коде находили CVE на 9.8 так же считает, а потом вместо кода пишет обёртки, а потом опять получает CVE на 9.8.

и вообще, спец обертки можно прям в std пропихнуть, чтобы прям все понимали, если встретят.

Можно в std сразу borrow checker пропихнуть, чего мелочиться.

Вопрос по-другому стоит - можно ли на rust написать не thread safe (ну если там нет явного unsafe?).

Нельзя, в том то и дело.

У меня пока ясного ответа нет, но подозреваю, что должно быть можно. Чуть позже напишу тест. Если я прав, то никаких плюшек раст не дает и нужно точно также лезть в исходники/доки/чаты

Выглядит так что в просто ищете аргументы почему не rust. Ну удачи тогда.

anonymous
()
Ответ на: комментарий от PPP328

И исходя из этого у меня возникает вопрос, а даётся ли гарантия, что в случае чтения в момент изменения, что операция чтения данных будет атомарной?

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

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

В результате либо ты мутируешь данные на которые гарантировано нет других ссылок (соответственно, вопроса thread safety вообще не стоит), либо ты только читаешь данные, на которые, возможно, есть ссылки из других потоков, но также гарантировано немутирующие (а чтения - thread safe), либо ты мутируешь, но только через interrior mutability которая следит за синхронизацией.

anonymous
()
Ответ на: комментарий от hippi90

А в Rust у тебя нет выбора, кроме как писать правильно.

Где ты дел выбор перехрюкивать борова?

pub const STATIC_UNIT: &&() = &&();
pub const fn lifetime_translator<'a, 'b, T: ?Sized>(_val_a: &'a &'b (), val_b: &'b T) -> &'a T {
	val_b
}
pub fn expand<'a, 'b, T: ?Sized>(x: &'a T) -> &'b T {
	let f: for<'x> fn(_, &'x T) -> &'b T = lifetime_translator;
	f(STATIC_UNIT, x)
}

fn main() {
    let obj = std::sync::Arc::new(String::from("Hello"));

    //let obj_ref = &obj; // borrowed value does not live long enough
    let obj_ref = expand(&obj);
    std::thread::spawn(move || {
        std::thread::sleep(std::time::Duration::from_secs(1));
        println!("Thread {}", obj_ref);
    });

    println!("Main {}", obj);
    drop(obj);
    std::thread::sleep(std::time::Duration::from_secs(2));
}

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

Нельзя написать не thread safe без явного или скрытого unsafe. Раст в этом плане даёт важные плюшки. Ровно то же, что и про memory safe (за вычетом гарантии отсутвтии утечек памяти).

Другой вопрос, что максимально эффективный многопоточный код тоже не факт, что напишешь, в рамках ограничений системы типов. Но это уже компромисс между производительностью и гарантированной корректностью.

vbr ★★★★★
()
Последнее исправление: vbr (всего исправлений: 1)

До тестов дело не дошло, всё ограничилось дебатами с нейросетью. В общем если я всё правильно понял, то тот же Arc при виде утекшего экземляра куда-то будет разрешать только получать доступ через рантайм проверки.

Что же, если раст действительно позволяет просто пользоваться либами и не парить себе мозг по поводу «thread safe» там оно или нет, то для меня это первый значимый аргумент, а не всё это ваше ко-ко-ко про утечки.

С другой стороны - это актуально далеко не для всех либ, а лишь для сложных комбайнов со своим thread pool,ом и тп. Большинству либ контекст передаёт родительское приложение, и все эти вопросы просто неактуальны. А вот замороченность раста + рантайм проверки будут всегда (ведь речь про либы, а там статический анализатор бессилен).

Скорее хотел бы, чтобы плюсовые либы-комбаины со своими потоками, в своих интерфейсах явно писали:

struct Some_class {
   namespace thread_safe {...};
   // или так
   namespace thread_safe_police_31 {};
};

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

PS: благодарю всех, кто отписался, было полезно

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

Не совсем. Если не задокументировали - значит опираются на растовый механизм Send+Sync, где, если кратко, ты без специальных приседаний aka Arc<Mutex<>> не сможешь получить мутабельную ссылку на объект из разных потоков одновременно.

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

В общем если я всё правильно понял, то тот же Arc при виде утекшего экземляра куда-то будет разрешать только получать доступ через рантайм проверки.

Что здесь написано?

anonymous
()
Ответ на: комментарий от anonymous
pub fn get_mut(this: &mut Arc<T, A>) -> Option<&mut T>

Returns a mutable reference into the given Arc, if there are no other Arc or Weak pointers to the same allocation.

Returns None otherwise, because it is not safe to mutate a shared value.

Ты каким образом собрался пользоваться статическим анализатором за гранцами elf модуля?

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

В общем если я всё правильно понял, то тот же Arc при виде утекшего экземляра куда-то будет разрешать только получать доступ через рантайм проверки.

Как ты это себе представляешь? *arc вместо объекта вернёт тебе «доступ отказан»?

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

А вот замороченность раста

Что? Раст на порядок проще и логичнее тех же плюсов.

  • рантайм проверки будут всегда

Какие ещё рантайм проверки? Никакого оверхеда относительно плюсов rust не привносит - атомики, mutex, arc по стоимости в точности равны аналогичным плюсовым примитивам, или дешевле (например, mutex по mut ссылке можно вообще не лочить, потому что известно что доступ к нему эксклюзивный).

(ведь речь про либы, а там статический анализатор бессилен).

Borrow checker опирается только на сигнатуры функций, ему пофиг либа там или не либа.

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

Ты каким образом собрался пользоваться статическим анализатором за гранцами elf модуля?

Причем тут elf модуль и статический анализатор, если это полностью рантайм поведение?

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

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

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

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

Ну так и я о том же, а проверить нельзя - потому что в разных elf модулях и в момент компиляции ничего неизвестно

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

Нет, не из-за «разных elf модулей», а потому что принципиально неизвестно, в какой момент времени в каком-то из клонов Arc в каком-то потоке выдана живая мутабельная ссылка на содержимое. Ровно как если бы у shared_ptr была функция «я пока поизменяю объект по ссылке, а ты никому больше не давай разыменовывать».

unC0Rr ★★★★★
()
Последнее исправление: unC0Rr (всего исправлений: 1)

На самом деле если не заморачиваться на callback, хотя их аргументы можно четко прописать как Arc<Mutex<…>> - то есть еще механизмы взаимодействия: Каналы, Очереди под мьютексами, которые можно замаскировать функциями интерфейса взаимодействия, атомики (но они могут все затормозить нафиг). А если подключить crossbeam - там еще появляются не блокирующие очереди различных типов,быстрые каналы, которые позволяют делать клоны от Receiver

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

Разные elf модули - частный случай, очевидно. Тут вообще разговор был в контексте библиотек.

Ровно как если бы у shared_ptr была функция «я пока поизменяю объект по ссылке, а ты никому больше не давай разыменовывать».

Но вот только у shared_ptr такой функции нет, и если так подумать, то не факт, что это плохо. Все вот эти ваши «безопасные штучки» работают быстро благодаря лишь статическому анализу, во многом поэтому вы собираете всё из исходников начиная от «lowlevel_libs-гороха». Подозреваю, что поэтому и переписать всё хотите, чтобы - всё было обмазано вашими «безопасными перделками» + статический анализ во время компиляции. В общем есть проблема - rust’у противопоказано использование разделяемых библиотек (so), иначе все превратится в тормозную тыкву с доргими runtime тестами. Не удивлюсь, если rust либы (если таковые вообще есть) экспортируют простой сишный интерфейс, где уже нужно точно так же копать доки на предмет thread safe. Всё ваше «удобство и безопасность и скорость» - оно лишь внутри вашей маленькой экосистемы

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