Processing events while waiting for thread to join

If you are using the main C++ distribution of wxWidgets, Feel free to ask any question related to wxWidgets development here. This means questions regarding to C++ and wxWidgets, not compile problems.
Post Reply
charvey1
Knows some wx things
Knows some wx things
Posts: 30
Joined: Sun Jan 09, 2022 6:39 am

Processing events while waiting for thread to join

Post by charvey1 »

I want to move a long intensive calculation into another thread so that I can provide GUI updates (by posting events), and also allow the user to stop the calculation if they want. I'm aware of the alternatives using either wxYield or wxIdleEvent, but I think threads will work best for my application.

I need to use join()'able threads - the result of the calculation is needed before anything else can happen. Detaching them creates a ton of problems I don't want.

My question is, is there any way events can still be handled when using std::thread and join()? Right now, the stop event is only processed after the thread has joined.

If not, do I understand correctly that event handling will happen while waiting for joinable wxThreads if Wait(wxTHREAD_WAIT_YIELD) is called on it?

I can switch to using wxThread if necessary - it's just a bit of pain to switch everything over from std::thread. I'd rather use std::thread if possible.

Thanks for any advice!
OS: Windows 10 Enterprise
Compiler: G++ 13.2.0
WX version: 3.2.2.1
New Pagodi
Super wx Problem Solver
Super wx Problem Solver
Posts: 466
Joined: Tue Jun 20, 2006 6:47 pm
Contact:

Re: Processing events while waiting for thread to join

Post by New Pagodi »

My question is, is there any way events can still be handled when using std::thread and join()?
Unfortunately no. join is a blocking operation, so it will block processing of the event queue if called in the main thread.

If you need to pass information from a secondary thread, you can use wxThreadEvent (it works with std::thread objects) or using the CallAfter method from wxEvtHandler. The catch is that you'll need a wxEvtHandler object that is guarenteed to outlive the thread. The wxThread documentation shows one way to do this which could be adapted to work with a std::thread object.
charvey1
Knows some wx things
Knows some wx things
Posts: 30
Joined: Sun Jan 09, 2022 6:39 am

Re: Processing events while waiting for thread to join

Post by charvey1 »

Thank you New Pagodi. I think I’ve understood.
  • Processing the event queue inside the main thread is not possible while waiting for std::thread to join.
  • Passing information from the secondary thread to the main thread is possible using wxThreadEvents.
  • Can I process the event queue inside the main thread while waiting for a joinable wxThread to join if I set wxTHREAD_WAIT_YIELD? I think yes, but want to be sure before I start converting my code to use wxThread.
Thanks again :)
OS: Windows 10 Enterprise
Compiler: G++ 13.2.0
WX version: 3.2.2.1
New Pagodi
Super wx Problem Solver
Super wx Problem Solver
Posts: 466
Joined: Tue Jun 20, 2006 6:47 pm
Contact:

Re: Processing events while waiting for thread to join

Post by New Pagodi »

If you're already using std::thread, I wouldn't convert to wxThread. wxThread was developed before std::thread existed and even the developer whose currently the main contributor to wxWidgets suggests using std::thread instead.

I think the best strategy for joinable threads is to signal when they are done (using wxThreadEvent or call after) and wait until that time to call join. That way, join will return instantly and not block the main thread.
charvey1
Knows some wx things
Knows some wx things
Posts: 30
Joined: Sun Jan 09, 2022 6:39 am

Re: Processing events while waiting for thread to join

Post by charvey1 »

If you're already using std::thread, I wouldn't convert to wxThread.
This is great to know. I was under the mistaken impression that wxThread was preferred to std::thread. I certainly prefer to use std::thread as this is what I know best.
I think the best strategy for joinable threads is to signal when they are done (using wxThreadEvent or call after) and wait until that time to call join. That way, join will return instantly and not block the main thread.
I'll give something like this a go. I need to think carefully about how to implement it. I have quite a complex design: The processing calculation can either be called directly by the user (starting a new thread), or called by another function (in which case it will run instead an existing thread). It also returns an object. I can't see how I can make this work with events. I think what might work for me is to set a variable at the end of processing (e.g. complete=true), then instead of calling join(), monitor this variable in a loop (maybe with while(!complete) { ... } ) that contains wxYield(), and possibly a wxMilliSleep(10) too, and then call join() afterwards, which will return instantly. I think looping with 'while' to monitor a variable must be terrible, but I think there is probably a standard c++ way, and I'll look for this.

