1

I have a thread which is responsible for calling a webapi from 4 websites exactly every 2 seconds. The Webapi call method should not be awaited because if a website is not available it will wait 5 second to get timeout and then the next website call will be delayed. As HttpClient in .NET 4.7.2 has only async methods , it should be used with await, and if not , compiler gives warning and we may get unexpected behavior (as Microsoft says) . So should I use Task.Run or call Threadpool.QueueUserWorkItem to make a webapi call in parallel. Here is sudocode :

public class Test1
{
        private AutoResetEvent waitEvent = new AutoResetEvent(false);
        private volatile bool _terminated = false;

        public void Start()
        {
            Thread T = new Thread(ProcThread);
            T.Start();
        }

        private async void ProcThread()
        {
            while (!_terminated)
            {
                await CallWebApi();    <=========== this line 
                waitEvent.WaitOne(2000);
            }

        }

        private async Task CallWebApi()
        {
            HttpClient client = new HttpClient();
            .....
            .....
        }

 }
4
  • 1
    You don't need to await if you are not expecting any return result from it (OR) have no dependent operation on it Commented Apr 21, 2020 at 20:53
  • Avoid async void. Its purpose is specifically to make asynchronous event handlers possible. You don't have an event handler here, you have the delegate to be executed by the Thread. Commented Apr 21, 2020 at 21:31
  • 1
    As a side note, HttpClient is intended to be instantiated once and re-used throughout the life of an application. Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads. (source) Commented Apr 21, 2020 at 21:35
  • 1
    And if the website repeatedly fails to respond how will you handle all the requests fired every 2s from swamping the system? Seems like you need to throttle the 2s loop for any website that fails? Commented Apr 21, 2020 at 21:55

3 Answers 3

2

So you have an async procedure that uses a HttpClient to fetch some information and process the fetched data:

async Task CallWebApiAsync() {...}

Improvement 1: it is good practice to suffix async methods with async. This is done to make it possible to let an async version exist next to a non-async version that does something similarly.

Inside this method you are using one of the HttpClient methods to fetch the information. As CallWebApiAsync is awaitable, I assume the async methods are used (GetAsync, GetStreamAsync, etc), and that the method only awaits when it needs the result of the async method.

The nice thing about this is, that as a user of CallWebApiAsync, as long as you don't await the call, you are free to do other things, even if the website isn't reacting. The problem is: after 2 seconds, you want to call the method again. But what to do if the method hasn't finished yet.

Improvement 2 Because you want to be able to start a new Task, while the previous one has not finished: remember the started tasks, and throw them away when finished.

HashSet<Task> activeTasks = new HashSet<Task>(); // efficient add, lookup, and removal

void TaskStarted(Task startedTask)
{
    // remember the startedTask
    activeTasks.Add(startedTask);
}

void TaskCompleted(Task completedTask)
{
    // If desired: log or process the results
    LogFinishedTask(completedTask);

    // Remove the completedTask from the set of ActiveTasks:
    activeTasks.Remove(completedTask);        
}

It might be handy to remove all completed tasks at once:

void RemoveCompletedTasks()
{
    var completedTasks = activeTasks.Where(task => task.IsCompleted).ToList();
    foreach (var task in completedTasks)
    {
        TaskCompleted(completedTask);
    }
}

Now we can adjust your ProcThread.

Improvement 3: in async-await always return Task instead of void and Task<TResult> instead of TResult. Only exception: eventhandlers return void.

async Task ProcThread()
{
    // Repeatedly: start a task; remember it, and wait 2 seconds
    TimeSpan waitTime = TimeSpan.FromSeconds(2);

    while (!terminationRequested)
    {
        Task taskWebApi = CallWebApiAsync();

        // You didn't await, so you are free to do other things
        // Remember the task that you started.
        this.TaskStarted(taskWebApi);

        // wait a while before you start new task:
        await Task.Delay(waitTime);

        // before starting a new task, remove all completed tasks
        this.RemoveCompletedTasks();            
    }
}

