4

I have a web service, where data for an identifier can be gotten (e.g. billing information for a customer id). In my client there are multiple threads. These threads call the method at the web service to get the data for the identifier.

Now it happens, that during a request to the web service for e.g CustomerID = 12345 another thread tries to call the web service for the same customer. Now I want to spare the server und to recycle the result of the request. Therefore I want to wait the second thread until the result of the first request is gotten and return the result also to the second thread.

An important requirement is, that if the second thread wants to get the data after the web request returned, a new request has to be sent. To illustrate my problem, I show a sequence diagram:

Can anybody explain me, how I can achieve this in C#.

leppie
  • 115,091
  • 17
  • 196
  • 297
scher
  • 1,813
  • 2
  • 18
  • 39
  • below link might help (http://www.codeproject.com/Questions/1009076/Concurrent-Web-Request-Csharp) – Dandy Jul 11 '16 at 09:41
  • I'm not sure how this can help me, cause I don't want to post several requests put to prevent them. – scher Jul 11 '16 at 09:44
  • 1
    maybe task caching can help you? http://stackoverflow.com/questions/36084495/when-to-cache-tasks – Nikola.Lukovic Jul 11 '16 at 09:45
  • https://msdn.microsoft.com/en-us/library/system.threading.readerwriterlock.aspx there are examples in link – Dandy Jul 11 '16 at 09:50
  • http://stackoverflow.com/questions/20810122/how-to-prevent-a-method-from-running-across-multiple-threads – Dandy Jul 11 '16 at 09:56

3 Answers3

1

I think you can do it like this (pseudocode):

  • Create a dictionary to store the action and the executing Task;
  • When receiving a request, temporarily lock the dictionary so you won't get two tasks at the same time, create a task and add it to the dictionary;
  • await the task (or thread);

  • If another request comes in, first check the dictionary for a running task. If it is there, await it too. If not, create a new one.

Something like this:

Dictionary<string, Task<string>> tasks = new Dictionary<string, Task<string>>();

public async Task<string> GetCustomer(int id)
{
    string taskName = $"get-customer-{id}";

    try
    {
        Task<string> task;

        lock (tasks)
        {
            if (!tasks.TryGetValue(taskName, out task))
            {
                task = new Task<string>(() => GetCustomerInternal(id));
                tasks[taskName] = task;
            }
        }

        string result = await task;

        return result;
    }
    finally
    {
        lock (tasks)
        {
            tasks.Remove(taskName);
        }
    }
}

private string GetCustomerInternal(int id)
{
    return "hello";
}
Patrick Hofman
  • 153,850
  • 22
  • 249
  • 325
1

You can do something like this:

private static ConcurrentDictionary<string, Task<string>> s_urlToContents;

public static Task<string> GetContentsAsync(string url)
{
    Task<string> contents;
    if(!s_urlToContents.TryGetValue(url, out contents))
    {
        contents = GetContentsAsync(url);
        contents.ContinueWith(t => s_urlToContents.TryAdd(url, t); },
        TaskContinuationOptions.OnlyOnRanToCompletion |
        TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
    }
    return contents;
}

private static async Task<string> GetContentsAsync(string url)
{
    var response = await new HttpClient().GetAsync(url);
    return response.EnsureSuccessStatusCode().Content.ReadAsString();
}

This code caches an http request, if another thread wants access to the same request, you just return the already cached task, if it doesn't exist, you spawn the task and cache it for future requests.

Nikola.Lukovic
  • 1,256
  • 1
  • 16
  • 33
  • You could still end up with two tasks running at the same time. If both pass the `if(!s_urlToContents.TryGetValue(url, out contents))` at the same time, you are going to create a task in both executions. – Patrick Hofman Jul 11 '16 at 10:14
0

Finally I ended with the following solution:

private class RequestHandler
{
    private EventWaitHandle _handle;

    public RequestHandler(Func<string> request)
    {
        _handle = new EventWaitHandle(false, EventResetMode.ManualReset);
        Start(request);
    }

    private void Start(Func<string> doRequest)
    {
        Result = doRequest();
        _handle.Set();
    }

    public void WaitForResponse()
    {
        _handle.WaitOne();
    }

    public string Result { get; private set; }
}

private Object _lockObject = new Object();
private Dictionary<string, RequestHandler> _pendingRequests = new Dictionary<string, RequestHandler>();

public string GetCustomer(int id)
{
    RequestHandler request;

    lock (_lockObject)
    {
        if (!_pendingRequests.TryGetValue(id, out request))
        {
            request = new RequestHandler(() => LoadEntity(id));
            _pendingRequests[id] = request;
        }
    }

    request.WaitForResponse();
    lock (_lockObject)
    {
        _pendingRequests.Clear();
    }
    return request.Result;
}

Although it is still possible to have duplicated posts to the server this is a very minimal chance (on thread deletes all the pending requests after the first thread created a request and before a second thread tries to get the same customer data).

Thanks to the inspiration the posts here gave.

scher
  • 1,813
  • 2
  • 18
  • 39