Многопоточные классы

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

Первоначальный вариант был таков:
// структура параметров передаваемых в поток
struct ArgsThread
{
  int *tmp1;
  char *tmp2;
  int indata1;
  char indata2;
};

// непосредственно поток выполняющий вычисления
unsigned long CalculationThread(void *arg)
{
  ThreadArgs* args = reinterpret_cast<ThreadArgs*>(arg); // выкапываем входные данные
  
  // что-то сумбурно и долго считаем
}

// класс который требуется реализовать
class MyCalc
{
  private:
    void *HandleThread;
    unsigned long IdThread;
    
    int tmp1;
    char tmp2;
    
  public:
    MyCalc(int indata1, char indata2)
    {
      ArgsThread* args=new ArgsThread();
      
      args->tmp1 = &tmp1;  // упаковываем данные
      args->tmp2 = &tmp2;
      args->indata1 = indata1;
      args->indata2 = indata2;
      
      HandleThread = CreateThread(NULL, 0, &CalculationThread, args, 0, &IdThread); // создаем поток
    };
    
    ~MyCalc
    {
      TerminateThread(HandleThreade, NULL);  // завершаем выполнение потока
      CloseHandle(HandleThread);        // закрываем хендл
    };
};


Поток находился вне класса, что, в принципе, удовлетворяло задаче. После раздумий пришла идея реализации потока внутри класса, а точнее, один из методов должен стать функцией, которую выполнял бы поток.
Недолго думая (что очень зря), родился такой код:
class MyCalc
{
  private:
    void *HandleThread;
    unsigned long IdThread;
    
    int tmp1;
    char tmp2;
  protected:
    unsigned long CalculationThread(void *arg)
    {
      // что-то сумбурно и долго считаем
    }
    
  public:
    MyCalc(int indata1, char indata2)
    {
      HandleThread = CreateThread(NULL, 0, &CalculationThread, this, 0, &IdThread); // создаем поток
    };
    
    ~MyCalc
    {
      TerminateThread(HandleThreade, NULL);  // завершаем выполнение потока
      CloseHandle(HandleThread);        // закрываем хендл
    };
};


Естественно, во время компиляции я получил ошибку о том, что передаваемые в функцию CreateThread параметры не корректны, а в частности прототип функции потока не соответствовал запрашиваемому типу. На одном из форумов было найдено решение, суть которого заключалась в том, что метод делаем static. Что тоже в принципе удовлетворяло меня, но как оказалось поток не имел доступа ко внтренним данным класса. И в конечном итоге решено было сделать этот метод полноправным членом класса. После полу часа возни с компилятором родилось такое решение:
// объявляем необходимые типы
typedef unsigned long (__stdcall *ThrdFunc)(void *arg);    // прототип функции потока
typedef unsigned long (__closure *ClassMethod)(void *arg);    // прототип метода класса

// данное объединение позволяет решить несостыковку с типами
typedef union
{
  ThrdFunc Function;
  ClassMethod Method;
}tThrdAddr;

// для гибкости использования храним все в одном месте
typedef struct
{
  void* Handle;        // хэндл потока
  tThrdAddr Addr;        // адрес
  unsigned long Id;      // ID потока
  unsigned long ExitCode;    // код выхода
}tThrd;

class MyCalc
{
  private:
    tThrd MyThread;
    
  protected:
    unsigned long ThrdHandle(void *arg)
    {
      // что-то сумбурно и долго считаем
    };
    
  public:
    MyCalc()
    {
      MyThread.Addr.Method = &ThrdHandle; // тут главная магия
      MyThread.Handle = CreateThread(NULL, 0, MyThread.Addr.Function, this, 0, &MyThread.Id);
      GetExitCodeThread(MyThread.Handle, &MyThread.ExitCode);
    };
    
    ~MyCalc()
    {
      if(MyThread.Handle)
      {
        TerminateThread(MyThread.Handle, MyThread.ExitCode);
        CloseHandle(MyThread.Handle);
      }
    };
};


Таким образом наш поток становится и членом класса одновременно, со всем вытекающими ООП возможностями. Отпадает необходимость в «огороде» из упаковки и распаковки аргументов содержащих данные, поскольку теперь есть доступ ко всем методам и полям класса. Таких методов-потоков можно реализовать бесконечно много, и пользователь класса не сможет получить к ним хоть какой-нибуть доступ (правила private и protected) или случайно вызвать выполнение кода содержащегося в методе-потоке.

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