Improvement 4: Use TimeSpan.

TimeSpan.FromSeconds(2) is much easier to understand what it represents than a value 2000.

How to stop?

The problem is of course, after you request termination there might still be some tasks running. You'll have to wait for them to finish. But even then: some tasks might not finish at all within reasonable time.

Improvement 5: use CancellationToken to request cancellation.

To cancel tasks in a neat way, class CancellationToken is invented. Users who start a task create a CancellationTokenSource object, and ask this object for a CancellationToken. This token is passed to all async methods. As soon as the user wants to cancel all tasks that were started using this CancellationTokenSource, he requests the CancellationTokenSource to cancel. All tasks that have a token from this source have promised to regularly check the token to see if cancellation is requested. If so, the task does some cleanup (if needed) and returns.

Everything summarized in one class:

class Test1
{
    private HttpClient httpClient = new HttpClient(...);
    private HashSet<TTask> activeTasks = new HashSet<TTask>();

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // repeated CallWebApiAsync until cancellation is requested
        TimeSpan waitTime = TimeSpan.FromSeconds(2);

        // repeat the following until OperationCancelled
        try
        {
            while (true))
            {
                // stop if cancellation requested
                cancellationToken.ThrowIfCancellationRequested();

                var taskWebApi = this.CallWebApiAsync(cancellationToken);
                this.activeTasks.Add(taskWebApi);
                await Task.Delay(waitTime, cancellationToken);

                // remove all completed tasks:
                activeTasks.RemoveWhere(task => task.IsCompleted);
            }
        }
        catch (OperationCanceledException exception)
        {
            // caller requested to cancel. Wait until all tasks are finished.
            await Task.WhenAll(this.activeTasks);

            // if desired do some logging for all tasks that were not completed.
        }
    }

And the adjusted CallWebApiAsync:

    private async Task CallWebApiAsync(CancellationToken cancellationToken)
    {
         const string requestUri = ...
         var httpResponseMessage = await this.httpClient.GetAsync(requestUri, cancellationToken);

         // if here: cancellation not requested
         this.ProcessHttpResponse(httpResponseMessage);
    }

    private void ProcessHttpRespons(HttpResponseMessage httpResponseMessage)
    {
        ...
    }
}

Usage:

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
Test1 test = new Test1();

Task taskCallWebApiRepeatedly = test.StartAsync(cancellationTokenSource.Token);

// because you didn't await, you are free to do other things, while WebApi is called
// every 2 seconds
DoSomethingElse();

// you get bored. Request cancellation:
cancellationTokenSource.Cancel();

// of course you need to await until all tasks are finished:
await Task.Wait(taskCallWebApiRepeatedly);

Because everyone promises to check regularly if cancellation is requested, you are certain that within reasonable time all tasks are finished, and have cleaned up their mess. The definition or "reasonable time" is arbitrary, but let's say, less than 100 msec?

Sign up to request clarification or add additional context in comments.

Comments

0

If all you want is to execute a method every two seconds, then a System.Timers.Timer is probably the most suitable tool to use:

public class Test1
{
    private readonly HttpClient _client;
    private readonly System.Timers.Timer _timer;

    public Test1()
    {
        _client = new HttpClient();
        _timer = new System.Timers.Timer();
        _timer.Interval = 2000;
        _timer.Elapsed += Timer_Elapsed;
    }

    private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        var fireAndForgetTask = CallWebApiAsync();
    }

    private async Task CallWebApiAsync()
    {
        var html = await _client.GetStringAsync("http://example.com");
        //...
    }

    public void Start() => _timer.Start();
    public void Stop() => _timer.Stop();
}

Comments

-1

something like this. BTW take this as pseudo code as I am typing sitting on my bed:)

List<Task> tasks = new List<Task>();
tasks.Add(CallWebApi());

while (! await Task.WhenAny(tasks))
            {
                tasks.Add(CallWebApi());    <=========== this line 
                await Task.Delay(2000);
            }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.