DLL 中的线程在 Synchronize() 上挂起,直到线程终止

Thread in DLL hangs on Synchronize() until thread is terminated

我在 C++Builder XE6 中编写了一个 DLL,它使用一个线程来执行一些后台任务。

这是 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;
    }
}

AddLog() 函数将文本字符串写入日志文件。

DLL 由 VCL 应用程序使用,用 C++Builder 1 编写。

这是 VCL 应用程序中的代码:

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();
}

我 运行 VCL 应用程序,单击 Button1 启动线程,等待几秒钟,然后单击 Button2 停止线程。 之后,日志文件包含:

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)

显然,对 Synchronize() 的调用挂起,直到线程终止。

为什么 Synchronize() 挂起?我该如何解决这个问题?

更新

我找到了一个 similar question,这解释了为什么 Synchronize() 挂起;因为 CheckSynchronize() 没有被调用。

C++Builder 1 没有 CheckSynchronize() 函数,TApplication::Idle() 似乎也不会检查与线程同步相关的任何内容。

我仍然不知道如何解决这个问题。

您是正确的,TThread::Synchronize() 挂起的原因是因为它的请求没有被处理。线程终止时请求得到处理的原因是因为 TThread::WaitFor() 在等待线程完全终止时处理 TThread::Synchronize() 请求。

为了解决这个问题,您的 DLL 必须从 DLL 内部导出第三个调用其自己的 CheckSynchronize() 函数(XE6 中确实存在)的函数,然后 EXE 将需要调用该函数定期,例如在计时器中。

DLL:

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

应用程序:

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

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

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

另一种解决方案是让 DLL 线程调用 OnError 回调 而不用 使用 Synchronize(),然后让 EXE 的处理程序执行自己的同步如所须。例如,在 C++Builder 1 中,您可以使用 Win32 SendMessage() 函数,因为 TThread::Synchronize() 在该版本中既不是 public 也不是 static

DLL:

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

应用程序:

// 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

正确。 CheckSynchronize() 是在 Delphi/C++Builder 6 中引入的,当时 TThread::Synchronize() 是 re-written 以支持 cross-platform(当 Delphi/C++Builder 6 首次引入时short-lived CLX 框架,用于 Windows 和 Linux 以及 cross-platform 版本的 VCL)。

这并不重要,即使 CheckSynchronize() 确实存在于 C++Builder 1 中,因为根本问题是 DLL 和 EXE 不共享 RTL 库的单个实例,所以任何EXE 对其 RTL 的处理不会影响 DLL 的 RTL 中的任何内容,包括处理 Synchronize() 请求。这就是为什么您必须公开访问 DLL 版本的 CheckSynchronize().

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

事实上,确实如此,只是不是你想的那样。在 Delphi/C++Builder 6 之前,TThread::Synchronize() 简单地调用 SendMessage() 发送请求到主 RTL-owned window 运行 UI线程。主线程的标准消息循环将在其正常的消息调度期间处理这些请求。在 Delphi/C++Builder 6 之后不再是这种情况,请求现在被放入队列,主线程定期调用 CheckSynchronize() 来处理队列。