I am setting up a standard standalone thread listening to RabbitMQ in C#. Suppose the method for listening in the thread looks like this:
public void Listen()
using (var channel = connection.CreateModel())
var consumer = SetupQueues(channel);
var ea = consumer.Queue.Dequeue(); // blocking call
consumer.Queue.Dequeue(int millisecondsTimeout, out T result)
The DeQueue with the timeout & flag is the way to do it. It's a very common pattern, and is why many blocking calls are provided with timeout-enabled versions.
Alternately, throwing a (known) exception isn't necessarily a bad thing for control flow. Gracefully shutting down could mean actually trapping the exception, commenting "this is thrown when requesting the channel shuts down", and then returning cleanly. This is how part of TPL works with the CancellationToken.