Изменяемое заимствование объекта внутри Mutex - как провести рефакторинг?

У меня есть следующий шаблон во многих моих функциях:

use std::sync::{Arc, Mutex};

struct State { 
    value: i32
}

fn foo(data: Arc<Mutex<State>>) {
    let state = &mut data.lock().expect("Could not lock mutex");
    // mutate `state`
}

&mut *data.lock().expect("Could not lock mutex") повторяется снова и снова, поэтому я хотел бы преобразовать его в функцию, чтобы написать что-то вроде

let state = get_state(data); 

Я пробовал следующее:

fn get_state(data: &Arc<Mutex<State>>) -> &mut State {
    &mut data.lock().expect("Could not lock mutex")
}

Что не удается скомпилировать с:

ОШИБКА: невозможно вернуть значение, ссылающееся на временное значение

Это заставляет меня поверить, что data.state.lock().expect("...") возвращается по значению. Однако я вижу, как состояние изменяется с помощью нескольких вызовов foo на этой игровой площадке.

Что здесь происходит? Почему мой, казалось бы, простой рефакторинг не компилируется?


РЕДАКТИРОВАТЬ:

Я ожидаю, что следующее будет работать также:

fn get_state<'a>(data: &'a Arc<Mutex<State>>) -> &'a mut State {
    let state: &'a mut State = &mut data.lock().expect("Could not lock mutex");
    state
}

Но это не удается с:

   |
12 | fn get_state<'a>(data: &'a Arc<Mutex<State>>) -> &'a mut State {
   |              -- lifetime `'a` defined here
13 |     let state: &'a mut State = &mut data.lock().expect("Could not lock mutex");
   |                -------------        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ creates a temporary which is freed while still in use
   |                |
   |                type annotation requires that borrow lasts for `'a`
14 |     state
15 | }
   | - temporary value is freed at the end of this statement

Почему время жизни того, что возвращается из lock, не совпадает со временем жизни параметра data?


person Vittorio Romeo    schedule 27.11.2019    source источник
comment
lock() возвращает MutexGuard, а не само значение. Вы можете получить доступ к значению, хранящемуся внутри, потому что оно реализует Deref и DerefMut, но вы все равно ссылаетесь на защиту мьютекса. Когда защита мьютекса выходит за пределы области действия, ваша ссылка будет указывать на освобожденную память, поэтому ржавчина предотвращает это. (Помните, что блокировка мьютекса будет снята, как только защита мьютекса выйдет из области действия!!!) PS: Вместо того, чтобы скрывать .expect() в другом методе, вы должны правильно обработать случай ошибки.   -  person Svetlin Zarev    schedule 27.11.2019
comment
@SvetlinZarev: это имеет смысл. Допустим, я не использую expect и обрабатываю ошибку, но обработка ошибок одинакова во всех функциях. Опять же, как мне избежать повторения в этом случае? Единственный способ, который я могу придумать, - это функция более высокого порядка - это идиоматично?   -  person Vittorio Romeo    schedule 27.11.2019
comment
Я действительно не могу сказать, что идиоматично, но мое наблюдение состоит в том, что обычные повторяющиеся задачи обычно обрабатываются макросами.   -  person Svetlin Zarev    schedule 27.11.2019
comment
Рефакторинг будет действительным, если вы измените тип результата с &mut State на MutexGuard<State>. @Svetlin: Я думаю, это то, что вы хотели выразить, верно?   -  person CoronA    schedule 27.11.2019
comment
Я имел в виду, что нельзя обойти MutexGuard. Если OP возвращает MutexGuard ->, то это должно работать.   -  person Svetlin Zarev    schedule 27.11.2019
comment
@SvetlinZarev: Я согласен вернуть MutexGuard, не стесняйтесь добавлять это в качестве ответа.   -  person Vittorio Romeo    schedule 27.11.2019


Ответы (2)


Метод lock() возвращает MutexGuard вместо прямой ссылки на охраняемый объект. Вы можете работать со ссылкой на объект, потому что MutexGuard реализует Deref и DerefMut, но вам все равно нужно, чтобы мьютекс-защита была в области действия, потому что, когда она выйдет за пределы области действия, блокировка мьютекса будет снята. Также время жизни ссылки на внутренний объект привязано к времени жизни защиты мьютекса, поэтому компилятор не позволит вам использовать ссылку на внутренний объект без защиты мьютекса.

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

person Svetlin Zarev    schedule 27.11.2019

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

fn with_state<R>(data: Arc<Mutex<State>>, f: impl FnOnce(&mut State) -> R) -> R {
    let state = &mut data.lock().expect("Could not lock mutex");
    f(state)
}

Учитывая with_state, вы можете записать foo следующим образом:

fn foo(data: Arc<Mutex<State>>) {
    with_state(data, |state| state.value += 1)
}

Это похоже на то, как контейнеры, такие как crossbeam, гарантируют, что потоки с ограниченной областью действия всегда объединяются. Это строже, чем возврат MutexGuard, потому что при вызове with_state защита гарантированно будет удалена после возврата замыкания. С другой стороны, возврат MutexGuard является более общим, потому что вы можете написать with_state в терминах функции, которая возвращает охрану, но вы не можете пойти наоборот (используйте with_state, чтобы написать функцию, которая возвращает охрану).

person trentcl    schedule 27.11.2019