Asynchronously checking a value without bogging do

2019-07-27 13:19发布

I have a pattern in my application where I would like to send out some command or start some IO work and wait for it to be completed, or have some mechanism to know if it has completed.

For this I plan to use the async/await pattern. I assume most people have come to the point in coding where they reach this pattern.

while(something)
    DoNothing();

Where DoNothing() usually ends up eating up some CPU time or just stopping the program altogether. I assume the proper way to get around this is with the async/await pattern.

In my case I have come up with the following simple method

public override async Task<Boolean> PerformProcessingAsync()
{
    StartSomeIOProcessing();
    while (TheIOProcessingResult == null)
        await Task.Yield();
    return true;
}

Which starts some IO processing and then waits for the result to be instantiated. In the meantime though it calls Task.Yield to return to the calling context which can then continue work and also places a continuation of this method (the next while loop iteration) onto the call stack.

Is this the correct explanation, is this the correct way to fix the scenario described above?

EDIT: In a more specific situation that I am facing... I also maintain another library that performs IO work, basically reading and writing to SerialPort objects. This library works by having a ConcurrentQueue of reads or writes that it processes with respect to a specific port. A Task "sits at the end" of this queue and consume the work as it goes.

Most reading work simply queries some value, parses it, and fires an event NewData(double myNewData) which is listened to by the UI. In the UI a state representation is held of the SerialPorts data. In the state representation the NewData event is handled which updates the value of whatever it corresponds to.

Writing is done in the same manner but there is no event fired upon completion, the port is simply written to. The only way to tell if it was successful is to wait for the reads to update the state. (Sadly because of how the hardware acts there is no better way to do this).


My current application makes use of this library to perform its IO work. Reads are periodically sent to the library to keep the UI with fresh values from the ports... When a user clicks a button writes are sent off to the library to command something from the hardware.

I am in the situation where I want to assure that the write has happened programmatically. The only way I can think to do this is to send the write off to the library, and wait for the read to update the data the write effects.

Thus the loop.

3条回答
等我变得足够好
2楼-- · 2019-07-27 13:46

I would extract the StartSomethingIOProcessing into a separate Task that returns the result. Await on that, then check the result.

public async PerformProcessingAsync()
{
    var ioProcessingResult = await StartSomeIOProcessing();
    return (null != ioProcessingResult);
}

private Task<TheIOProcessingResultType> StartSomeIOProcessing()
{
    return Task.Run(()=>
    {
        return StartSomeIOProcessing();
    });
}
查看更多
smile是对你的礼貌
3楼-- · 2019-07-27 13:56

I believe you are misinterpreting the async/await operations. The await keyword is actually used to wait until the async operation finished so you don't need to implement a while loop anymore and check if it is null.

public override async Task<Boolean> PerformProcessingAsync()
{
    await Task.Run((() =>
    {
            StartSomeIOProcessing();
    });
    return true;
}
查看更多
爷的心禁止访问
4楼-- · 2019-07-27 14:08

I am in the situation where I want to assure that the write has happened programmatically. The only way I can think to do this is to send the write off to the library, and wait for the read to update the data the write effects.

Yes, that likely is the only way to do it, but there are better ways than spin waiting to see if the read update happens.

Here is a example way to handle the processing like we talked in the chat we did. In summary, you have a queue of IDataRequest, in those request's they hold a TaskCompletionSource<T> that represents the completion of sending the data. Once you do a request to the device and get a response you set the result of the completion source. I combined it with your existing event based implmentation, but honestly I would drop the event's and just have the callers do the update to the UI after awaiting the result of RequestTemp().

public interface IDataRequest
{
    bool TrySetException(Exception ex);
}

public abstract class DataRequest<T> : IDataRequest
{
    public TaskCompletionSource<T> RequestTask { get; } = new TaskCompletionSource<T>();

    public bool TrySetException(Exception ex)
    {
        return RequestTask.TrySetException(ex);
    }
}

public class TempRequest : DataRequest<double>
{
}

public class RpmRequest : DataRequest<int>
{
}

public sealed class DeviceManager : IDisposable
{

    private readonly Task _workerThread;
    private readonly BlockingCollection<IDataRequest> _queue;
    private readonly SerialPort _serialPort;

    public DeviceManager()
    {
        _queue = new BlockingCollection<IDataRequest>();
        _workerThread = Task.Factory.StartNew(ProcessQueue, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
        _serialPort = //...
    }

    public event EventHandler<double> TempUpdate;
    public event EventHandler<int> RpmUpdate;

    public Task<double> RequestTemp()
    {
        var request = new TempRequest();
        _queue.Add(request);
        return request.RequestTask.Task;
    }

    public Task<int> RequestRpm()
    {
        var request = new RpmRequest();
        _queue.Add(request);
        return request.RequestTask.Task;
    }
    public void Dispose()
    {
        _queue.CompleteAdding();
        _workerThread.Wait();
    }

    private void ProcessQueue()
    {
        foreach (var dataRequest in _queue.GetConsumingEnumerable())
        {
            try
            {
                if (dataRequest is TempRequest)
                {
                    DoTempRequest((TempRequest)dataRequest);
                }
                else if (dataRequest is RpmRequest)
                {
                    DoRpmRequest((RpmRequest)dataRequest);
                }
                else
                {
                    throw new NotSupportedException($"A Request of type {dataRequest.GetType()} is not supported.");
                }
            }
            catch (Exception ex)
            {
                dataRequest.TrySetException(ex);
            }
        }
    }

    private void DoTempRequest(TempRequest dataRequest)
    {
        _serialPort.WriteLine("Temp ?");
        var line = _serialPort.ReadLine();
        double result;

        //I am deliberately using Parse instead of TryParse so responses that 
        //fail to parse will throw and get their exception propagated up via the 
        //catch in ProcessQueue().
        result = double.Parse(line);

        //Sends the info back to the caller saying it is done and what the result was.
        dataRequest.RequestTask.TrySetResult(result);

        //Raises the event so subscribers know the new value.
        OnTempUpdate(result);
    }

    private void DoRpmRequest(RpmRequest dataRequest)
    {
        _serialPort.WriteLine("RPM ?");
        var line = _serialPort.ReadLine();
        int result;
        result = int.Parse(line);

        dataRequest.RequestTask.TrySetResult(result);
        OnRpmUpdate(result);

    }

    private void OnTempUpdate(double result)
    {
        TempUpdate?.Invoke(this, result);
    }

    private void OnRpmUpdate(int result)
    {
        RpmUpdate?.Invoke(this, result);
    }
}
查看更多
登录 后发表回答