Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > C/C++: Общие вопросы > Thread-safe, или как безопасно работать с потоками


Автор: mr.DUDA 27.8.2003, 11:53
Народ, кто-нибудь уже делал многопоточные приложения ?

Нужно написать класс типа:
Код
class CShareable
{
   public:
       enum {WR, RD} m_CurrState;
       HANDLE hMutex;

       bool   TryLock(int newState, int nWaitMillisecs=0);
       bool   Unlock();

       CShareable()  {hMutex=0; m_CurrState=RD;}
       ~CShareable()  {Unlock(); if(hMutex) CloseHandle(hMutex);}
};

bool  CShareable::TryLock(int newState, int nWaitMillisecs)
{
       if(newState==RD && m_CurrState==RD) return true;
       if(!hMutex)
       {
               hMutex = CreateMutex(0, true, 0);
               if(!hMutex) return false;
       }
       else
       {
               if(WaitForSingleObject(hMutex, nWaitMillisecs) != WAIT_OBJECT_0)
                       return false;
       }
       m_CurrState = newState;
       return true;
}

void CShareable::Unlock()
{
       if(ReleaseMutex(hMutex))
               m_CurrState = RD;
}


т.е. так, чтобы объекты классов, производных от CShareable можно было бы разделять между двумя и более потоками, например:

Код
class  CSomeClass: public CShareable {/* здесь что угодно */};

// функция потока, например запись в объект некоторых данных:
DWORD WINAPI Thread1Func(LPVOID lParam)
{
       CSomeClass *pObj = (CSomeClass *)lParam;
       if(pObj->TryLock(WR, 5000))
       {
               // (пишем в объект данные)

               // ...

               // освобождаем объект для записи другим потоком:
               pObj->Unlock();
       }
}

void  main()
{
       CSomeClass  obj;
       // запускаем 2 потока
       CreateThread(0, 0, &Thread1Func, (LPVOID)&obj, 0, 0);
       CreateThread(0, 0, &Thread1Func, (LPVOID)&obj, 0, 0);
       // ... ждем их завершения
}


ЗЫ, может как-то можно сделать без мьютексов ? Или с критическими секциями (работают быстрее, чем мьютексы) ??

Автор: Fantasist 29.8.2003, 19:31
Обычно используются критические секции. Правда Lock с тайм-аутом не очень удобно делать. Однако можно замутить что-то типа:

Код

class CShareable
{
  public:
      enum {WR, RD} m_CurrState;
      CRITICAL_SECTION cs;

      bool   TryLock(int newState, int nWaitMillisecs=0);
      bool   Unlock();

      CShareable()  {InitializeCriticalSection(&cs); m_CurrState=RD;}
      ~CShareable()  {Unlock(); DeleteCriticalSection(&cs);}
};

bool  CShareable::TryLock(int newState, int nWaitMillisecs)
{

      int wait=nWaitMillisecs;
      while (wait>=0&&TryEnterCriticalSection(&cs) )
      {
          sleep(10)
          wait-=10;
      }

      if (wait<=0) return false;
         
      m_CurrState = newState;
      return true;
}

void CShareable::Unlock()
{
      if(LeaveCriticalSection(&cs))
              m_CurrState = RD;
}



Mutex'ы делать не удобно. Для каждего экземляра класса надо создать уникальный mutex, а значит уникальное имя. Опять же, создавать mutex при каждом Lock'e как-то не красиво и не удобно. И в Unlock его надо удалять, а не Release'ать.

Однако наиболее интересна концепция Lock'ирующих указателей. Типа:

Код


template <class T>
class lock_ptr
{
private:
  T* mRes;
  CRITICAL_SECTION cs;
  HANDLE mOwner;
public:
  lock_ptr(T* Res): mRes(Res), mOwner(0); {InitializeCriticalSection(&cs);};
  ~lock_ptr()  {Unlock(); DeleteCriticalSection(&cs);}

 
 void Lock()
 {
    HANDLE tmp=GetCurrentThread();
    if (tmp==mOwner) return;
 
    EnterCriticalSection(&cs);
    mOwner= tmp;
 }

