Это плохая идея получить блокировку в деструкторе?

Скажем, у нас есть что-то вроде следующего псевдокода с целью достижения параллелизма и использования преимуществ RAII:

class Foo {
public:
    vector<int> nums;
    mutex lock;
};

class Bar {
public:
    Bar(Foo &foo) : m_foo(foo) 
    {
        lock_guard<mutex>(foo.lock);
        m_num = foo.nums.back();
        foo.nums.pop_back();
    }
    ~Bar()
    {
        lock_guard<mutex>(foo.lock);
        foo.nums.push_back(m_num);
    }
private:
    Foo &m_foo;
    int m_num;
};

Затем, скажем, у нас может быть любое количество экземпляров Bar, при этом идея состоит в том, что когда они выходят за пределы области видимости, деструктор вернет удерживаемый ими «ресурс» классу контроллера Foo. Однако нам также необходимо обеспечить потокобезопасность, а значит и блокировки. Однако я немного настороженно отношусь к этому дизайну, поскольку использование мьютекса в деструкторе интуитивно кажется плохой идеей. Я слишком много думаю, или, если нет, есть ли лучший способ воспользоваться преимуществами RAII здесь?

Это неплохо, но я не могу сказать то же самое о дизайне кода. Если Foo владеет критическими данными, имеет mutex lock;, то он должен заботиться о защищенном доступе к данным.

273K 10.10.2022 00:12

Получение блокировки в деструкторе ничем не отличается от получения блокировки в любой другой функции.

Maxim Egorushkin 10.10.2022 00:39

(Но не забудьте перехватить возможные исключения непосредственно в деструкторе; если деструктор вызывается во время раскручивания стека из другого исключения, а он, в свою очередь, выдает дополнительное исключение при попытке заблокировать мьютекс, программа завершится безвозвратно)

Alexander Guyer 10.10.2022 00:44

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

Taekahn 10.10.2022 00:51

@Taekahn Я не думаю, что это не разрешится само собой, если только я неправильно понимаю намерение, стоящее за вопросом. Операция pop по-прежнему будет вызываться в деструкторе, и это по-прежнему будет блокировать мьютекс. Проблемы с блокировкой мьютекса в деструкторе не исчезнут, если просто переместить блокировку мьютекса на другую функцию, которую будет вызывать деструктор.

Alexander Guyer 10.10.2022 00:55
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
5
106
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Нет ничего плохого в том, чтобы заблокировать мьютекс в деструкторе. Например, может потребоваться сделать общие ресурсы потокобезопасными. Таким образом, для освобождения права собственности на общий ресурс может потребоваться блокировка мьютекса. Если бы RAII просто развалился в многопоточном программировании, это был бы не очень полезный инструмент. Действительно, доступ к блоку управления std::shared_ptr является потокобезопасным, в том числе при уменьшении счетчика ссылок во время уничтожения общего указателя. По-видимому, это обычно реализуется с помощью атомарных операций, а не блокировки мьютекса (не цитируйте меня по этому поводу), но контекст тот же: вы освобождаете право собственности на общий ресурс во время уничтожения, и это должно быть записано в потокобезопасный способ.

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

Но может быть способ реструктурировать ваш код, чтобы полностью избежать проблемы: Bar на самом деле не нужна ссылка на Foo; нужен только int. В вашем коде Bar запрашивает int у заданного Foo. Когда Bar уничтожается, ему нужно вернуть int обратно Foo, чтобы его можно было переработать; для этого требуется хранить внутреннюю ссылку на Foo на протяжении всего его жизненного цикла, чтобы он мог взаимодействовать с ним во время уничтожения. Вместо этого рассмотрите возможность отдать intBar непосредственно при строительстве и забрать int у него при разрушении. Это движущий принцип внедрения зависимостей, который представляет собой букву «D» в слове «SOILD». Следовательно, он приносит с собой все типичные преимущества внедрения зависимостей (например, улучшение тестируемости класса Bar).

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

class BarPool:
    Foo foo;
    Map<int, Bar> bars;
    mutex m;

    BarPool(Foo foo) : foo(foo) {}

    int add_bar():
        lock m;
        // Note: foo.pop() should probably be made thread-safe
        // by internally locking / unlocking foo's mutex
        int i = foo.pop()
        bars.add(i, new Bar(i));
        unlock m;
        return i

    void remove_bar(int i):
        lock m;
        // foo.push() should also probably be made thread-safe
        bars.remove(i)
        foo.push(i)
        unlock m;

    ...

Еще одним преимуществом этого дизайна является то, что объект Bar не должен получать свой int от объекта Foo; он может получить его из любого места. Если вы решите, что хотите иметь набор объектов Bar, которые вместо этого получают свои int от объекта Baz, вы можете написать отдельный класс, который делает это. Вы можете дополнительно описать интерфейс для объектов IntCollection, реализованный Foo и Baz. Тогда BarPool будет зависеть только от абстрактного IntCollection интерфейса, что также соответствует принципу внедрения зависимостей.

Alexander Guyer 10.10.2022 01:16

Другие вопросы по теме