Обработка исключений C++ в критической секции (pthreads)

[Редактировать: (скопировано из комментария) Как оказалось, проблема была в другом, но спасибо всем за ваш вклад.]

У меня есть класс общего контейнера, который использует один мьютекс для блокировки функций push() и pop(), поскольку я не хочу одновременно изменять начало и конец. Вот код:

int Queue::push( WorkUnit unit )
{
    pthread_mutex_lock( &_writeMutex );
    int errorCode = 0;

    try
    {
        _queue.push_back( unit );
    }
    catch( std::bad_alloc )
    {
        errorCode = 1;
    }

    pthread_mutex_unlock( &_writeMutex );

    return errorCode;
}

Когда я запускаю это в режиме отладки, все прекрасно. Когда я работаю в режиме выпуска, я получаю сбои примерно в то время, когда программа драйвера начинает нажимать и выталкивать «одновременно». Вызывает ли блок try/catch немедленный выход, если он перехватывает исключение std::bad_alloc? Если да, то должен ли я заключить оставшуюся часть функции в блок finally?

Кроме того, возможно ли, что более медленный режим отладки работает только потому, что мои вызовы push() и pop() никогда не происходят одновременно?


person psublue    schedule 06.01.2010    source источник
comment
Каков характер аварии? И не могли бы вы также опубликовать функцию pop()?   -  person Mike Seymour    schedule 07.01.2010


Ответы (8)


В C++ мы используем Инициализация получения ресурсов (RAII) для защиты от исключений.

person Nikolai Fetissov    schedule 06.01.2010

Это действительно бомбит после исключения? Судя по вашему фрагменту, гораздо более вероятно, что у вас просто плохая синхронизация. Это начинается с имени вашего мьютекса: «writeMutex». Это не сработает, если есть также «readMutex». Все операции чтения, просмотра и записи должны быть заблокированы одним и тем же мьютексом.

person Hans Passant    schedule 06.01.2010
comment
Как я уже сказал, я использую один мьютекс для блокировки функций push() и pop(), который называется _writeMutex. Как оказалось, проблема была в другом, но спасибо всем за ваш вклад. - person psublue; 07.01.2010
comment
Итак, вы собираетесь изменить его название? - person Hans Passant; 07.01.2010

Вызывает ли блок try/catch немедленный выход, если он перехватывает исключение std::bad_alloc?

Нет. Если std::bad_alloc выброшен внутри блока try {...}, сработает код в блоке catch {...}.

Если ваша программа на самом деле дает сбой, то кажется, что либо ваш вызов push_back генерирует какое-то исключение, отличное от bad_alloc (которое не обрабатывается в вашем коде), либо bad_alloc генерируется где-то за пределами блока try {...}.

Кстати, вы действительно хотите использовать здесь блок try...catch?

