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)
Ответ на: комментарий от kvpfs_2

работают быстро благодаря лишь статическому анализу

Основанному на сигнатурах функций. Раст специально так спроектирован, что не нужно лезть внутрь функции, чтобы иметь возможность рассуждать о её поведении.

rust’у противопоказано использование разделяемых библиотек (so), иначе все превратится в тормозную тыкву с доргими runtime тестами

Вот объясните мне, я пишу функцию с сигнатурой fn fff(a: Arc<SomeType>) -> bool или fn hhh(a: &mut SomeType) -> bool

Компилятор её компилит, а я беру и сую её в библиотеку. В какой момент код поменяется так, что в нём появятся ранее невиданные рантаймовые проверки?

если rust либы (если таковые вообще есть) экспортируют простой сишный интерфейс, где уже нужно точно так же копать доки на предмет thread safe.

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

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

Вот объясните мне, я пишу функцию с сигнатурой fn fff(a: Arc) -> bool или fn hhh(a: &mut SomeType) -> bool Компилятор её компилит, а я беру и сую её в библиотеку. В какой момент код поменяется так, что в нём появятся ранее невиданные рантаймовые проверки?

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

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

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

Дело не только в thread safety, все эти перделки работают эффективно внутри одной единицы трансляции, поэтому у вас «крейты». И вообще я прошвырнулся по инету с вопросом «so в раст», удивительно, но таки да, это проблема и экзотика, удивленный растаманы с вопросами «как же так, почему всё влинковывается статично, а что если все будет на расте, где so»? Ну и всякие там советы, среди который: «экспортируй из либ сишный интерфейс», супер)

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

Ну так как ты переступишь границу-то с этим кодом? У тебя в .so будет функция, она будет принимать либо объект, либо ссылку на него.

Т.е. имеем ситуацию, когда в .so объявлены функции:

fn so_a(obj: Arc<String>);
fn so_b(obj: &Arc<String>) -> &str;
fn so_c(obj: &mut Arc<String>) -> &str;

Будем пытаться их вызывать:

let mut obj = Arc::new(String::new());

so_a(obj);

let a = &obj; // ошибка, объект уже перемещён
let mut obj = Arc::new(String::new());

let a = so_b(&obj); // пока жива ссылка a, ссылка на obj тоже считается живой
let b = &obj; // окей, две ссылки разрешены
let mut obj = Arc::new(String::new());
let a = so_c(&mut obj); // пока жива ссылка a, ссылка на obj тоже считается живой
let b = &mut obj; // ошибка, мы уже держим одну ссылку

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

магия испарится

Borrow checker - не магия, работает с сигнатурами функций, которые не меняются в зависимости от того, в .so функция или нет.

всякие там советы, среди который: «экспортируй из либ сишный интерфейс»

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

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

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

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

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

Дело не только в thread safety, все эти перделки работают эффективно внутри одной единицы трансляции, поэтому у вас «крейты».

В расте у каждой ссылки есть свой лайфтайм. Borrow checker в статике проверяет, чтобы тот объект, на который указывает ссылка жил дольше, чем сама ссылка.

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

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

Лайфтаймы ссылок описываются в сигнатурах функции

fn func1(ref1: &'a Obj1, ref2: &'b Obj2) -> &'a Obj1 {...}

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

Благодаря тому, что все лайфтаймы расписаны в сигнатруе функции, возможно параллельная сборка (с разными «единицами трансляции»). В месте где вызывается функция не нужно анализировать само тело этой функции, по сигнатуре уже можно вычислить все лайфтамы ссылок, которые будут созданы функцией.

Darfin
()