Многопоточность, общие данные и мьютексы

    Введение


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

    Для блокировки общих данных от одновременного доступа необходимо использовать объекты синхронизации.

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

    Неудаление мютекса приводит к утечке памяти, незахват — к некорректным данным, а неосвобождение — к блокировке всех функций, работающих с общими данными.

    Ниже рассматривается работа с мютексами в Windows и Unix, подобная идея может быть использована при работе с другими объектами синхронизации.

    Эта идея является частным случаем методики «Выделение ресурса — есть инициализация (RAII)».



    Создание, настройка и удаление мютекса


    Для начала объявим класс CAutoMutex, который создает мютекс в конструкторе и удаляет в деструторе.
    Плюсы:
    — не нужно плодить по всему проекту похожие фрагменты коды инициализации, настройки и удаления мютекса
    — автоматическое удаление мютекса и освобождение ресурсов, занятых им

    // класс-оболочка, создающий и удаляющий мютекс (Windows)
    class CAutoMutex
    {
      // дескриптор создаваемого мютекса
      HANDLE m_h_mutex;

      // запрет копирования
      CAutoMutex(const CAutoMutex&);
      CAutoMutex& operator=(const CAutoMutex&);
      
    public:
      CAutoMutex()
      {
        m_h_mutex = CreateMutex(NULL, FALSE, NULL);
        assert(m_h_mutex);
      }
      
      ~CAutoMutex() { CloseHandle(m_h_mutex); }
      
      HANDLE get() { return m_h_mutex; }
    };

    * This source code was highlighted with Source Code Highlighter.


    В Windows мютексы по умолчанию рекурсивные, а в Unix — нет. Если мютекс не является рекурсивным, то попытка захватить его два раза в одном потоке приведет к deadlock-у.
    Чтобы в Unix создать рекурсивный мютекс, необходимо установить соответствующий флаг при инициализации. Соответствующий класс CAutoMutex выглядел бы так (проверки возвращаемых значений не показаны для компактности):
    // класс-оболочка, создающий и удаляющий рекурсивный мютекс (Unix)
    class CAutoMutex
    {
      pthread_mutex_t m_mutex;

      CAutoMutex(const CAutoMutex&);
      CAutoMutex& operator=(const CAutoMutex&);

    public:
      CAutoMutex()
      {
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        pthread_mutex_init(&m_mutex, &attr);
        pthread_mutexattr_destroy(&attr);
      }
      ~CAutoMutex()
      {
        pthread_mutex_destroy(&m_mutex);
      }
      pthread_mutex_t& get()
      {
        return m_mutex;
      }
    };


    * This source code was highlighted with Source Code Highlighter.


    Захват и освобождение мютекса


    По аналогии с предыдущим классом объявим класс CMutexLock, который занимает мютекс в конструкторе и освобождает в деструкторе. Созданный объект этого класса автоматически захватит мютекс и освободит его в конце области действия независимо от того, какой именно был выход из этой области: нормальный выход, преждевременный return или выброс исключения. Плюсом также является, что можно не плодить похожие фрагменты кода работы с мютексами.

    // класс-оболочка, занимающий и освобождающий мютекс
    class CMutexLock
    {
      HANDLE m_mutex;

      // запрещаем копирование
      CMutexLock(const CMutexLock&);
      CMutexLock& operator=(const CMutexLock&);
    public:
      // занимаем мютекс при конструировании объекта
      CMutexLock(HANDLE mutex): m_mutex(mutex)
      {
        const DWORD res = WaitForSingleObject(m_mutex, INFINITE);
        assert(res == WAIT_OBJECT_0);
      }
      // освобождаем мютекс при удалении объекта
      ~CMutexLock()
      {
        const BOOL res = ReleaseMutex(m_mutex);
        assert(res);
      }
    };

    * This source code was highlighted with Source Code Highlighter.


    Для еще большего удобства объявим следующий макрос:

    // макрос, занимающий мютекс до конца области действия
    #define SCOPE_LOCK_MUTEX(hMutex) CMutexLock _tmp_mtx_capt(hMutex);

    * This source code was highlighted with Source Code Highlighter.


    Макрос позволяет не держать в голове имя класса CMutexLock и его пространство имен, а также не ломать голову каждый раз над названием создаваемого (например _tmp_mtx_capt) объекта.

    Примеры использования


    Рассмотрим примеры использования.

    Для упрощения примера объявим мютекс и общие данные в глобальной области:
    // автоматически создаваемый и удаляемый мютекс
    static CAutoMutex g_mutex;

    // общие данные
    static DWORD g_common_cnt = 0;
    static DWORD g_common_cnt_ex = 0;

    * This source code was highlighted with Source Code Highlighter.


    Пример простой функции, использующей общие данные и макрос SCOPE_LOCK_MUTEX:
    void do_sth_1( ) throw()
    {
      // ...
      // мютекс не занят
      // ...

      {
        // занимаем мютекс
        SCOPE_LOCK_MUTEX(g_mutex.get());

        // изменяем общие данные
        g_common_cnt_ex = 0;
        g_common_cnt = 0;
        
        // здесь мютекс освобождается
      }
      
      // ...
      // мютекс не занят
      // ...
    }

    * This source code was highlighted with Source Code Highlighter.


    Не правда ли, что функция do_sth_1() выглядит элегантнее, чем следующая? do_sth_1_eq:
    void do_sth_1_eq( ) throw()
    {
      // занимаем мютекс
      if (WaitForSingleObject(g_mutex.get(), INFINITE) == WAIT_OBJECT_0)
      {
        // изменяем общие данные
        g_common_cnt_ex = 0;
        g_common_cnt = 0;

        // надо не забыть освободить мютекс
        ReleaseMutex(g_mutex.get());
      }
      else
      {
        assert(0);
      }
    }


    * This source code was highlighted with Source Code Highlighter.


    В следующем примере точек выхода из функции три, но упоминание о мютексе только одно (объявление области блокировки мютекса):
    // какое-то исключение
    struct Ex {};

    // фунцкция, использующая общие данные
    int do_sth_2( const int data ) throw (Ex)
    {
      // ...
      // мютекс не занят
      // ...
      
      // занимаем мютекс на критическом участке
      SCOPE_LOCK_MUTEX(g_mutex.get());
      
      int rem = data % 3;
      
      if (rem == 1)
      {
        g_common_cnt_ex++;
        // мютекс автоматически освободится при выбросе исключения
        throw Ex();
      }
      else if (rem == 2)
      {
        // мютекс автоматически освободится при возврате
        g_common_cnt++;
        return 1;
      }
      
      // здесь мютекс автоматически освободится при возврате
      return 0;
    }

    * This source code was highlighted with Source Code Highlighter.


    Примечание: я не сторонник использовать несколько return-ов в одной функции, просто пример от этого
    становится чуть показательнее.
    А если бы функция была длиннее и точек выброса исключений было бы с десяток? Без макроса нужно было поставить перед каждой из них ReleaseMutex(...), а ошибиться здесь можно очень легко.

    Заключение


    Приведенные примеры классов и макросов достаточно просты, они не содержат сложных проверок и ожидают освобождения мютекса в течение бесконечного времени. Но даже это облегчает жизнь во многих случаях. А если облегчает жизнь, то почему бы это не использовать?

    UPD: Первый класс CAutoMutex по ошибке не был написан, вместо него было повторное объявление второго класса CMutexLock. Исправлено.

    UPD2: Убраны слова inline в объявлении методов внутри классов за ненадобностью.

    UPD3: Был добавлен вариант класса CAutoMutex с рекурсивным мютексом для Unix.

    UPD4: Перенесено в блог «C++»
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 50

      +3
      А давайте просто скажем одно ключевое слово — RAII, и не просто дадим людям рыбу, но и научим её ловить раз и навсегда?
        0
        Спасибо, добавил в начале упоминание об этом.
        +2
        Вообще-то данный велосипед уже есть в boost: www.boost.org/doc/libs/1_40_0/doc/html/thread/synchronization.html
          +2
            +3
            Конечно. Но использовать QT в качестве коллекции велосипедов, как мне кажется, не очень разумно. Не особо представляю себе причины, которые могут побудить человека использовать QT в демоне, например.
              +2
              А что с QT в этом смысле не так?
                0
                Размер программы, в первую очередь.
                Да и ни к чему тянуть сторонние библиотеки ради пары макросов и классов.
                  +1
                  >Размер программы, в первую очередь.

                  $ cat main_stl.cpp 
                  #include <string>
                  #include <iostream>
                  
                  int main(int argc, char* argv[]) {
                  	std::string s = "Hello, world!";
                  	std::cout << s << std::endl;
                  	return 0;
                  }

                  $ cat main_qt.cpp 
                  #include <QString>
                  #include <QDebug>
                  
                  int main(int argc, char* argv[]) {
                  	QString s = "Hello, world!";
                  	qDebug() << s;
                  	return 0;
                  }

                  $ cat strings_qt.pro 
                  TARGET   = strings_qt
                  SOURCES  = main_qt.cpp

                  $ qmake && make 1>/dev/null 2>1
                  $ g++ main_stl.cpp -o strings_stl
                  $ ls -l strings_*
                  -rwxr-xr-x 1 mikhail mikhail 9125 2009-11-13 15:45 strings_qt
                  -rwxr-xr-x 1 mikhail mikhail 9364 2009-11-13 15:45 strings_stl

                  $ strip strings_*
                  $ ls -l strings_*
                  -rwxr-xr-x 1 mikhail mikhail 5692 2009-11-13 15:46 strings_qt
                  -rwxr-xr-x 1 mikhail mikhail 5684 2009-11-13 15:46 strings_stl

                  Вы какой именно размер имели в виду? :)

                  >Да и ни к чему тянуть сторонние библиотеки ради пары макросов и классов.

                  Сторонние библиотеки для того и созданы ;)
                  Что касается Qt — то libQtCore весит 2 мегабайта. Как по мне — вполне приемлемый размер (для того, что она несёт внутри).
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    Да просто смысла нет. =)
                    0
                    А почему бы не построить некоторые моменты демона на томже Qt, проблемы в этом не вижу. Знаю пример решения задачи разработки демона под FreeBSD (тз такое), но человек владел разработкой только под WIN, а разбираться в тонкостях UNIX времени не было. На помощь пришел Qt, что облегчило разработку в разы:
                    — любимыая, удобная IDE;
                    — интуитивно понятная архитектура библиотеки;
                    — все необходимые абстракции от платформы.

                    Как итог все счастливы и довольны.
                      0
                      Ну, если надо срочно, а опыта с никсами нет, то может и осмысленно. Но в обычном случае это все-таки из пушки по воробьям.
                  –2
                  Да, есть что-то похожее, но:
                  — нужно подключать стороннюю библиотеку
                  — не понял исходя из текста, каким образом в упомянутых классах я могу выбрать объект синхронизации: мютекс или критическая секция (Win).
                  — слишком «много букв» :)

                    +1
                    Там шаблон на шаблоне и шаблоном погоняет. Какой объект ему подставишь туда, такой и будет. И никаких системных типов, все через объекты той же библиотеки.
                    Кстати, подключать там вроде бы ничего особо не надо. Половина boost — это только .h, линковать там что-то приходится не так уж часто, особенно в таких вот «оберточных» частях.
                • НЛО прилетело и опубликовало эту надпись здесь
                    –1
                    А что бы вы хотели услышать?
                    В STL есть очень удобный класс-шаблон std::auto_ptr<>, освобождающий память по указателю при уничтожении объекта этого класса.
                      +2
                      Который объявлен deprecated в c++0x. Вместо auto_ptr следует использовать unique_ptr, так как он поддерживает только явные операции с семантикой смены владения объектом.
                      • НЛО прилетело и опубликовало эту надпись здесь
                          –1
                          В MSDN по этому поводу пишут кратко и емко.
                      0
                      Я не сказал бы, что имеет смысл действительно ипользовать эту штуковину в реальных проектах, ведь есть множество уже готовых и заведомо лучших решений.
                      Однако, большое спасибо за статью об устройстве мьютексов.
                        0
                        Проблема: макрос SCOPE_LOCK_MUTEX не получится использовать два раза в одном и том же блоке.
                          0
                          обычно это и не нужно
                          • НЛО прилетело и опубликовало эту надпись здесь
                        • НЛО прилетело и опубликовало эту надпись здесь
                            0
                            inline для метода, объявленного внутри класса вообще не нужно, оно там подразумевается.
                            • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                ISO 14882, 7.1.2.3:

                                A function defined within a class definition is an inline function. The inline specifier shall not appear on a block scope function declaration.
                                • НЛО прилетело и опубликовало эту надпись здесь
                                    0
                                    Далеко не всегда это можно так просто решить. Вот вы определите функцию в файле code.cc, а в заголовке оставите только forward declaration. Тут может помочь только whole program optimization при условии что компилируются сразу все файлы исходного кода вместе. А если перенесёте функцию из code.cc в заголовочный файл, то опишете её как static, иначе получите одну и ту же функцию определённую несколько раз в разных объектниках. Теперь если такой заголовок включить в code2.cc, в котором именно эта функция не нужна, но нужна какая-то другая, то получим warning от компилятора про неиспользуемую функцию.
                                    • НЛО прилетело и опубликовало эту надпись здесь
                                        0
                                        code.h: void f() {...}
                                        code2.cc: #include «code.h»
                                        code3.cc: #include «code.h»

                                        компилируем
                                        code2.cc ->code2.o
                                        code3.cc ->code3.o

                                        линкуем
                                        code2.o + code3.o = error, f() два раза определена
                                0
                                согласен, убрал
                              0
                              Поправьте пожалуйста первый кусок кода. Там вместо CAutoMutex тот же CMutexLock.
                                0
                                опередили:)
                                  0
                                  поправлено
                                  +1
                                  еще неплохо бы дописать про рекурсивные мьютексы тогда.
                                  А именно

                                  f()
                                  {
                                  AutoMutex m;
                                  f();
                                  }

                                  рекурсивный мьютекс — сможет зайти сам в себя, нерекурсивный нет.

                                  зы. boost::mutex::scoped_lock lock(mutex); тоже кстати решает эту задачу
                                    +1
                                    В Windows мютексы по умолчанию рекурсивные, а в Unix — нерекурсивные.
                                    Чтобы в Unix сделать мютекс рекурсивным, необходимо установить соответствующий флаг при инициализации. Класс CAutoMutex выглядел бы так (проверки возвращаемых значений не показаны для компактности):
                                    class CAutoMutex
                                    {
                                      pthread_mutex_t m_mutex;

                                      CAutoMutex(const CAutoMutex&);
                                      CAutoMutex& operator=(const CAutoMutex&);

                                    public:
                                      inline CAutoMutex()
                                      {
                                        pthread_mutexattr_t attr;
                                        pthread_mutexattr_init(&attr);
                                        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
                                        pthread_mutex_init(&m_mutex, &attr);
                                        pthread_mutexattr_destroy(&attr);
                                      }
                                      inline ~CAutoMutex()
                                      {
                                        pthread_mutex_destroy(&m_mutex);
                                      }
                                      inline pthread_mutex_t& get()
                                      {
                                        return m_mutex;
                                      }
                                    };


                                    * This source code was highlighted with Source Code Highlighter.

                                      0
                                      а как в винде сделать нерекурсивный?
                                        0
                                        полностью аналогичным образом можно использовать семафор с максимальным счетчиком 1, он будет нерекурсивным.
                                        HANDLE sem_1 = CreateSemaphore(NULL, 1, 1, NULL)

                                    +2
                                    evgeny-lazin.blogspot.com/2008/08/blog-post.html
                                    вот тут поинтереснее будет реализация.

                                    ну и присоединяясь к остальным — не надо городить велосипеды)
                                      +1
                                      > При написании многопоточных приложений…

                                      Виндовые мьютексы для синхронизации тредов — не из пушки ли по воробьям? Критической секции обычно хватает за глаза.
                                        +2
                                        > Ниже рассматривается работа с мютексами в Windows и Unix
                                        И где там Unix?
                                          +1
                                          1. Зачем каждый раз писать get в auto locker-e? Можно сделать чтобы он принимал не только handle но и объект мютекс вами написаный.
                                          2. Зачем делать #define не понятно. Какая разница что запоминать, как класс автолокера называется или как макрос пишется?
                                            0
                                            Для элегантности еще убрать макрос хорошо было бы.
                                            Ну и упоминание про Unix как-то не к месту, позиксовые мютексы отличаются в обработке, заметно.
                                              +2
                                              Присоединяюсь ко всем остальным по поводу той мысли, что не надо строить велосипеды, но спешу добавить, что порой, сделать велосипед — это единственный способ разобраться в чем-либо. Совет автору статьи — почитайте Рихтера «Windows via C/C++» и скачайте примеры кода к этой книге, там уже большинство всего необходимого реализовано.
                                                0
                                                >спешу добавить, что порой, сделать велосипед — это единственный способ разобраться в чем-либо.
                                                полностью согласен

                                                Статья в себе сочетает отрывки из Стивенсова про мьютексы и принципа Александреску: один ресурс — один класс, Конструктор — захват ресурса, деструктор — освобождение. Но в целом — хорошее пособие для начинающего в многопоточности… Мне статья понравилась, жаль примеров нет или тестов, Это совет автору на будущее.

                                                что касается замечания по использованию сторонних библиотек — не всегда рационально тащить из-за одной-двух компонент громадную стороннюю библиотеку. Однако, возможности таких библиотек как STL & Boost надо использовать максимально.
                                                  0
                                                  А я правильно понял, что рекурсивные мьютексы отличаются от рекурсивных тем что:
                                                  — Рекурсивный может захватываться одним и темже потоком много раз, и никого не пустит, пока не будет отдан потоком столько же раз. (типа захват ++а, релиз --а, пускает когда а==0)
                                                  — А нерекурсивный можно захватывать тоже сколько угодно раз, но позволит всем ходить он как только его отпустят ( (типа захват ++а, релиз а=0, пускает когда а==0)
                                                  ?
                                                    0
                                                    Про рекурсивный вы поняли верно, а с нерекурсивным дело обстоит иначе: при повторной попытке захватить нерекурсивный мютекс в одном потоке возникнет deadlock.
                                                      0
                                                      Спасибо. Собственно это и был вопрос, с виндовыми я собаку съел (ведь если их не отпустить n раз то тоже дедлок). Собака на 2х концах :)
                                                    0
                                                    На память себе, и для нагугливших эту статью раньше остальных: Критическая секция (critical section) под виндой делает то же самое, что и mutex, но лишь в пределах одного процесса и быстрее. А mutex это получается глобальный объект системы для межпроцессного взаимодействия. см. также Mutex или CRITICAL_SECTION? В каких случаях что использовать?

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

                                                    Самое читаемое