 void Unlock() {LeaveCriticalSection(&cs); mOwner=0;}
 
 T* operator->()
 {
    if (mOwner==0)  throw "Not locked!";
    return mRes;
 }
 
 
 bool Lock(int nWaitMillisecs)
 {
    HANDLE tmp=GetCurrentThread();
    if (tmp=mOwner) return true;
 
    while (nWaitMillisecs>=0&&TryEnterCriticalSection(&cs) )
    {
        sleep(10)
        nWaitMillisecs-=10;
    }

    if (nWaitMillisecs<=0) return false;
    mOwner= tmp;
    return true;
 }

}


Оператор-> следит за тем, чтобы к объекту не обращались бы не заблокировав его вначале. Если не хочеться терять эффективность, то проверку можно окружить #ifdef _DEBUG ... #endif

Применять просто:
Код

{
  C obj;
  lock_ptr<C> p_obj(&obj);
 
  p_obj.Lock();

  p_obj->DoMethod1();
  p_obj->DoMethod2();

  p_obj.Unlock();
}


Можно еще добавить operator T*. Тогда в этом указателе можно будет блокировать не только экземпляры классов, а вообще любые переменные.

Автор: mr.DUDA 29.8.2003, 23:36
Fantasist, спасибо за отличную идею с lock_ptr !!!

Пару вопросов (м.б. глупых):
1) а нельзя ли делать WaitForSingleObject для критических секций (внутри CRITICAL_SECTION есть интересная переменная LockSemaphore):
Код
typedef struct _RTL_CRITICAL_SECTION {
   PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
   LONG LockCount;
   LONG RecursionCount;
   HANDLE OwningThread;
   HANDLE LockSemaphore; // <----------------------------------
   DWORD SpinCount;
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

2) почему:
Цитата
Mutex'ы делать не удобно. Для каждего экземляра класса надо создать уникальный mutex, а значит уникальное имя.

если не задать имя для объекта WinAPI при его создании (а передать NULL), то мьютекс будет без имени, но тоже в единственном экземпляре.

Автор: Fantasist 2.9.2003, 06:23
Цитата
1) а нельзя ли делать WaitForSingleObject для критических секций (внутри CRITICAL_SECTION есть интересная переменная LockSemaphore):


Не знаю. Никогда не пробывал. Попробуй - нам расскажешь.

Цитата
если не задать имя для объекта WinAPI при его создании (а передать NULL), то мьютекс будет без имени, но тоже в единственном экземпляре.


Да это я ошибся. Думал по привычной схеме: синхронизация потоков через ownership handle'а объекта синхронизации с помощью явных функций захвата владения и освобождения (критические секции), а mutex'ы используем как глобальный именованный синхронизатор. Для меня mutex'ы без имени были бесполезны. Поэтому твою схему сразу и не понял.

Автор: Ars 3.9.2003, 10:43
Весьма удобно использовать следующее:
Код

class TCritSection
{
public:
   CRITICAL_SECTION CritSect;
   TCritSection(){InitializeCriticalSection(&CritSect);};
   ~TCritSection(){DeleteCriticalSection(&CritSect);};
};


class TSyncOp
{
private:
   LPCRITICAL_SECTION CritSect;
   void Enter(LPCRITICAL_SECTION ACritSect)
   {
       CritSect=ACritSect;
       EnterCriticalSection(CritSect);
   };
public:
   TSyncOp(TCritSection *ACritSection){Enter(&ACritSection->CritSect);};
   TSyncOp(LPCRITICAL_SECTION ACritSect){Enter(ACritSect);};
   ~TSyncOp(){LeaveCriticalSection(CritSect);};
};


Позволяет использовать несколько CriticalSection и не париться с обработкой exception:

Код

