Dispatching to main thread stalls in Vapor 3.2?

I'm prototyping a server with Swift 4.2 and Vapor 3.2.2 on ubuntu 16.04.. I have a dependency in C that I'm invoking and the dependency is trying to run code on the main thread.

I'm successfully running warmup and test code in a test program and in my Vapor server's configure hook – both running on the main thread. In my request/response loop though, the dependency stalls when running the same code path with the same input, apparently because the main thread is blocked. Based on this issue:


I understood that the main thread wasn't blocked anymore. Separately I'm wondering if there's a straightforward way to configure the number of threads available to the dispatch queues.

I'm hoping the community will have some advice for me, and I'm happy to provide more details if that's helpful (I can't talk about what the specific dependency is, but I can provide redacted output and thread states for example).

Thanks a ton!

What kind of dependency is that? Something like a UI framework with an own concept of a runloop or more like a regular C library? Because this statement is unusual:

the (C) dependency is trying to run code on the main thread

Plain C/Posix dependencies don't (can't) usually run code on specific threads unless they come with a hook to let them callback. (there isn't even something like a pthread_get_main()).

Note that the issue linked still blocks the main thread (someone has to block the main thread, otherwise the program would just exit). Instead of hard-blocking using a private lock, it starts the Foundation runloop (which then sits there waiting for runloop events, i.e. blocking the thread).
NIO itself doesn't bother about the main thread, e.g. this is what the demos are doing to keep the servers running: NIOEchoServer.

Without knowing anything specific about your dependency: If you have interdependent tasks in a NIO server (e.g. an endpoint that depends on some setup), you may want to use Futures to synchronize those operations.
E.g. you could schedule your C-init in a background thread and provide a future which is fulfilled when that finishes (and which your handlers can hook onto before calling into the dep).

Separately I'm wondering if there's a straightforward way to configure the number of threads available to the dispatch queues

Nope, GCD is supposed to do thread assignment magically. NIO programs don't usually use it.

Instead it comes with an threading model based around the EventLoop/EventLoopGroup classes.
The MultiThreadedEventLoopGroup
gives you control about the number of threads.
So if you want a way to schedule background work, you can setup an own EventLoopGroup with the number of threads you desire and then call execute to queue work to that (like with dispatchGroup.async {}).

Hi Helge! Thanks for the reply.

Here's what I know: The identical code path with the same input data, succeeds when called from my configure hook in the same Vapor app. But when I invoke it from my controller in the request/response loop it stalls and gdb shows me a bunch of threads waiting on locks. I just got a great workaround to get the main runloop going in the Vapor forums but I'm still seeing the same behavior, so I'm starting to wonder if thread exhaustion is the issue.

I've done a Future-based implementation but that ends up not helping – the dependency code gets exactly as far along as otherwise before stalling. I'm initializing the dependency in the configure hook and exercising it there before moving on to accept requests, so that doesn't seem to be the issue.

Next I'll try my own EventLoopGroup and see how that goes.

This is like fishing in the dark, but if you are currently using GCD in combination with locks, that might be the cause already. Remember that an arbitrary number of GCD queues can run on a single thread. So if one queue holds a lock another queue on the same thread is waiting for => deadlock.
This doesn't happen with EventLoop's, which have a 1:1 loop-thread mapping.

1 Like

It seems like GCD's presence isn't inherently a problem – for whatever reason the dependency wants to run on the main thread. I used the workaround I got on the Vapor forum (which is to replace the template main.swift with an implementation that doesn't block main):

_ = try app(.detect()).asyncRun()

and then invoke my dependency from DispatchQueue.main. Clearly this won't scale, but it gets my prototype off the ground and I can either figure out the thread issue or remove the dependency when I need to.