Как стать автором
Обновить

Комментарии 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();
        }
    }
}

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

У вас тут 1 ресурс, соответственно и в расте нужен будет 1 мьютекс. В итоге вопрос не раскрыт кажется

Имхо не надо делать мьютекс частью структуры данных (т.к. средства синхронизации, всякие 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? Если надо работать с переменными, которые по смысле не объединить в одну структуру?

Потокобезопасная обработка 2, 3, 4, любого небольшого числа объектов разных типов делается через создание n-элементного кортежа из ссылок на нужные объекты и затем закрытие кортежа мьютексом. Колхоз, но должно работать. В чём состоит более общая задача мне не ясно

Так данные не делаются частью, а просто оборачиваются. На плюсах такое тоже легко закодировать, но обычно называют “synchronized” и не считается базовым строительным блоком.

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


  1. У вас один мютекс который вы берете каждый раз когда обращаетесь к любому значению.
  2. У вас два мютекса, каждый охраняет свое значение.

Так будет и в Яве, и в Си, и в Расте.

как я понял, задача в общем виде - есть N мьютексов, защищающих M переменных, при этом иногда может потребоваться доступ сразу к нескольким переменным, защищенным разными мьютексами. В с++ мы можем просто сделать std::lock на несколько мьютексов, не боясь deadlock'а, а потом свободно использовать все защищенные ими переменные.

Грубо говоря, возможно в rust нужен некоторый метод, который по аналогии с std::lock залочит сразу несколько мьютексов через deadlock avoidance алгоритм и вернет кортеж ссылок на unwrapped значения. А может быть он уже существует и надо лишь его найти.

Вопрос, а какую задачу решали? Не в первый раз вижу оверинжениринг на пустом месте

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

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? Насколько понял из документации, разблокировка и происходит при дропе.

А почему бы не прочитать статью внимательно?


В неё вообще-то сравниваются два варианта — с MutexGuard и его drop(), и другой вариант, где никакого MutexGuard нет, а есть unlock() у мьютекса. И показывается почему второй вариант небезопасен.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории