📜 ⬆️ ⬇️

Workflow organization: engine state management

This article is a continuation of the article - Organization of workflows: synchronization channel . Continuation was born as an attempt to write an example of using the approach with synchronous messages.

In this part, I want to show by example how to manage and display the state of the engine with the workflow using synchronous messages between the threads. And show how to bypass the problem of interlocking threads when closing an application.

Let's go back to the example from the previous article. We have a graphical interface that displays the state of the engine with the workflow. Suppose the engine can be started, stopped, paused and, accordingly, removed from the pause. To implement this behavior, the easiest way to apply something similar to the design patterns of the state machine and the observer.

To begin with, we will define the set of engine states. The engine will be asynchronous, so the transition states will also be in the state set. I got the following set of states: NotStarted, StartPending, Started, PausePending, Paused, ResumePending, StopPending.
')
These states will primarily be used to change the state of a GUI. Ie, the GUI will receive a notification about the state change of the engine, and display this state accordingly. For example, when switching to the NotStarted state, the GUI should show the “Start” button, and the “Pause” buttons “Continue” and “Stop” should be disabled. Accordingly, when switching to the Paused state, the “Start” and “Pause” buttons will be locked, and the “Continue” and “Stop” buttons must be unlocked.

Let's take a look at how the engine status notification handler can look like, using the example of the WTL dialog with the corresponding control buttons:
void CMainDlg::OnStateChanged(EngineState::State state) { //    ,     GUI     CSyncChannel, //              //   Pending ,   ,       //          GetDlgItem(IDC_BUTTON_START).EnableWindow(FALSE); GetDlgItem(IDC_BUTTON_STOP).EnableWindow(FALSE); GetDlgItem(IDC_BUTTON_PAUSE).EnableWindow(FALSE); GetDlgItem(IDC_BUTTON_CONTINUE).EnableWindow(FALSE); switch (state) { case EngineState::NotStarted: GetDlgItem(IDC_BUTTON_START).EnableWindow(TRUE); break; case EngineState::Started: GetDlgItem(IDC_BUTTON_STOP).EnableWindow(TRUE); GetDlgItem(IDC_BUTTON_PAUSE).EnableWindow(TRUE); break; case EngineState::Paused: GetDlgItem(IDC_BUTTON_STOP).EnableWindow(TRUE); GetDlgItem(IDC_BUTTON_CONTINUE).EnableWindow(TRUE); break; } } 

To manage its state, the engine will have a corresponding set of functions: Start, Pause, Resume, Stop, which in the dialog class will be called from the appropriate button handlers.

Due to the use of synchronous messages, the transition from one state to another will be carried out synchronously with the flow of the GUI. That is, while the GUI thread is in the handler for pressing the “Start” button, the engine cannot transition to the Started or Paused state asynchronously, it waits until the handler for pressing the “Start” button is completed. This greatly simplifies the management of engine states.

Transition to pending states, such as StartPending, is performed inside a call to control functions, such as Start, and therefore, after exiting the Start function, the engine will have StartPending status. That is, notification of the transition to the StartPending state will be triggered synchronously, even before the completion of the Start function call.

Let's look at the implementation of the engine.
I quote the engine class completely, because When working with multithreading, any missing part can play a big role.
 //Engine.h namespace EngineState { enum State { NotStarted, StartPending, Started, PausePending, Paused, ResumePending, StopPending }; }; class IEngineEvents { public: virtual void OnStateChanged(EngineState::State state) = 0; }; class CEngine { public: CEngine(IEngineEvents* listener); ~CEngine(void); public: //      void Start(); void Stop(); void Pause(); void Resume(); public: // GUI          bool ProcessMessage(MSG& msg); private: void WaitForThread(); static DWORD WINAPI ThreadProc(LPVOID param); void Run(); bool CheckStopAndPause(); void ChangeState(EngineState::State state); void OnStateChanged(EngineState::State state); private: CSyncChannel m_syncChannel; //        IEngineEvents* m_listener; HANDLE m_hThread; volatile EngineState::State m_state; }; 

 // Engine.cpp CEngine::CEngine(IEngineEvents* listener) : m_listener(listener), m_hThread(NULL), m_state(EngineState::NotStarted) { m_syncChannel.Create(GetCurrentThreadId()); } CEngine::~CEngine(void) { //      ,      m_state = EngineState::StopPending; //  ,    . //          GUI,   . //  ,       , //  m_syncChannel,      . m_syncChannel.Close(); //     . WaitForThread(); } void CEngine::WaitForThread() { if (m_hThread) { //    . //         : // StopPending  NotStarted _ASSERT(m_state == EngineState::StopPending || m_state == EngineState::NotStarted); //      DWORD waitResult = WaitForSingleObject(m_hThread, INFINITE); _ASSERT(waitResult == WAIT_OBJECT_0); //    ,    HANDLE   CloseHandle(m_hThread); m_hThread = NULL; } } void CEngine::Start() { //   ,   GUI  //        ,      //      if (m_state == EngineState::NotStarted) { //   Start   ,    , //         //       ,     WaitForThread(); //    , //   this,      Run    m_hThread = CreateThread(NULL, 0, CEngine::ThreadProc, this, 0, NULL); if (m_hThread) { //     StartPending //       ChangeState(EngineState::Started) //  ,      SyncChannel, //       , //    ,        "Start" //     StartPending    , //    Started    ChangeState(EngineState::StartPending); } } } void CEngine::Stop() { //   ,   GUI  if (m_state != EngineState::NotStarted && m_state != EngineState::StopPending) { //   ,          ChangeState(EngineState::StopPending); } //    ,     //      //       ChangeState(EngineState::Stopped) //         WaitForThread } void CEngine::Pause() { //   ,   GUI  if (m_state == EngineState::Started) { //    PausePending     Started ChangeState(EngineState::PausePending); } } void CEngine::Resume() { //   ,   GUI  if (m_state == EngineState::Paused) { //    ResumePending     Paused ChangeState(EngineState::ResumePending); } } bool CEngine::ProcessMessage(MSG& msg) { // GUI         return m_syncChannel.ProcessMessage(msg); } DWORD WINAPI CEngine::ThreadProc(LPVOID param) { //    ,   //        Run reinterpret_cast<CEngine*>(param)->Run(); return 0; } void CEngine::Run() { //  GUI  ,     ChangeState(EngineState::Started); for (;;) { //    ,   -  //          if (!CheckStopAndPause()) { break; } //    -  ,    :) Sleep(1000); } //   ,     ChangeState(EngineState::NotStarted); //     NotStarted   //  -        //        WaitForSingleObject } bool CEngine::CheckStopAndPause() { //       . //        GUI. if (m_state == EngineState::StopPending) { //    Stop,     return false; } if (m_state == EngineState::PausePending) { //     , //    GUI     //        ChangeState(EngineState::Paused); while (m_state == EngineState::Paused) { Sleep(100); } if (m_state == EngineState::StopPending) { //    Stop,     return false; } //      //    GUI         ChangeState(EngineState::Started); } //       return true; } void CEngine::ChangeState(EngineState::State state) { //       , //    GUI ,  m_syncChannel m_syncChannel.Execute(boost::bind(&CEngine::OnStateChanged, this, state)); } void CEngine::OnStateChanged(EngineState::State state) { //      GUI ,   m_state //     m_state       //      ,       //             GUI  m_state = state; m_listener->OnStateChanged(m_state); } 

Using the CEngine class.
In my example, the CEngine class object is declared a member of the WTL dialog class.
The set of dialogue functions comes down to handlers for pressing the “Start”, “Stop”, “Continue”, and “Pause” keys, which resemble the corresponding functions of the CEngine object.

Also, the dialog class subscribes to the engine state change notifications using the IEngineEvents interface, the implementation of the OnStateChanged function of this interface was given at the beginning of the article.

To provide streaming messages to the CEngine class, the dialog class sets itself as a window message filter using standard WTL :: CMessageLoop :: AddMessageFilter methods, and implements the WTL :: CMessageFilter interface:
 class CMessageFilter { public: virtual BOOL PreTranslateMessage(MSG* pMsg) = 0; }; 

The implementation of the PreTranslateMessage function is reduced to re-calling the CEngine :: ProcessMessage function.

To solve the problem of interlocking threads when closing an application, no additional action is required in the dialog class. This problem is completely solved by using CSyncChannel :: Close in the CEngine class destructor. Thus, the workflow is fully encapsulated inside the CEngine class, which provides tangible benefits when working with this class.

Source: https://habr.com/ru/post/141783/


All Articles