Wiki

Реализация Взаимного исключения для чтения - записи

Volker Hilsheimer
Разработка многопоточного программного обеспечения - всегда большая проблема. Разбиение сложного приложения на отдельные исполнимые модули, не ставя под угрозу стабильности его работы, требует не только хорошего структурного дизайна, который препятствует разработчику обращаться к опасным данным, но также требует и хорошего понимания концепций, инструментальных средств, и "подводных камней" многопоточного программирования.

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

Использование взаимных исключений и сигналов

Понимание - трех-гранный меч.

-- Kosh

Самый основной инструмент синхронизации - взаимное исключение (mutex). Взаимное исключение гарантирует взаимоисключающий доступ к общедоступному ресурсу в любое время. Приложения, которые хотят обратиться к ресурсу, защищенному взаимным исключением, должны ждать, пока в настоящее время активное приложение не закончено и не разблокировано взаимным исключением. Взаимные исключения очень удобны, но могут серъезно замедлить выполнение программы, если ими злоупотреблять.

Другой часто используемый инструмент - семафор (semaphore). Семафоры представляют собой "доступные ресурсы", которые могут быть приобретены несколькими потоками в одно и то же время, пока пул ресурсов не опустеет. Тогда дополнительные потоки должны ждать, пока требуемое количечтво ресурсов не будет снова доступно. Семафоры очень эффективны, поскольку они позволяют одновременный доступ к ресурсам. Но неподходящее использование часто ведет к "зависанию" потока или его "блокировке", когда два потока блокируют друг друга (каждый ждет ресурса, который другой поток в настоящее время блокировал).

В Qt, взаимные исключения и семафоры обеспечены классами QSemaphore и QMutex.

Взаимоисключающие Объекты чтения

Давайте предположим, что мы имеем один или несколько потоков чтения, читающих из файла, и один или несколько потоков записи, записывающих в этот же файл. Чтобы гарантировать, что файл не изменяется, в то время как поток его читает, мы можем использовать взаимное исключение. Например:

QMutex mutex;
 
void ReaderThread::run()
{
    ...
    mutex.lock();
    read_file();
    mutex.unlock();
    ...
}
 
void WriterThread::run()
{
    ...
    mutex.lock();
    write_file();
    mutex.unlock();
    ...
}

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

Быстро, но не достаточно

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

const int MaxReaders = 32;
QSemaphore semaphore(MaxReaders);
 
void ReaderThread::run()
{
    ...
    semaphore++;
    read_file();
    semaphore--;
    ...
}
 
void WriterThread::run()
{
    ...
    semaphore += MaxReaders;
    write_file();
    semaphore -= MaxReaders;
    ...
}

Используя такое решение проблемы, до MaxReaders потоков могут читать из файла в одно и тоже время. Как только поток записи захочет изменить файл, он попробует распределить все ресурсы семафора, таким образом удостоверяясь, что никакой другой поток не сможет получить доступ к файлу в течение операции записи.

Эти подходы разумны, но вместе с тем существует проблема . Для потока записи шанс получить доступ ко всем ресурсам быстро уменьшается с растущим числом потоков чтения до числа, где поток записи зависнет полностью. Это возможно, так как оператов QSemaphore + = блокируется, пока счетчик ресурсов семафора не станет нулем, вместо того, чтобы приобретать ресурсы один за другим при их освобожлении потоком чтения.

Другими словами, QSemaphore одобряет приложения, которые запрашивают меньше ресурсов. Это "несправедливо", но предотвращает "блокировку", это мы увидим со следующим примером.

Достаточно, но совершенно ограниченно

Заманчиво заменить оператор + = циклом, который неоднократно вызывает оператор ++ ,чтобы решить проблему, описанную выше:

void WriterThread::run()
{
    ...
    for (int i = 0; i < MaxReaders; ++i)
        semaphore++;
    write_file();
    semaphore -= MaxReaders;
    ...
}

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

Ресурс и использование

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

void WriterThread::run()
{
    ...
    mutex.lock();
    for (int i = 0; i < MaxReaders; ++i)
        semaphore++;
    mutex.unlock();
    write_file();
    semaphore -= MaxReaders;
    ...
}

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

Класс Взаимного исключения для чтения – записи

Мы можем оформить наше решение как класс Взаимного исключения для чтения - записи:

class ReadWriteMutex
{
public:
    ReadWriteMutex(int maxReaders = 32)
        : semaphore(maxReaders)
    {
    }
 
    void lockRead() { semaphore++; }
    void unlockRead() { semaphore--; }
    void lockWrite() {
        QMutexLocker locker(&mutex);
        for (int i = 0; i < maxReaders(); ++i)
            semaphore++;
    }
    void unlockWrite() { semaphore -= semaphore.total(); }
    int maxReaders() const { return semaphore.total(); }
 
private:
    QSemaphore semaphore;
    QMutex mutex;
};

Здесь приведен пример того, как мы можем использовать это в приложениях:

ReadWriteMutex mutex;
 
void ReaderThread::run()
{
    ...
    mutex.lockRead();
    read_file();
    mutex.unlockRead();
    ...
}
 
void WriterThread::run()
{
    ...
    mutex.lockWrite();
    write_file();
    mutex.unlockWrite();
    ...
}

Для вашего удобства, класс ReadWriteMutex, разработанный в этой статье будет сделан доступным в Qt Solutions вместе с классом ReadWriteMutexLocker.

Заключение

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

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


Copyright © 2004 Trolltech Trademarks