'TIdHTTP::OnAuthorization in multithreaded application

I'm writing a DLL in C++Builder XE6 to communicate with a REST server. The DLL creates a TThread-derived thread to do GET requests in the background, using TIdHTTP. The DLL also exports functions to do GET and POST requests in the main thread, using a second TIdHTTP instance. Both TIdHTTP instances can trigger an OnAuthorization event if the server returns a 401 (Unauthorized) response code.

I've written a function to prompt the user for credentials, called PromptForCredentials(), which shows a standard Windows credentials dialog. This function is called if either instance triggers an OnAuthorization event (in the background thread via Synchronize()). Only thing is, the background thread can trigger an OnAuthorization event while the user is being prompted for credentials during an OnAuthorization event in the main thread and vice versa. This would show two copies of the credentials dialog at the same time.

I can use TCriticalSection to prevent calling PromptForCredentials() twice, so that only one copy of the credentials dialog is shown at a time. That way, if the main thread prompts for credentials, the background thread will block when it also tries to prompt for credentials and vice versa.

If authentication is successful in one thread, the other thread should use the same credentials instead of prompting the user a second time. I can do this by copying the Username and Password properties from one TIdHTTP instance to the other, either at the end of the OnAuthorization event or after the request is completed. However, if a second OnAuthorization event occurs during the first one, the user is still prompted twice for credentials. AFAIK, TCriticalSection doesn't provide a way to check if the lock is already acquired. Otherwise, I could wait until the lock is released and use the credentials without prompting a second time.

How do I prevent prompting the user for credentials twice in succession?

Update

The documentation for TMultiReadExclusiveWriteSynchronizer::BeginWrite() states:

As a rule, a thread should always discard previous samples from protected memory after promoting a read lock to a write lock. However, the calling thread can determine whether a state change has occurred by examining return value of BeginWrite: True if the protected memory has not been written to by another thread or False if another thread may have modified the protected memory.

So, here's what I tried:

#include <System.SysUtils.hpp>

TMultiReadExclusiveWriteSynchronizer* Lock = NULL;

void GetCreds (const String Caption)
{
    Lock->BeginRead();

    try
    {
        // Retrieve globally stored credentials here.

        bool Prompt = Lock->BeginWrite();

        try
        {
            if (Prompt)
                Application->MessageBox(_T("Prompt"), Caption.c_str(), MB_OK);
            else
                Application->MessageBox(_T("Use"), Caption.c_str(), MB_OK);
        }
        __finally
        {
            Lock->EndWrite();
        }
    }
    __finally
    {
        Lock->EndRead();
    }
}

class TMyThread : public TThread
{
    typedef TThread inherited;

    protected:
                void __fastcall DoTask  ();
        virtual void __fastcall Execute ();

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

void __fastcall TMyThread::DoTask ()
{
    GetCreds("Thread");
}

void __fastcall TMyThread::Execute ()
{
    while (!Terminated)
    {
        Sleep(2000);

        if (Terminated)
            break;

        Synchronize(DoTask);
    }
}

void __fastcall TForm1::Button1Click (TObject *Sender)
{
    Lock = new TMultiReadExclusiveWriteSynchronizer();

    try
    {
        TMyThread* Thread = new TMyThread();

        try
        {
            Thread->Start();

            GetCreds("Main");
        }
        __finally
        {
            Thread->Terminate();
            Thread->WaitFor();

            delete Thread;
        }
    }
    __finally
    {
        delete Lock;
    }
}

When I click Button1, I get a message box with caption "Main" and text "Prompt". As expected, the user is prompted for credentials the first time.

With the message box still open, the background thread kicks in after 2 seconds with a (synchronized) call to GetCreds(). Since the main thread acquired a write lock, you would expect the background thread to be blocked on the BeginRead() call and no second message box to appear. However, I still get a second message box with caption "Thread" and text "Prompt".

To test, I switched from TMultiReadExclusiveWriteSynchronizer to TCriticalSection.

GetCreds() now looks like this:

void GetCreds (const String Caption)
{
    Lock->Acquire();

    try
    {
        Application->MessageBox(_T("Prompt"), Caption.c_str(), MB_OK);
    }
    __finally
    {
        Lock->Release();
    }
}

I still get two message boxes, while I should only see the one from the main thread.

What am I doing wrong?

Update 2

As Remy pointed out, TCriticalSection locks are reentrant for the thread that has the lock. If I call GetCreds() directly in the thread, i.e. without Synchronize(), the code in my first update works as expected.

I also found that TCriticalSection has a TryEnter() method, whose return value can be used to check if another thread currently has the lock.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source