class MyClass
{
   TCritSection MyCritSect1,MyCritSect2;
   MyMethod()
   {
       // Do something
       {
           TSyncOp MySynchronization(&MyCritSect1);
           // Do threadsafe operations
       }
       TSyncOp MySynchronization(&MyCritSect2);
       // Do another threadsafe operations
   };
};

Автор: mr.DUDA 3.9.2003, 12:07
У меня есть несколько вопросов, Ars и Fantasist:
1) сколько таких объектов можно создавать (если в каждом - хотя бы по одной критической секции) с точки зрения WinAPI, т.е. не запарится ли винда держать в памяти тысячу проинициализированных критических секций (если учесть, что под каждую выделяется не просто кусок памяти, а еще и семафор - объект ядра)

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

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

ЗЫ, Fantasist, можно конечно
Код
while (wait>=0&&TryEnterCriticalSection(&cs) )
{
   sleep(10)
   wait-=10;
}

и так делать, но не проще ли мьютекс+WaitForSingleObject ?

Или есть какая-то другая причина для использования только критических секций ? Нужно определиться, что именно выгоднее в данной ситуации (когда критично время выполнения и корректность работы с кучей объектов).

ЗЫ(2), я пробовал с семафором внутри CRITICAL_SECTION - что-то не получается. Винда как-то хитро его использует.

Автор: Ars 3.9.2003, 13:44
1. Думаю, ограничения могут быть связаны только с максимальным количеством системных объектов, поэтому пока создаются - можно создавать... smile.gif
2. В проге чаще выполняются не InitializeCriticalSection/DeleteCriticalSection, а EnterCriticalSection/LeaveCriticalSection (т.к. все объекты должны использовать одни и те же CriticalSection). У потока при ожидании накладные расходы минимальны.
3. В хелпе написано:
Цитата
Critical section objects provide synchronization similar to that provided by mutex objects, except that critical section objects can be used only by the threads of a single process. Event, mutex, and semaphore objects can also be used in a single-process application, but critical section objects provide a slightly faster, more efficient mechanism for mutual-exclusion synchronization.

К тому же, в critical section реализован механизм предотвращения deadlock'ов при повторном входе потока в одну и ту же critical section. Может так и у mutex'ов? Не проверял.
А Билли Гейтс запретил лазить в CRITICAL_SECTION. Говорит, что она only для системного использования smile.gif

P.S. IMHO для работы с большим количеством объектов лучше не перегружать объекты операциями синхронизации, а сделать менеджер объектов, который возьмет на себя эти функции.

Автор: Ars 3.9.2003, 16:11
mr.DUDA, прошу прощения. Из-за узости мышления не совсем въехал в суть и допустил несколько неточностей... sad.gif

Для решения твоей задачи надо разобраться с несколькими вопросами:
1. Что необходимо синхронизировать - доступ к многочисленным объектам или к данным этих объектов?
2. Как долго выполняется код между вызовами Lock и Unlock?

Некоторые соображения:
Если необходимо синхронизировать доступ к объектам или их данным и обращения к ним носят характер чтения-записи (без длительной обработки), то с этим вполне может справиться менеджер объектов, используя только один синхро-объект по схеме:
а) залочить список объектов;
б) создать или удалить объект, прочитать или записать его данные;
в) разлочить список объектов.
Если есть длительная обработка, то может ее можно вынести в отдельный поток со всеми данными для нее?
Ну а если все до кучи - видимо без синхро-объекта в каждом экземпляре класса не обойтись, но я с такими несчастными случаями не сталкивался.

Вообще, во многих случаях можно обойтись следующим кодом.
Грубо, но ни разу не глючило. smile.gif
Код

class
{
   BYTE IsSomeThreadInside;
   MyMethod()
   {
       while(IsSomeThreadInside) Sleep(10);
       IsSomeThreadInside=1;
       // Do pseudothreadsafe operations
       IsSomeThreadInside=0;
   };
};

