Joining on a thread that fires a delegate

I’m launching a background thread with a FRunnable. In this background thread, I’m using the HttpModule to make an http request periodically. The IHttpInterface uses a delegate for request completion. To that delegate, I’ve attached a lambda that does other work but also triggers an FEvent at the end. This allows me to call my http request “synchronously” in my thread because I FEvent::Wait() in the thread’s loop, and call FEvent::Trigger() in my http request completion delegate. In a blueprint, I have a function which calls Kill() on the FRunnableThread. Kill() calls join on the thread internally, so it blocks until the thread completes.

That’s the setup, here is the problem: I am experiencing a deadlock. From poking around at the thread stacks in gdb, it appears that the following happening:

  1. My background thread makes the http request and blocks using FEvent::Wait()
  2. The BP that calls Kill()/Join() on my thread executes on the game thread
  3. Kill()/Join() blocks waiting for my thread, so the game thread is now blocked
  4. The http call FHttpRequestCompleteDelegate can never fire, because delegates appear to be scheduled on the game thread, which is blocked from the BP
  5. My thread can never exit, because it is FEvent::Wait()ing on a trigger that will never fire
  6. Background thread is waiting on Game thread which is waiting on background thread
  7. Deadlock

I’ve been thinking about what I am doing wrong specifically, and nothing is standing out. I can put in some ugly hacks to work around the deadlock, but they will be confusing and error prone. The main problem that I see is that I am unable to tell a delegate to fire on a thread not also shared with blueprints. If I was able to do that, the deadlock wouldn’t exist.

Does anyone have any suggestions?

The problem actually seems to boil down to a more simplified problem:

How do you call a function in BP that fires and waits on a delegate? It doesn’t seem possible, because the BP function call blocks the game thread, preventing any delegates from being processed before the function returns.

My “solution” was to write my own libcurl wrapper that is synchronous. Because my wrapper is synchronous and never needs to run a delegate on the game thread, the deadlock is avoided.

What’s the use case for a synchronous http request? I imagine that would lag the game. A blueprint async action node could have an extra output that fires when the async request completes normally, avoiding the lag.

@Zeblote I have a class, call it MonitorThing, that polls an external http service every N seconds in a background thread. I don’t expose any of the threading or http semantics to the user of the class…they just call MonitorThing::GetStatus() and get a cached value of the service’s status. So the http call can be synchronous and not lag the game, because it’s already running in a thread.

The original problem was the fact that using the async Http library inside of a thread is prone to deadlocks because it tries to schedule its delegate call on the game thread, not the original thread it was initiated from.

You can do this more simply without any locking.

  • spawn thread that does the periodic HTTP request
  • have that thread cache anything that needs to be sent to the Game
  • check for any incoming cached data during Tick() on Game thread

Yep, that works and it’s what I ended up doing by making the http call synchronous in my thread. I just store the results of the synchronous call onto a thread safe variable, and pick it up from the game thread when I need it.

I wish delegates had some more flexibility about where they are fired though. I can’t be the only person who has tried to use a delegate-firing object from within a thread.

The problem are not the delegates. They actually have no thread related features at all and simply execute in the thread you call the execute function in. The http manager is explicitly sending work to the game thread to execute it there since that’s the most common I guess…

I was sure you were wrong about this, but I ran a test:


uint32 AMyActor::Runnable::Run()
{
    uint32 ThreadId = FPlatformTLS::GetCurrentThreadId();
   FString ThreadName = FThreadManager::Get().GetThreadName(ThreadId);
    UE_LOG(LogTemp, Warning, TEXT("background thread name %s"), *ThreadName);
    Delegate.BindLambda(](){
        uint32 ThreadId = FPlatformTLS::GetCurrentThreadId();
       FString ThreadName = FThreadManager::Get().GetThreadName(ThreadId);
        UE_LOG(LogTemp, Warning, TEXT("delegate callback in thread name %s"), *ThreadName);
    });
    Delegate.Execute();
    return 0;
}

A background thread launched on BeginPlay, which binds and fires a delegate in the thread. The output:


[2019.08.26-17.33.31:279][839]LogTemp: Warning: background thread name BgThread
[2019.08.26-17.33.31:279][839]LogTemp: Warning: delegate callback in thread name BgThread

So it appears you’re right…I was assuming that the delegates themselves put themselves on a queue when they Broadcast/Execute, and the Gamethread then runs through that queue and processes the callbacks.

Digging a little deeper though, it looks like the HttpManager ticks on the game thread, which means finishing requests and firing the FHttpRequestCompleteDelegate on the game thread. I guess this is what you meant when you said “explicitly sending the work to the game thread” ? I took that to mean the IO transfer and was thinking “no way, that would lag the game so much” :slight_smile: The IO transfer itself is done in a background thread managed by HttpManager

So I guess the issue I was having isn’t with the delegates themselves, but when the HttpManager decides to fire the delegates, which is on the game thread, causing my deadlock.