EDIT: You mention not wanting to "breaking the function up into pre/post waiting parts."
What language are you developing in? If it has continuations (yield return
in C#) then that provides a way to write code that appears to be procedural but which can easily be paused until a blocking operation makes its completion callback.
Here's an article about the idea: http://msdn.microsoft.com/en-us/magazine/cc546608.aspx
UPDATE:
Unfortunatly, the language is C++
That would make a great T-shirt slogan.
Okay, so you might find it helpful to structure your sequential code as a state-machine, so it becomes interrupt/resume-capable.
e.g. your pain is needing to write two functions, the one that initiates and the one that acts as the handler for the completion event:
void send_greeting(const std::string &msg)
{
std::cout << "Sending the greeting" << std::endl;
begin_sending_string_somehow(msg, greeting_sent_okay);
}
void greeting_sent_okay()
{
std::cout << "Greeting has been sent successfully." << std::endl;
}
Your idea was to wait:
void send_greeting(const std::string &msg)
{
std::cout << "Sending the greeting" << std::endl;
waiter w;
begin_sending_string_somehow(msg, w);
w.wait_for_completion();
std::cout << "Greeting has been sent successfully." << std::endl;
}
In that example, waiter
overloads operator() so it can serve as a callback, and wait_for_completion
somehow hangs up until it sees that the operator() has been called.
I'm assuming that begin_sending_string_somehow
's second parameter is a template parameter that can be any callable type accepting no parameters.
But as you say, this has drawbacks. Any time a thread is waiting like that, you've added another potential deadlock, and you are also consuming the "resource" of a whole thread and its stack, meaning that more threads will have to be created elsewhere to allow work to be done, which is contradictory to the whole point of a thread pool.
So instead, write a class:
class send_greeting
{
int state_;
std::string msg_;
public:
send_greeting(const std::string &msg)
: state_(0), msg_(msg) {}
void operator()
{
switch (state_++)
{
case 0:
std::cout << "Sending the greeting" << std::endl;
begin_sending_string_somehow(msg, *this);
break;
case 1:
std::cout << "Greeting has been sent successfully."
<< std::endl;
break;
}
}
};
The class implements the function call operator ()
. Each time it is called, it executes the next step in the logic. (Of course, being such a trivial example, this now is mostly state management noise, but in a more complex example with four or five states it may help clarify the sequential nature of the code).
Problems:
If the event callback function signature has special parameters, you'll need to add another overload of operator()
that stores the parameters in extra fields and then calls onto the parameterless overload. Then it starts to get messy because those fields will be accessible at compile-time in the initial state, even though they are not meaningful at runtime in that state.
How do objects of the class get constructed and deleted? The object has to survive until the operation completes or is abandoned... the central pitfall of C++. I'd recommend implementing a general scheme to manage it. Create a list of "things that will need to be deleted" and ensure that this happens automatically at certain safe points, i.e. try to get as close as possible to GC as you can. The further away you are from that, the more memory you will leak.