Thanks again for the help.
OS: Windows 10 Enterprise
Compiler: G++ 13.2.0
WX version: 3.2.2.1
PB
Part Of The Furniture
Part Of The Furniture
Posts: 4193
Joined: Sun Jan 03, 2010 5:45 pm

Re: Processing events while waiting for thread to join

Post by PB »

IMO, using a thread and a loop is not a great combination.

I would still prefer the usual way, i.e., the worker thread communicating with the UI one using events.

For example, something like this (the old way, no promises/futures/lambdas), may appear too long but it is actually very simple:

Code: Select all

#include <atomic>
#include <chrono>
#include <memory>
#include <string>
#include <thread>

#include <wx/wx.h>

using namespace std;

// wxDECLARE_EVENT macros should normally be in a .h file
wxDECLARE_EVENT(wxEVT_COMPUTATION_STARTED, wxThreadEvent);
wxDECLARE_EVENT(wxEVT_COMPUTATION_PROGRESSED, wxThreadEvent);
wxDECLARE_EVENT(wxEVT_COMPUTATION_CANCELLED, wxThreadEvent);
wxDECLARE_EVENT(wxEVT_COMPUTATION_COMPLETED, wxThreadEvent);

wxDEFINE_EVENT(wxEVT_COMPUTATION_STARTED, wxThreadEvent);
wxDEFINE_EVENT(wxEVT_COMPUTATION_PROGRESSED, wxThreadEvent);
wxDEFINE_EVENT(wxEVT_COMPUTATION_CANCELLED, wxThreadEvent);
wxDEFINE_EVENT(wxEVT_COMPUTATION_COMPLETED, wxThreadEvent);

struct ComputationResult
{
    string name;
    double value {0.};
};

void Compute(string name, atomic_bool& cancelSignal, wxEvtHandler* eventSink)
{
    const int maxSteps = 10;

    ComputationResult result;
    wxThreadEvent*    evt {nullptr};

    result.name = name;

    evt = new wxThreadEvent(wxEVT_COMPUTATION_STARTED);
    evt->SetString(name);
    wxQueueEvent(eventSink, evt);

    // simulate a computation
    for ( size_t step = 1; step <= maxSteps; ++step )
    {
        if ( cancelSignal )
            break;

        result.value += 1.5;
        this_thread::sleep_for(chrono::milliseconds(500));

        evt = new wxThreadEvent(wxEVT_COMPUTATION_PROGRESSED);

        evt->SetString(name);
        evt->SetInt(step / (double)maxSteps * 100); // percentage done
        wxQueueEvent(eventSink, evt);
    }

    evt = new wxThreadEvent(cancelSignal ? wxEVT_COMPUTATION_CANCELLED : wxEVT_COMPUTATION_COMPLETED);

    evt->SetString(name);
    if ( !cancelSignal )
        evt->SetPayload(result);

    wxQueueEvent(eventSink, evt);
}