person John Dibling    schedule 06.01.2010
comment
К сожалению, мне нужно использовать блок try/catch (который в любом случае перехватит std::exception), потому что вызывающая функция может выбрать выполнение своей собственной работы (вместо постановки в очередь) в случае любого типа сбоя в Функция push(). Это все часть гигантского CF, который нам нужно заставить работать без помощи разработчика, который написал большую его часть :( - person psublue; 07.01.2010

плюс

как выглядит поп

создайте класс-оболочку блокировки, который автоматически освободит блокировку, когда она выйдет за пределы области действия (как в комментарии RAII)

c++ не имеет finally (спасибо мистеру Стустропу, который был строптивым)

я бы поймал std::exception или вообще ничего (утки опустили голову для войны с пламенем). Если вы ничего не поймаете, вам нужна обертка

person pm100    schedule 06.01.2010
comment
К счастью, у нас нет финала. Это делает код намного сложнее написать правильно. Деструкторы гораздо приятнее и обеспечивают более чистую реализацию. См. stackoverflow.com/questions/161177/ для получения подробной информации. - person Martin York; 07.01.2010

Что касается релиза/отладки: Да, вы часто будете сталкиваться с изменением условий гонки между двумя типами сборок. Когда вы имеете дело с синхронизацией, ваши потоки будут работать с разным уровнем обучения. Хорошо написанные потоки в основном будут выполняться одновременно, в то время как плохо написанные потоки будут высоко синхронизированы друг с другом. Все типы синхронизации приводят к некоторому синхронному поведению. Как будто синхронность и синхронизация произошли от одного и того же корня слова...

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

person Torlack    schedule 06.01.2010

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

Вы должны использовать только один мьютекс для доступа к списку.
Даже если у вас есть мьютекс только для чтения, который используется потоком, который только читает. Это не означает, что безопасно читать, когда другой поток обновляет очередь. Очередь может находиться в некотором промежуточном состоянии, вызванном вызовом потока push(), в то время как другой поток пытается перейти во включенное промежуточное состояние.

class Locker
{
    public:
        Locker(pthread_mutex_t &lock)
            :m_mutex(lock)
        {
            pthread_mutex_lock(&m_mutex);
        }
        ~Locker()
        {
            pthread_mutex_unlock(&m_mutex);
        }
    private:
        pthread_mutex_t&    m_mutex;
};

int Queue::push( WorkUnit unit )
{
    // building the object lock calls the constructor thus locking the mutex.
    Locker  lock(_writeMutex);
    int errorCode = 0;

    try
    {
        _queue.push_back( unit );
    }
    catch( std::bad_alloc )  // Other exceptions may happen here.
    {                        // You catch one that you handle locally via error codes. 
        errorCode = 1;       // That is fine. But there are other exceptions to think about.
    }

    return errorCode;
}  // lock destructor called here. Thus unlocking the mutex.

PS. Я ненавижу использование символа подчеркивания в начале.
Хотя технически здесь все в порядке (предполагая переменные-члены), в нем так легко запутаться, что я предпочитаю не добавлять '' перед идентификаторами. См. Что такое правила о использование символа подчеркивания в идентификаторе C++? для полного списка правил, которые нужно делать с '' в именах идентификаторов.

person Martin York    schedule 06.01.2010
comment
В настоящее время у меня похожая проблема, и хотя я создал свой собственный Locker , я не совсем уверен, что делать с обработкой ошибок. Я, ctor, могу генерировать исключение при сбое блокировки, но я также хотел бы знать, когда происходит сбой разблокировки, но мне не очень нравится генерировать исключения из dtor. И создание метода close противоречит цели наличия оболочки, которая закрывает мьютекс в конце областей. Каков был бы ваш подход к этому? - person Scis; 16.04.2014

Предыдущий пример кода с классом Locker имеет серьезную проблему: что вы делаете, когда и если pthread_mutex_lock() терпит неудачу? Ответ заключается в том, что в этот момент вы должны сгенерировать исключение из конструктора, и оно может быть перехвачено. Отлично. Тем не менее, согласно спецификациям исключений С++, создание исключения из деструктора недопустимо. КАК ВЫ СПРАВЛЯЕТЕСЬ С ОШИБКАМИ pthread_mutex_unlock?

person Ivan D    schedule 18.02.2010

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

В C это работает отлично:

pthread_cleanup_pop( 0 );
r = pthread_mutex_unlock( &mutex );
if ( r != 0 )
{
    /* Explicit error handling at point of occurance goes here. */
}

Но поскольку C++ — это программный аборт, просто нет разумного способа с какой-либо степенью уверенности справляться с ошибками многопоточного кодирования. Безмозглые идеи вроде включения pthread_mutex_t в класс, добавляющий какую-то переменную состояния, просто мертвы для мозга. Следующий код просто не работает:

Locker::~Locker()
{
    if ( pthread_mutex_unlock( &mutex ) != 0 )
    {
        failed = true; // Nonsense.
    }
}

И причина этого в том, что после возврата pthread_mutex_unlock() этот поток вполне может быть вырезан из процессора - вытеснен. Это означает, что общедоступная переменная .failed по-прежнему будет ложной. Другие потоки, просматривающие его, получат неверную информацию - переменная состояния говорит об отсутствии сбоев, в то время как pthread_mutex_unlock() не удалось. Даже если по какой-то причине эти два оператора выполняются одновременно, этот поток может быть вытеснен до возврата ~Locker(), а другие потоки могут изменить значение .failed. Суть в том, что эти схемы не работают — нет атомарного механизма проверки и установки для переменных, определяемых приложением.

Некоторые говорят, что деструкторы никогда не должны иметь код, который дает сбой. Все остальное — плохой дизайн. Хорошо. Мне просто любопытно посмотреть, что является хорошим дизайном, чтобы быть 100% исключением и потокобезопасным в С++.

person Ivan D    schedule 18.02.2010