Автор: Fantasist 3.9.2003, 18:31
Цитата
В моей проге возможен одновременный доступ нескольких потоков к любому объекту (а их может существовать от сотни до тысячи - как мелких, так и толстых)


В этом случае полезно подумать, а нельзя ли разбить объекты на функционально связанные группы и блокировать сразу группами. Типа транзакции.

Цитата
Может так и у mutex'ов? Не проверял


Конечно, с mutex'ами также. С какой-то стороны они даже безопаснее, ибо в reference есть такое упоминание:

Цитата

If a thread calls LeaveCriticalSection when it does not have ownership of the specified critical section object, an error occurs that may cause another thread using EnterCriticalSection to wait indefinitely.


Про mutex'ов такого нет. Конечно, такая ситуация может возникнуть только в запутанной системе управления синхронизацией. На практике я не видел ничего близкого к подобной ситуации. И тем не менее.

Автор: mr.DUDA 3.9.2003, 21:49
to Ars:
1) к данным любого объекта м.б. предоставлен доступ на чтение и запись, из любого потока
2) объекты упорядочены по иерархии (каждый объект имеет 2 списка дочерних объектов)
3) объектов м.б. очень много, и структура м.б. очень сложной
4) обработка данных объекта может занять определенное время (если например используются файловые или оконные функции для сохранения/чтения или отображения объекта)

имхо, можно было бы сделать внутри каждого потока свою CRITICAL_SECTION, и в каждом объекте хранить указатель на CRITICAL_SECTION потока-владельца (тогда любой другой поток сначала подождет полного освобождения критической секции, прежде чем писать или читать объект). Только недостаток этого подхода в том, что если скажем мы заблокировали одним потоком доступ сначала к объекту X, а потом - к объекту Y, то другой поток не сможет получить доступа к объекту X, пока не будет полностью освобождена критическая секция (т.е. нужно освободить доступ как к X, так и к Y).

С другой стороны, хранить внутри каждого объекта мьютекс или критическую секцию невыгодно.

Менеджер объектов даст только лишнюю головную боль.

Автор: Ars 4.9.2003, 10:51
mr.DUDA, да-а-а!..

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

Не совсем понял идею
Цитата
сделать внутри каждого потока свою CRITICAL_SECTION

CRITICAL_SECTION - это как раз то, что у них должно быть общее. Как они будут подменять указатель на CRITICAL_SECTION в объекте? Что будет делать поток-владелец, когда его CRITICAL_SECTION поимеют другие (потоки), т.е. как он вернет ее себе?

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

Автор: mr.DUDA 4.9.2003, 17:03
А если забить на многопотоковую модель и сделать 1 поток для выполнения списка действий ? А еще можно сделать 2-й поток, извлекающий из очереди сообщения одно за другим и посылающий их второму потоку-исполнителю; в случае "подвисания" второго потока, первый после таймаута перезапустит его.

Так можно снять вообще проблему разделения объектов несколькими потоками.

Суть решаемой проблемы - сделать возможной обработку т.н. "сообщений" (задаваемых числовым ID'ом и списком параметров) с "завёрткой" в тело сообщения указателей на объекты из общего для всех потоков дерева объектов. Для расширяемого (с пом. модулей) приложения нужно выбрать способ реакции внутри каждого модуля на "приходящее" в очередь сообщение: либо делать внутри каждого DLL свой поток обработки сообщений, либо делать внутри "главного модуля" (EXE-шки) один единственный поток (или 2, описанных выше), и при загрузке модуля регистрировать сообщения данного модуля так, чтобы при поступлении сообщения MSG_A вызывалась ф-ция из модуля MOD_A.

Дерево объектов общее, т.к. собственно решается задача совместной работы множества подключаемых модулей над одним объектом-"документом".

Автор: Ars 15.9.2003, 11:11
mr.DUDA, если это еще актуально, скинь мне на личку пересказ ТЗ в части описания пользователей (и/или внешних систем), их количества и их требований к скорости доступа к объектам. Подумаю на досуге...

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)