UPD: данный код генерировался и оттачивался в среде C++ Builder, поэтому в прототипе метода присутствует __closure. изменяя прототип вы можете без больших потерь и изменений использовать данный код в других компиляторах.

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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

    0
    Спасибо. Когда то давно с этим же сталкивался.
      0
      Можно было, конечно, пойти по пути наследования какого-нибудь готового класса из таких библиотек как: VCL (классTThread) иди Qt (классQThread), но тогда мы бы ограничивались лишь реализацией одного единственного потока в рамках данного класса. А тут мы получаем возможность реализовывать многопоточные классы, которые с легкостью переносятся на другие платформы.
        +1
        Свою проблему я как раз решил переходом на Qt.
      +3
      Стоило бы упомянуть что __closure – расширение языка в реализации C++ Builder, подобное лихачество непереносимо и небезопасно.
        0
        Полностью с вами согласен, поскольку данный код оттачивался в среде C++ Builder, в примере было использовано данное выражение. Справедливо, что для других компиляторов оно будет свое и прототип метода класса изменится.
        +7
        а не было бы проще это сделать как-то так?
        class MyCalc
        {
        private:
        	void *HandleThread;
        	unsigned long IdThread;
        	
        	int tmp1;
        	char tmp2;
        
        	static DWORD WINAPI ThreadF(void *arg)
        	{
        		return ((MyCalc*)arg)->CalculationThread();
        	}
        	
        protected:
        	unsigned long CalculationThread()
        	{
        		/////////
        	}
        	
        public:
        	MyCalc(int indata1, char indata2)
        	{
        		HandleThread = CreateThread(NULL, 0, ThreadF, this, 0, &IdThread);
        	};
        	
        	~MyCalc()
        	{
        		TerminateThread(HandleThread, NULL);
        		CloseHandle(HandleThread);
        	};
        };
        
          0
          ваше решение абсолютно справедливо. в своем примере я лишь хотел показать, как сделать потоком метод класса, со всеми плюшками ООП. ваш случай не защищен от вызова функции ThreadF только классом. к тому же она не является его членом, хоть и описана внутри.
            +1
            если ThreadF была бы friend, то да, можно было бы вызвать её из вне класса. в моём примере она является static и находится в блоке private, что как раз и защищает её от вызова из вне и сохраняет её принадлежность классу. по крайней мере в Visual Studio это работет по такому принципу
              –1
              в C++ Builder ваш пример потребовал необходимого преобразования типов, или же объявления ThreadF как __stdcall. также в вашем случае вы не имеете полного доступа к данным класса непосредственно из потока без указания пренадлежности к объекту класса, поэтому вынужденны делать дополнительный метод. в моем примере достаточно лишь выполнить необходимое преобразование типов для адресов метода и функции, после чего метод становится потоком и может получать беспрепятственный доступ данным класса. к тому же при использовании static при динамическом создании и удалении экземпляра класса есть вероятность, что метод ThreadF останется в памяти, что может грозить некоторыми ее утечками.
            0
            __closure — если бы он еще всеми компиляторами поддерживался бы.
            Так что да, лучше не занимать любовью себе мозги и передавать указатель на this в аргументе.
              0
              мой способ не претендует на оригинальность. вы всегда можете воспользоваться другими или привести прототип метода к необходимому типу, это ничуть не усложнит и почти не изменит код.
            +1
            Тут уже писали — использовать __closure — это читерство! :) Существует стандартный метод — попробуйте использовать такое колдунство, как указатель на метод класса, тогда код станет собираться и работать с любым компилятором.
              0
              как уже говорилось выше и в топике, вы можете изменять прототип метода как вам угодно. я лишь показал один из возможных примеров реализации.
                –1
                Ну зачем везде городить ифдефы, если можно сразу всё написать по-человечески?
                  0
                  в чем же тогда заключается не человечность примера из топика, и где вы увидели в нем использование препроцессора?
                    0
                    Ну приведите пример без __closure для Visual C++
                      0
                      не имею под рукой данного компилятора, но думаю это будет работать:
                      typedef unsigned long (ClassName:: *ClassMethod)(void *arg); // прототип метода класса ClassName
                      0
                      А препроцессоры придется ставить именно для определения компилятора например.
                        0
                        а зачем его определять, например, если разработка ведется в конкретной среде с, вполне, явным компилятором, к чему тогда использовать препроцессор для определения его типа, или вы при написании кода одновременно используете множество компиляторов? согласен что его использование возможно для обеспечения кроссплатформенности, но в рабочем проекте, который уже отлажен и написан под вполне конкретные условия, не вижу смысла добиваться кросскомпиляции.
                          0
                          >а зачем его определять, например, если разработка ведется в конкретной среде с, вполне, явным компилятором

                          Так говорят только программисты-неудачники. Увы, но во всех книжках, на всех учебных курсах говорят о том, что нужно писать масштабируемый и переносимый код, чтобы не иметь геморроя в будущем.
                +1
                Пожалуй, это стоит перенести в блог «Ненормальное программирование».
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    с boost::thread я банально не очень хорошо знаком. а beginthread(ex) не использую потому, что пример тривиальный и лишь для наглядности был приведен, но также и по причине того, что в потоке не используются вызовы CRT, в остальном согласен — лучше использовать beginthread(ex).
                    0
                    Бред какой-то

                    >UPD: данный код генерировался и оттачивался в среде C++ Builder, поэтому в прототипе метода присутствует __closure. изменяя прототип вы можете без больших потерь и изменений использовать данный код в других компиляторах.

                    Если бы я увидел такое в коде коллеги, послал бы его переписывать всё нафиг.
                      0
                      за union'ы впрочем тоже.
                        0
                        Согласен. Всё это вывглядит как кастыли. Но он молодец, что написал статью на хабр. Главное, чтобы он всю критику нормально воспринял. Эта статья с комментариями довольно поучительна!
                          0
                          с костылем соглашусь. но ведь это работает. и вполне стабильно. просто на момент написания данного фрагмента это решение было одним из явных и реализуемых на мой взгляд. кто-то решил бы по другому и интересно было бы взглянуть как.
                          0
                          а что не так в них, и вообще во всем что в топике, почему сразу бред? можно увидеть вполне конкретные указания на наличие ошибок или тех мест где бред, или других возможных вариантов решения задачи? интересно было бы прочитать и обоснование ваших выводов.
                            0
                            Юзать нормальные API например. И не считать себя умнее компилятора. union'ы и reinterpret_cast'ы вовсе не для таких случаев придумали.
                        • НЛО прилетело и опубликовало эту надпись здесь
                        • НЛО прилетело и опубликовало эту надпись здесь
                          • НЛО прилетело и опубликовало эту надпись здесь
                            • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                спасибо за критику. признаю ошибки. про TerminateThread даже не догадывался о возможных последствиях. но тогда к вам вопрос. как же корректно поступить с завершением и уничтожением потока в данном случае?
                                • НЛО прилетело и опубликовало эту надпись здесь
                            0
                            Наверное это можно было бы реализовать следующим образом, только зачем?..
                            template<typename T>
                            struct tThrd
                            {
                                typedef DWORD(__thiscall T::* PMethod)();
                                static DWORD WINAPI Function(PVOID pParam)
                                {
                                    return (((tThrd*)pParam)->pThis->*((tThrd*)pParam)->pMethod)();
                                };
                                T*          pThis;
                                PMethod     pMethod;
                                HANDLE      Handle;
                            };
                            
                            class MyCalc
                            {
                            public:
                                MyCalc()
                                {
                                    // здесь нет никакой магии :)
                                    MyThread.pThis = this;
                                    MyThread.pMethod = &MyCalc::ThrdHandle;
                                    MyThread.Handle = CreateThread(NULL, 0, MyThread.Function, &MyThread, 0, NULL);
                                }
                                ~MyCalc()
                                {
                                    WaitForSingleObject(MyThread.Handle, INFINITE);
                                    CloseHandle(MyThread.Handle);
                                }
                            public:
                                DWORD ThrdHandle()
                                {
                                    // что-то сумбурно и долго считаем
                                    Sleep(10000);
                                    return 1;
                                }
                            private:
                                tThrd<MyCalc> MyThread;
                            };
                            
                              0
                              или даже вот так:
                              template<typename T, DWORD(__thiscall T::* pMethod)()>
                              DWORD WINAPI Function(PVOID pParam)
                              {
                                  return ((T*)pParam->*pMethod)();
                              };
                              ...
                              CreateThread(NULL, 0, Function<MyCalc, &MyCalc::ThrdHandle>, this, 0, NULL);
                              

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

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