Взаимоблокировки мьютексов

Последнее обновление: 17.05.2024

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

Взаимоблокировки ресурсов в Rust

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

use std::sync::{Mutex, Arc};
use std::thread;
use std::time::Duration;

fn main() {

    let data1 = Arc::new(Mutex::new(0)); // определяем первый общий ресурс
    let data2 = Arc::new(Mutex::new(0)); // определяем второй общий ресурс

    // создаем 1-й поток
    let thread1 = thread::spawn({
        // копируем оба ресурса
        let data1_clone = Arc::clone(&data1);
        let data2_clone = Arc::clone(&data2);
        move || {

            let mut guard1 = data1_clone.lock().unwrap(); // получаем блокировку на 1-й ресурс
            *guard1 += 1;
            thread::sleep(Duration::from_millis(200));
            let mut guard2 = data2_clone.lock().unwrap(); // получаем блокировку на 2-й ресурс
            *guard2 += 1;
        }
    });

    // создаем 2-й поток
    let thread2 = thread::spawn({

        // копируем оба ресурса
        let data1_clone = Arc::clone(&data1);
        let data2_clone = Arc::clone(&data2);

        move || {
            let mut guard2 = data2_clone.lock().unwrap(); // получаем блокировку на 2-й ресурс
            *guard2 += 1;
            thread::sleep(Duration::from_millis(200));
            let mut guard1 = data1_clone.lock().unwrap(); // получаем блокировку на 1-й ресурс
            *guard1 += 1;
        }
    });
    thread1.join().unwrap();
    thread2.join().unwrap();

    println!("data1 = {}, data2 = {}",
        data1.lock().unwrap(),
        data2.lock().unwrap()
    );
}

Здесь в фукции main определяется два ресурса. И также создается два потока. Первый поток сначала блокирует первый ресурс, изменяет его и после небольшой задержки пытается блокировать второй ресурс. Второй поток, наборот, сначала блокирует второй ресурс, изменяет его и потом пытается блокировать первый ресурс. С высокой вероятностью пока первый поток будет ждать разблокировки второго ресурса, первый поток будет ждать разблокировки первого ресурса. В итоге взаимное ожидание приведет к блокировке работы программы.

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

use std::sync::{Mutex, Arc};
use std::thread;
use std::time::Duration;

fn check_mutex_and_increase(data_clone: Arc<Mutex<i32>>){
    let mut guard = data_clone.try_lock();
    match guard {
        Ok(ref mut value) => { **value += 1; }
        Err(_) => { println!("Failed to acquire lock, continuing…"); }
    }
}
fn main() {

    let data1 = Arc::new(Mutex::new(0)); // определяем первый общий ресурс
    let data2 = Arc::new(Mutex::new(0)); // определяем второй общий ресурс

    // создаем 1-й поток
    let thread1 = thread::spawn({
        // копируем оба ресурса
        let data1_clone = Arc::clone(&data1);
        let data2_clone = Arc::clone(&data2);
        move || {

            let mut guard1 = data1_clone.lock().unwrap(); // получаем блокировку на 1-й ресурс
            *guard1 += 1;
            thread::sleep(Duration::from_millis(200));
            // получаем блокировку на 2-й ресурс
            check_mutex_and_increase(data2_clone);
        }
    });

    // создаем 2-й поток
    let thread2 = thread::spawn({

        // копируем оба ресурса
        let data1_clone = Arc::clone(&data1);
        let data2_clone = Arc::clone(&data2);

        move || {
            // получаем блокировку на 2-й ресурс
            let mut guard2 = data2_clone.lock().unwrap(); // получаем блокировку на 1-й ресурс
            *guard2 += 1;
            thread::sleep(Duration::from_millis(200));
             // получаем блокировку на 1-й ресурс
            check_mutex_and_increase(data1_clone);
        }
    });
    thread1.join().unwrap();
    thread2.join().unwrap();


    println!("data1 = {}, data2 = {}",
        data1.lock().unwrap(),
        data2.lock().unwrap()
    );
}

Здесь также первую блокировку потоки получают с помощью метода lock(). Код получения второй блокировки я вынес в отдельную функцию:

fn check_mutex_and_increase(data_clone: Arc<Mutex<i32>>){
    let mut guard = data_clone.try_lock();
    match guard {
        Ok(ref mut value) => { **value += 1; }
        Err(_) => { println!("Failed to acquire lock, continuing…"); }
    }
}

Здесь сначала с помощью вызова data_clone.try_lock() пытаемся получить блокировку (метод возвращает объект Result). Далее с помощью match проверяем полученный результат. Если ошибки нет, то в value мы получаем мьютекс и через него обращаемся к самому значению, увеличивая его на 1. Обратите внимание на двойное разыменование - **value

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

Два потока также пытаются получить блокировку для двух одних и тех же ресурсов:

let mut guard1 = data1_clone.lock().unwrap(); // получаем блокировку на 1-й ресурс
*guard1 += 1;
thread::sleep(Duration::from_millis(200));
check_mutex_and_increase(data2_clone);

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

Failed to acquire lock, continuing…
Failed to acquire lock, continuing…
data1 = 1, data2 = 1
Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850