Andrius Naruševičius
Andrius Naruševičius

Reputation: 8578

How to properly delay when there is no task to wait for

I have a task that is waiting for a property to be set to true (= completed). The way I am receiving that property value change is via EventHandler (System.Diagnostics.Process.OutputDataReceived to be exact - it continuously reads the output of another process until the correct output is provided). However checking for the property all the time feels somewhat inefficiencient. I have tried adding a small delay of one tick because I believe I can allow myself such a wait if that would save CPU time, but I read .NET struggles with fractional milliseconds. Can I improve this code?

private ConcurrentBag<string> _allMessages = new ConcurrentBag<string>();

public OutputRetriever()
{
    var process = new System.Diagnostics.Process();
    ...
    process.OutputDataReceived += OutputDataReceived;
    process.Start();
}

public async Task<string[]> GetAllOutput()
{   
    while (!IsCompleted)
    {
        // how to properly wait here?
        // await Task.Delay(TimeSpan.FromTicks(1)); // is this ok?
    }
    return _allMessages.ToArray();
}

private void ConsoleDataReceived(object sender, DataReceivedEventArgs e)
{
    _allMessages.Add(e?.Data);
    if (e?.Data == "success")
    {
        IsCompleted = true;
    }
}

Upvotes: 1

Views: 99

Answers (1)

dymanoid
dymanoid

Reputation: 15197

The timers in Windows have a resolution of approx. 16 ms, so any delay below 16 ms cannot be precisely achieved. This applies to any timer - the .NET timers are just wrappers for Windows native timers.

Instead of busy-waiting in a loop, create a custom TaskCompletionSource<T> and return a Task that can be awaited.

class OutputRetriever
{
    private readonly ConcurrentBag<string> _allMessages = new ConcurrentBag<string>();

    private readonly TaskCompletionSource<string[]> _taskSource
        = new TaskCompletionSource<string[]>();

    // Note: this method is not async anymore
    public Task<string[]> GetAllOutput()
    {
        // We just return a task that can be awaited
        return _taskSource.Task;
    }

    void ConsoleDataReceived(object sender, DataReceivedEventArgs e)
    {
        _allMessages.Add(e?.Data);
        if (e?.Data == "success")
        {
            // Here we notify that the task is completed by setting the result
            _taskSource.SetResult(_allMessages.ToArray());
        }
    }
}

Now the clients can simply await the results as usual:

var receiver = new OutputReceiver();
string[] messages = await receiver.GetAllOutput();

Upvotes: 1

Related Questions