class MyFrame : public wxFrame
{
public:
    MyFrame(wxWindow* parent = nullptr) : wxFrame(parent, wxID_ANY, "Test", wxDefaultPosition, wxSize(800, 600))
    {
        wxPanel*    mainPanel = new wxPanel(this);
        wxBoxSizer* mainPanelSizer = new wxBoxSizer(wxVERTICAL);
        wxButton*   button = nullptr;

        button = new wxButton(mainPanel, wxID_ANY, "&Start Computation");
        button->Bind(wxEVT_BUTTON, &MyFrame::OnStartComputation, this);
        mainPanelSizer->Add(button, wxSizerFlags().Expand().DoubleBorder());

        button = new wxButton(mainPanel, wxID_ANY, "&Cancel Computation");
        button->Bind(wxEVT_BUTTON, &MyFrame::OnCancelComputation, this);
        mainPanelSizer->Add(button, wxSizerFlags().Expand().DoubleBorder());

        wxTextCtrl* logCtrl = new wxTextCtrl(mainPanel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
            wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH2);
        wxLog::SetActiveTarget(new wxLogTextCtrl(logCtrl));
        mainPanelSizer->Add(logCtrl, wxSizerFlags().Proportion(1).Expand().DoubleBorder());

        mainPanel->SetSizer(mainPanelSizer);

        Bind(wxEVT_COMPUTATION_STARTED, &MyFrame::OnComputationStarted, this);
        Bind(wxEVT_COMPUTATION_PROGRESSED, &MyFrame::OnComputationProgressed, this);
        Bind(wxEVT_COMPUTATION_CANCELLED, &MyFrame::OnComputationCancelled, this);
        Bind(wxEVT_COMPUTATION_COMPLETED, &MyFrame::OnComputationCompleted, this);

        Bind(wxEVT_CLOSE_WINDOW, &MyFrame::OnClose, this);
    }
protected:
    unique_ptr<thread> m_thread;
    atomic_bool        m_threadCancelSignal;

    void OnStartComputation(wxCommandEvent&)
    {
        static size_t computationNumber = 1;

        if ( m_thread )
        {
            wxLogMessage("There is already a running computation.");
            return;
        }

        wxString computationName = wxString::Format("Computation #%zu", computationNumber++);

        wxLogMessage("Starting computation '%s'...", computationName);
        m_threadCancelSignal = false;
        m_thread = make_unique<thread>(Compute,
            computationName.ToStdString(),
            ref(m_threadCancelSignal), this);
    }

    void OnCancelComputation(wxCommandEvent&)
    {
        if ( !m_thread )
        {
            wxLogMessage("There is no computation running.");
            return;
        }

        StopThread();
    }

    void OnClose(wxCloseEvent&)
    {
        if ( m_thread )
        {
            if ( wxMessageBox("Computation not finished, quit anyway?", "Question", wxYES_NO | wxNO_DEFAULT) != wxYES)
                return;
            StopThread();
        }

        Destroy();
    }

    void StopThread()
    {
        if ( !m_thread )
            return;

        wxLogMessage("Stopping the thread....");
        m_threadCancelSignal = true;
        m_thread->join();
        m_thread.reset();
        wxLogMessage("Thread stopped.");
    }

    void OnComputationStarted(wxThreadEvent& evt)
    {
        wxLogInfo("Computation '%s' started.", evt.GetString());
    }

    void OnComputationProgressed(wxThreadEvent& evt)
    {
        if ( m_thread ) // ignore stray events received after computation cancellation
            wxLogInfo("Computation '%s' progressed: %d%% done.", evt.GetString(), evt.GetInt());
    }

    void OnComputationCancelled(wxThreadEvent& evt)
    {
        wxLogInfo("Computation '%s' cancelled.", evt.GetString());
    }

    void OnComputationCompleted(wxThreadEvent& evt)
    {
        if ( !m_thread )
            return; // cancelled just after completed

        ComputationResult result = evt.GetPayload<ComputationResult>();
        wxLogInfo("Computation '%s' completed: result = %g.", result.name, result.value);
        StopThread();
    }
};


class MyApp : public wxApp
{
public:
    bool OnInit() override
    {
        (new MyFrame())->Show();
        return true;
    }
}; wxIMPLEMENT_APP(MyApp);
Where the worker thread communicates with the UI thread using wxEVT_COMPUTATION_* events and the result is passed as the event payload. Of course, the result can also be passed as a pointer or a variable shared between the threads can be used.

IMO, the biggest issue is that a real computation would probably not be able to check for the cancel signal very often and thus the computation cancellation may be delayed. That is unless one is willing to just kill the thread.

This approach could also be used with a "progress window" blocking the rest of the UI (e.g., a modal dialog or a frame using wxWindowDisabler).
Post Reply