Комментарии 26
Спасибо за хорошую статью.
Возникает интересный вопрос: а если нам надо свопнуть два значения с помощью мьютексов, как это нормально реализовать в Расте? Ну то есть через мьютексы в Си или локи в Жаве можно просто объявить кусок кода однопоточным и сделать там, что угодно
Но в расте, получается, придется делать по мьютексу на каждую используемую переменную. А если это не своп и их не 2, а больше? Оптимизационная проблема получается
С этим языком знаком плохо, поэтому буду рад, если подскажете, как это можно сделать лучше
Но на данный момент ничего лучше следующего не приходит на ум, а это не очень:
use std::mem;
use std::sync::Mutex;
struct Data1 {
m_x: Mutex<usize>,
}
struct Data2 {
m_y: Mutex<usize>,
}
fn swap(src: Data1, dest: Data2) {
let mut lock_src = src.m_x.lock().unwrap();
let mut lock_dest = dest.m_y.lock().unwrap();
mem::swap(&mut *lock_src, &mut *lock_dest);
println!("was 5 now {}", (*lock_src));
println!("was 10 now {}", (*lock_dest));
}
fn main() {
let data1 = Data1 { m_x: Mutex::new(5) };
let data2 = Data2 { m_y: Mutex::new(10) };
swap(data1, data2);
}
Конкретно в вашем примере можно сделать один мьютекс для структуры, которая содержит и Data1 и Data2. (Сори, нельзя, невнимательно прочитал код)
Но в общем случае для N мьютексов интересно - в с++ есть std::lock(...), в который можно передать произвольное количество мьютексов и не бояться дедлока, мьютексы будут захвачены в каком-то порядке. Интересно, можно ли так в расте, или в нём программист должен сам запомнить, что мьютекс А всегда должен захватываться не позже мьютекса Б?
Эмм, так когда вы объявляете кусок кода в джаве однопоточным вы разве не должны передать туда некий объект, который и играет роль мутекса? И в любом случае у вас всегда будет либо два мутекса (по одному на значение) и свопы медленнее, чем хотелось бы, либо общий мутекс на оба значение и быстрые свопы (но потенциально лишние блокировки, когда нужен доступ только у одному значению).
Вы говорите про synchronized, который синхронизирует по объекту. А я говорю про реализации интерфейса Lock из пакета java.util.concurrent.locks. Например ReentrantLock
Вот пример с аналогичным для Сишных мьютексов кодом на Джаве
class CommonResource {
int x = 0;
}
public class Main {
public static void main(String[] args) {
CommonResource resource = new CommonResource();
Lock locker = new ReentrantLock();
for (int i = 1; i < 6; i++) {
Thread t = new Thread(() -> {
locker.lock();
try {
resource.x = 1;
for (int j = 1; j < 5; j++) {
System.out.printf("%s %d %n", Thread.currentThread().getName(), resource.x);
resource.x++;
Thread.sleep(100);
}
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
finally {
locker.unlock();
}
});
t.setName("Thread " + i);
t.start();
}
}
}
Имхо не надо делать мьютекс частью структуры данных (т.к. средства синхронизации, всякие guard-ы и т.д. это всё для Data1, Data2 внешние сущности), и надо закрывать мьютексом 1 раз пару значений:
use std::mem;
use std::sync::Mutex;
struct Data1 {
m_x: usize
}
struct Data2 {
m_y: usize
}
fn swap(src: &mut (&mut Data1, &mut Data2)) {
mem::swap(&mut src.0.m_x, &mut src.1.m_y);
}
fn main() {
let mut data1 = Data1 { m_x: 5 };
let mut data2 = Data2 { m_y: 10 };
let mutex = Mutex::new((&mut data1, &mut data2));
swap(&mut mutex.lock().unwrap());
println!("was 5 now {}", data1.m_x);
println!("was 10 now {}", data2.m_y);
}
Вы использовали тупль, который тут хорошо подходит, да. Но если переменных больше 2? Если надо работать с переменными, которые по смысле не объединить в одну структуру?
Так данные не делаются частью, а просто оборачиваются. На плюсах такое тоже легко закодировать, но обычно называют “synchronized” и не считается базовым строительным блоком.
Задача не совсем понятна.
У вас есть значение которое потенциально может изменяться в нескольких потоках. Значит вам нужен мютекс, который вы будете брать каждый раз когда обращаетесь к значению.
Если у вас два значения, значит либо:
- У вас один мютекс который вы берете каждый раз когда обращаетесь к любому значению.
- У вас два мютекса, каждый охраняет свое значение.
Так будет и в Яве, и в Си, и в Расте.
как я понял, задача в общем виде - есть N мьютексов, защищающих M переменных, при этом иногда может потребоваться доступ сразу к нескольким переменным, защищенным разными мьютексами. В с++ мы можем просто сделать std::lock на несколько мьютексов, не боясь deadlock'а, а потом свободно использовать все защищенные ими переменные.
Грубо говоря, возможно в rust нужен некоторый метод, который по аналогии с std::lock залочит сразу несколько мьютексов через deadlock avoidance алгоритм и вернет кортеж ссылок на unwrapped значения. А может быть он уже существует и надо лишь его найти.
С удовольствием использую в С++ библиотеку https://github.com/LouisCharlesC/safe, которая реализует аналогичный API.
Вопрос, а какую задачу решали? Не в первый раз вижу оверинжениринг на пустом месте
Так вроде ж написали: гарантировать правильное использование мютексов на уровне языка. Если код скомпилировался, значит мютексы используется правильно и гарантируют потокобезопасность.
if(data.is_zero() ) // под мьютексом
x /= data.get(); // тоже под мьютексом
В итоге две операции под мьютексом, код не безопасный. Поздравляю, вы оверинжинирили зря
Ммм, ну я же не зря написал "потокобезопасность". При чем тут деление на 0?
При том что потокобезопасности нет, гонку устроить проще простого
Каким образом? В приведенном примере я вижу только банальное деление на 0, которые будет что с мютексом, что без него.
Если сделать проверку что data.not_zero() и потом также пойти никаких гарантий нет, потому что между двумя методами гонка, неважно что мы проверили в if, потому что между ним и следующей строчкой значение может произвольно измениться
Так is_zero или not_zero?
Я правильно понял что вы предлагаете засовывать работу с мютексом в эти методы?
Ну это как минимум гарантирует что значение в data будет консистентно. Другими словами, у вас будет гарантия, что один поток прочитает именно то значение, которое запишет другой. А то знаете, могут быть всякие приколы, например при работе с невыровнеными 64-битными значениями…
В общем, мютекс внутри объекта data гарантирует что значение объекта будет корректно. Большего от него ожидать было бы странно.
А знаете что самое интересное? В Расте так не получится. Если бы вы прочитали статью, вы бы увидели что в случае Раста мютекс надо повесить "сверху" на объект. И чтобы повторить ваш пример на Расте, надо будет явно отпустить мютекс после if, а потом явно взять его обратно. Что будет выглядеть ну мягко говоря подозрительно. И даже в такой ситуации внутренне состояние объекта будет защищено. А то что вы пользуетесь объектом небезопасно — ну тут уж никто не поможет.
Вы не понимаете о чем я говорю. Я говорю о том что вы не сможете сделать безопасной работу с мьютексом никак, в С++ такие же гварды только получше сделанные, т.к. мьютекс можно по разному захватывать. Первая половина статьи про то что оказывается если никто не пишет то можно и не делать мьютекс раст молодец(при чем тут раст непонятно)
Как раз то, что вы привели в примере, не будет валидно в случае мьютексов Раста.
Валидным тут будет:
// где-то
let data = Mutex::new(123);
// ...
// при необходимости использовать значение
// получили "умный указатель",
// дающий эксклюзивный доступ к объекту
let mut dataLocked = data.lock()?; // <lock()>
if *dataLocked != 0 {
*dataLocked /= 42;
}
// <unlock()> там, где заканчивается область видимости `dataLocked`
Непосредственно на data
вы никакие операции с данными выполнять не можете.
А почему не использовать вместо unlock() функцию drop() у MutexGuard? Насколько понял из документации, разблокировка и происходит при дропе.
Почему мьютексы в Rust реализованы именно так