1

I wrote a DLL in C++Builder XE6 that uses a thread to do some background task.

Here's the (simplified) code in the DLL:

typedef void (__stdcall* ERRORCALLBACK)();

class TMyThread : public TThread
{
    typedef TThread inherited;

    private:
        void __fastcall DoError ();

    public:
        inline __fastcall TMyThread () : inherited(true) {}

        ERRORCALLBACK OnError;
};

void __fastcall TMyThread::Execute ()
{
    AddLog("Thread started (ID: " + UIntToStr(ThreadID) + ")");

    try
    {
        while (!Terminated)
        {
            if (true)  // Some error condition
            {
                AddLog("Before Synchronize(DoError)");

                if (OnError)
                    Synchronize(DoError);

                AddLog("After Synchronize(DoError)");
            }
        }
    }
    __finally
    {
        AddLog("Thread stopped (ID: " + UIntToStr(ThreadID) + ")");
    }
}

void __fastcall TMyThread::DoError ()
{
    AddLog("DoError() begins");

    try
    {
        OnError();
    }
    catch (...)
    {
    }

    AddLog("DoError() ends");
}

TMyThread* Thread = NULL;

void __declspec(dllexport) __stdcall StartThread (ERRORCALLBACK ErrorProc)
{
    if (!Thread)
    {
        Thread = new TMyThread();
        Thread->OnError = ErrorProc;
        Thread->Start();
    }
}

void __declspec(dllexport) __stdcall StopThread ()
{
    if (Thread)
    {
        AddLog("Stopping thread (ID: " + UIntToStr(Thread->ThreadID) + ")");

        Thread->Terminate();
        Thread->WaitFor();
        delete Thread;
        Thread = NULL;
    }
}

The AddLog() function writes a text string to a log file.

The DLL is used by a VCL application, written in C++Builder 1.

Here's the code in the VCL application:

typedef void (__stdcall* ERRORCALLBACK)();

extern "C"
{
    void __declspec(dllimport) __stdcall StartThread (ERRORCALLBACK ErrorProc);
    void __declspec(dllimport) __stdcall StopThread ();
}

void __stdcall ThreadError ()
{
    Form1->Memo1->Lines->Add("An error occurred!");
}


void __fastcall TForm1::Button1Click (TObject *Sender)
{
    StartThread(ThreadError);
}


void __fastcall TForm1::Button2Click (TObject *Sender)
{
    StopThread();
}

I run the VCL application, click Button1 to start the thread, wait a couple of seconds and click Button2 to stop the thread. Afterwards, the log file contains:

18-05-2022 13:52:22.798: Thread started (ID: 2756)
18-05-2022 13:52:22.804: Before Synchronize(DoError)
18-05-2022 13:52:45.530: Stopping thread (ID: 2756)
18-05-2022 13:52:45.530: DoError() begins
18-05-2022 13:52:45.530: DoError() ends
18-05-2022 13:52:45.530: After Synchronize(DoError)
18-05-2022 13:52:45.530: Thread stopped (ID: 2756)

Apparently, the call to Synchronize() hangs until the thread is terminated.

Why does Synchronize() hang? How do I fix this?

Update

I found a similar question, which explains why Synchronize() hangs; because CheckSynchronize() isn't called.

C++Builder 1 doesn't have a CheckSynchronize() function, nor does TApplication::Idle() seem to check anything related to thread synchronization.

I still have no idea how to fix this.

  • The post you linked to doesn't involve a DLL. However, I did cover this same issue just yesterday (written for Delphi, but applies to C++Builder, too): [Why does TThread not work in a DLL? But works in a VCL Form application](https://stackoverflow.com/questions/72266438/) – Remy Lebeau May 18 '22 at 19:16

1 Answers1

1

You are correct that the reason TThread::Synchronize() hangs is because its request is not being processed. The reason why the request gets processed when the thread is terminated is because TThread::WaitFor() processes TThread::Synchronize() requests while waiting for the thread to fully terminate.

To solve this, your DLL will have to export a 3rd function that calls its own CheckSynchronize() function (which does exist in XE6) from inside the DLL, and then the EXE will need to call that function periodically, such as in a timer.

DLL:

void __declspec(dllexport) __stdcall CheckThreadSyncs ()
{
    CheckSynchronize();
}

App:

extern "C"
{
    ...
    void __declspec(dllimport) __stdcall CheckThreadSyncs ();
}

__fastcall TForm1::TForm1 (TComponent* Owner)
    : TForm(Owner)
{
}

void __fastcall TForm1::Timer1Timer (TObject* Sender)
{
    CheckThreadSyncs();
}

An alternative solution is to make the DLL thread call the OnError callback without using Synchronize(), and then make the EXE's handler perform its own synchronization as needed. For instance, in C++Builder 1, you can use the Win32 SendMessage() function, since TThread::Synchronize() was neither public nor static yet in that version.

DLL:

void __fastcall TMyThread::Execute ()
{
    ...
    if (OnError)
        DoError();
    ...
}

App:

// sending to a private hidden HWND because TForm::Handle is not thread-safe!
HWND hThreadErrorWnd = NULL;
const UINT WM_THREAD_ERROR = WM_APP + 1;

void __stdcall ThreadError ()
{
    SendMessage(hThreadErrorWnd, WM_THREAD_ERROR, 0, 0);
}

__fastcall TForm1::TForm1 (TComponent* Owner)
    : TForm(Owner)
{
    hThreadErrorWnd = AllocateHWnd(&ThreadErrorWndProc); 
}

__fastcall TForm1::~TForm1 ()
{
    DeallocateHWnd(hThreadErrorWnd); 
}

void __fastcall TForm1::ThreadErrorWndProc (TMessage &Message)
{
    if (Message.Msg == WM_THREAD_ERROR)
        Memo1->Lines->Add("An error occurred!");
    else
        Message.Result = DefWindowProc(hThreadErrorWnd, Message.Msg, Message.WParam, Message.LParam);
}

C++Builder 1 doesn't have a CheckSynchronize() function

Correct. CheckSynchronize() was introduced in Delphi/C++Builder 6, when TThread::Synchronize() was re-written for cross-platform support (when Delphi/C++Builder 6 first introduced the short-lived CLX framework for targeting Windows and Linux with a cross-platform version of the VCL).

Not that it matters, even if CheckSynchronize() did exist in C++Builder 1, since the root problem is that the DLL and EXE are not sharing a single instance of the RTL library, so anything the EXE does with its RTL doesn't affect anything in the DLL's RTL, including processing Synchronize() requests. That is why you have to expose access to the DLL's version of CheckSynchronize().

nor does TApplication::Idle() seem to check anything related to thread synchronization.

Actually, it does, just not the way you think. Prior to Delphi/C++Builder 6, TThread::Synchronize() simply called SendMessage() to send a request to a hidden RTL-owned window running in the main UI thread. The main thread's standard message loop would handle those requests during its normal message dispatching. That is no longer the case in Delphi/C++Builder 6 onwards, where the request is now put into a queue, and the main thread calls CheckSynchronize() periodically to process the queue.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thanks for your help, Remy. Very useful information on how synchronization actually works in both C++Builder versions. I decided to go for your alternative solution; omit `Synchronize()` and send a custom message. – Martin Nijhoff May 19 '22 at 06:59