15 мая 2023 года "Исходники.РУ" отмечают своё 23-летие!
Поздравляем всех причастных и неравнодушных с этим событием!
И огромное спасибо всем, кто был и остаётся с нами все эти годы!

Главная Форум Журнал Wiki DRKB Discuz!ML Помощь проекту


Создание классов с потоками


Автор: Покрашенко Александр.

Встречая в форумах вопросы о том, как создавать классы с потоками, я решил написать эту статью.

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

Конечно можно использовать класс MFC, но не все приложения используют эту библиотеку.

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

Итак, создадим базовый класс:

class CMyThread
{
            friend DWORD WINAPI ThreadProc(LPVOID lpParameter); // пояснения см. ниже
            HANDLE m_hThread;

public:
            DWORD Terminate(BOOL bCritical=FALSE);
            BOOL Execute();
            CMyThread();
            virtual ~CMyThread(); 

protected:

            DWORD m_dwID;
            virtual DWORD ThreadFunc()=NULL; // эта функция будет переопределяться в наследующих классах
            BOOL m_bTerminated; // этот флаг будет указывать потоку, что пора закругляться и завершать свю работу корректно.
};

Мы описали класс со всеми необходимыми функциями: запуска потока, остановки. 

Теперь напишем глобальную потоковую функцию: 

extern DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
            CMyThread *pMyThread=(CMyThread *)lpParameter;
            return pMyThread->ThreadFunc();
}

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

CMyThread::CMyThread()
{
            m_bTerminated=FALSE;
            m_hThread=NULL;
}

CMyThread::~CMyThread()
{
   Terminate(FALSE); // при уничтожении объекта, убедимся, что поток завершен или завершим его
}

Теперь перейдем к самому главному - запуску нашего потока:

BOOL CMyThread::Execute()
{
            if (m_hThread)
            {
                        return FALSE;
            }
            m_hThread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE) ThreadProc,this,0,&m_dwID);

    return (m_hThread==NULL)?FALSE:TRUE;

}

В качестве параметров мы передали адрес глобальной функции и указатель на наш объект!

Эта функция завершает работу потока. Сперва она предупреждает поток о завершении, но если он сам не завершается, "убьем" его.

DWORD CMyThread::Terminate(BOOL bCritical)
{
            if (m_hThread==NULL)
                        return (DWORD)-1;

            m_bTerminated=TRUE;
            DWORD dwExitCode;

            if (!bCritical)
            {
                        for(int i=10;i>0;i--)
                        {
                                   GetExitCodeThread(m_hThread,&dwExitCode);
                                   if (dwExitCode==STILL_ACTIVE)
                                               Sleep(25);
                                   else
                                               break;
                        }
            }

            GetExitCodeThread(m_hThread,&dwExitCode);
            if (dwExitCode==STILL_ACTIVE)
                        TerminateThread(m_hThread,-1);
            GetExitCodeThread(m_hThread,&dwExitCode);
            CloseHandle(m_hThread);
            m_hThread=NULL;
            m_bTerminated=FALSE;
            return dwExitCode;
}

Вот и все, базовый класс готов. Теперь создаем наш класс, с необходимыми характеристиками, функцией потока и наследуем базовый класс:

class CMathThread : public CMyThread
{
public:
            double m_dValue;
            CMathThread();
            virtual ~CMathThread();

protected:
            DWORD ThreadFunc(); // обязательно определяем эту функцию, и помещаем в нее свой код
};

CMathThread::CMathThread()
{
            m_dValue=0;
}

 

Вот эта функция и запустится на выполнение после вызова CMathThread::Execute() :

DWORD CMathThread::ThreadFunc()
{
            for(;;)
            {
                        if (m_bTerminated)  // проверяем, не пора ли закругляться (см. комментарий ниже)
                                   break;

                        m_dValue+=.00001; // собственно вчисления
            }
            return 0;
}

 

Создаем любые классы, переопределяем виртуальную функцию ThreadFunc() и создаем хоть сотню объектов такого типа, передавая им аргументы через члены класса.

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

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

В данном случае я допустил использование переменной для предупреждения потока, т.к. она подразумевает только 2 состояния 0 и не 0. Но считывание переменной m_dValue при работающем потоке чревато. Необходимо использовать Event, Mutex, Semaphore либо другие механизмы для синхронизации с потоком для считывания общих данных. А это, как я уже сказал, совсем другая история…

Примеры:

Исполняемый - 5Кб
Исходники - 13Кб

Покрашенко Александр
Владивосток
31.08.2001
email: bestx@mail.ru