Austin Wise
Go to home page
2021-06-09

Async Part 5 - How do async functions in Hack behave

Hack programs execute on the HHVM runtime. In this article we are going to perform a series of experiments on HHVM. We will use these experiments to infer properties about how async works.

I often find that is can be faster to write a small test program to figure out how something works than dig through it's documentation or source code.

Simple Async Function

Let's start with something that will help reveal the flow of execution through an async function:

<?hh
async function myAsyncFunction(int $a, int $b) : Awaitable<int> {
    echo "myAsyncFunction: entry\n";
    await HH\Asio\usleep(100000);
    echo "myAsyncFunction: returning\n";
    return $a + $b;
}

<<__EntryPoint>>
async function main(): Awaitable<void> {
  echo "main: enter\n";
  $wh = myAsyncFunction(1, 2);
  echo "main: called async function and got WH, awaiting\n";
  $num = await $wh;
  echo "main: awaited, got: " . $num . "\n";
}

This outputs:

main: enter
myAsyncFunction: entry
main: called async function and got WH, awaiting
myAsyncFunction: returning
main: awaited, got: 3

We can infer from this output that myAsyncFunction() starts executing code as soon as we call it, before it returns anything. So async functions will opportunistically try to execute synchronously until they reach an await. This increases efficiency, because we don't have to yield to the scheduler unless we actually await.

Concrete return type of async functions

Let's see if we can glean any information from the return type of the objects returned from a few different async functions:

<?hh
async function myAsyncFunction(int $a, int $b) : Awaitable<int> {
    await HH\Asio\usleep(100000);
    return $a + $b;
}

async function getInt() : Awaitable<int> {
    return 42;
}

<<__EntryPoint>>
function main() {
    echo var_dump(myAsyncFunction(1,2));
    echo var_dump(getInt());
    echo var_dump(curl_multi_await(curl_multi_init()));
}

When run, this outputs:

object(HH\AsyncFunctionWaitHandle) (0) {
}
object(HH\StaticWaitHandle) (0) {
}
object(HH\ExternalThreadEventWaitHandle) (0) {
}

We can immediately see that there different async function return different types of objects. The type hierarchy of these different subclasses of Awaitable is defined in the hphp/hack/hhi/classes.hhi file in the HHVM source code.

Our myAsyncFunction() returns AsyncFunctionWaitHandle, which seems reasonable. It is an async function. getInt() returns StaticWaitHandle. This appears to be an optimization for async functions that don't ever await anything. This pairs well with the eager synchronous execution behavior we described earlier.

The last one is the most interesting. This the ExternalThreadEventWaitHandle is an extensibility point for plugging async operations into the HHVM runtime.

If we look at the PHP source code for this function, we find that this function has no body defined:

<<__Native("NoFCallBuiltin")>>
function curl_multi_await(resource $mh,
                          float $timeout = 1.0): Awaitable<int>;

It is marked with the __Native attribute, which means it calls into the C++ runtime. greping in the C++ source code, we find this definition:

Object HHVM_FUNCTION(curl_multi_await, const Resource& mh,
                                       double timeout /*=1.0*/) {
  CHECK_MULTI_RESOURCE_THROW(curlm);
  auto ev = new CurlMultiAwait(curlm, timeout);
  try {
    return Object{ev->getWaitHandle()};
  } catch (...) {
    assertx(false);
    ev->abandon();
    throw;
  }
}

We see it is creating an new instance of the CurlMultiAwait class and returning a wait handle off of it. Looking at the definition of the CurlMultiAwait class, we can see it inherits from AsioExternalThreadEvent. This name sounds familiar; it is pretty similar to the ExternalThreadEventWaitHandle we saw earlier.

Looking at the definition of AsioExternalThreadEvent we find a big, juicy doc-comment. It states:

A root class of all classes of objects representing events external to the web request thread that synchronizes on them using ASIO framework.

This appears to be the main extensibility point for adding new types of async functionality to HHVM. It is a mechnism that allow extension authors to connect their thread-based concurrency tools into the single-threaded world of Hack.

Conclusion

With a little experimentation, we were able to find out some interesting properties of how async/await works in Hack. With a bit more greping, we were able to find the interface between the world of Hack and the C++ code implementing the async abstractions in the HHVM runtime.

With a bit more digging, I think I will be able to paint a clearer picture of how the async systems work in the HHVM runtime.