Quantcast
Channel: WE MOVED to github.com/microsoft/cpprestsdk. This site is not monitored!
Viewing all articles
Browse latest Browse all 4845

New Post: Thread safety of tasks?

$
0
0
So let me try to ask in other words, then.

Assume that there is a server to which we can send API requests. Of course, the server requires authentication first, and after authentication, it returns a token that must be sent in all subsequent calls.

Now let's assume we have a class that models this. It allows the client to do API calls to the server while hiding the complexity of network latency and authentication.

In the constructor of this class, we create a timer and then we start task B and assign it to m_TaskB, e.g.:
Class::Class()
{
    m_Timer.setup(/*...*/);
    m_TaskB = pplx::create_task(&Class::TaskB, this);
}
Let's ignore why the timer is there for now. Task B's job is to authenticate with the server and retrieve the token. That's all.
Now, we can't return a task from the constructor, so the client must assume that the class is ready for use after the constructor call, so let's say the client calls DoX to do some API request.

In DoX, we first need to get our access token and then we can send our actual API request. Since the function shouldn't block (and because we may not actually have our access token yet), we will return a task that signals when the operation is finished, so it would look something like:
pplx::task<void> Class::DoX()
{
    return GetAccessToken().then([this](pplx::task<u8> AccessTokenTask)
    {
        auto AccessToken = AccessTokenTask.get();
        // Send network request here
    });
}
I'm assuming that u8 is a type for a UTF-8 formatted std::string here. Great, now what about the implementation of GetAccessToken? It would need to return a task whose value would be the access token. And to get that, we first need to ensure that Task B has finished executing and so have acquired an access token, so it would look something like:
pplx::task<u8> Class::GetAccessToken()
{
    return m_TaskB.then([this](pplx::task<void> GetAccessTokenTask)
    {
        GetAccessTokenTask.get(); // Just to ensure any error propagates to the caller of the class (i.e. the one who called GetX).
        return m_AccessToken; // Task B will store the access token here.
    });
}
Let's assume that Task B looks something like:
pplx::task<void> Class::TaskB()
{
    m_Client.Send(/*...*/).then([this](pplx::task<web::http_response> ResponseTask)
    {
        m_AccessToken = ResponseTask.get();
    });
}
Now there's just one thing left. We have to consider the fact that the access token won't last forever. It needs to be periodically refreshed. There's two ways to model this. The first one is in GetAccessToken, where we detected that the access token has expired and thus start up Task B again to get a new access token:
pplx::task<u8> Class::GetAccessToken()
{
    if (HasAccessTokenExpired())
        m_TaskB = pplx::create_task(&Class::TaskB, this);
    return m_TaskB.then([this](pplx::task<void> GetAccessTokenTask)
    {
        GetAccessTokenTask.get(); // Just to ensure any error propagates to the caller of the class (i.e. the one who called GetX).
        return m_AccessToken; // Task B will store the access token here.
    });
}
This is not very ideal because it will add extra latency to get a new token on demand. So ideally we'd simply like to periodically refresh the token in the background so that when the client calls DoX, we guarantee (or guarantee as much as we can; it's not going to be 100%) that we have a valid access token. So here's where our timer in the constructor comes into play. Every N seconds, we have the timer interrupt. In that interrupt, we restart Task B:
void Class::TimerInterrupt()
{
    m_TaskB = pplx::create_task(&Class::TaskB, this);
}
But now we immediately see problems. The TimerInterrupt can run at any time. GetAccessToken can run at any time and wait on m_TaskB, so we have a race. How do we solve this? Is it safe to assign a new task to m_TaskB while GetAccessToken is waiting on it?

But wait--there's more. In fact, we want multiple threads (or tasks) to be able to use this class concurrently so that multiple API calls may be on-flight to the server at once. The problem is that for some reason, the access token may no longer be valid and so the server returns an error during an API call. In this case, one or perhaps all of the API calls may fail due to the expired token. So they need to refresh it by once again calling GetAccessToken and then redoing the web request. Something like:
web::http_client Client(/*...*/);
web::http_request Request(/*.../*);
return Client.request(Request).then([Client, Request](pplx::task<web::http_response> ResponseTask)
{
    auto Response = ResponseTask.get();
    if (IsNotValidAccessToken(Response))
        return GetAccessToken().then([Client, Request](pplx::task<u8> AccessTokenTask)
        {
            auto AccessToken = AccessTokenTask.get();
            return Client.request(Request);
        }).then([](pplx::task<web::http_response> ResponseTask)
        {
            ResponseTask.get();
        });
    return pplx::create_task([]{}); // Return empty task because request succeeded
});
Then we have to take into account that multiple threads may simultaneously call GetAccessToken and GetAccessToken must not spawn additional tasks to get a new access token. It must done once and only once and only one such request must be in flight. So these additional threads will most likely wait on m_TaskB to know when the access token is ready.

But now we have the scenario that multiple threads are waiting on m_TaskB and a timer interrupt which may assign a new task B to m_TaskB. So what are the consequences with this design? Does it work? Should it be done differently?

Viewing all articles
Browse latest Browse all 4845


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>