c# – CancellationToken for wrapper task?

Question:

Good day!!! I use the library for reading via the Modbus rtu protocol in the project. But I found out that the timeout parameter and / or CancellationToken are not passed to it. What should be mandatory in such long-blocking tasks. ReadHoldingRegistersAsync (slaveAddress, startAddress, numberOfPoints);

I decided to make a wrapper and cancel the task after the time has elapsed.

 public async Task<ushort[]> ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct)
    {
        return await await Task.Factory.StartNew(async () =>
        {
            Task<ushort[]> task = _master.ReadHoldingRegistersAsync(slaveAddress, startAddress, numberOfPoints);

            while (!task.IsCompleted)
            {
                ct.ThrowIfCancellationRequested();
            }
            return await task;
        }, ct);
    }

Where, first, I create a hot task and then, until the task is completed, I check the status of the cancellation token.

Calling code:

 using (var cts = new CancellationTokenSource(timeRespoune))        //время на ожидание ответа
        {
            try
            {
                byte[] sendBuffer = dataProvider.GetDataByte();
                if (sendBuffer == null || !sendBuffer.Any())                                     //READ
                {
                   var takeBuff = await ReadHoldingRegistersAsync(slaveAddress, startAddress, (ushort)(dataProvider.CountGetDataByte / 2), cts.Token);

                } 
                else                                                                             
                _countTryingTakeData = 0;

            }
            catch (OperationCanceledException)
            {

                StatusString = string.Format("Время на ожидание ответа вышло");

                if (++_countTryingTakeData > numberTryingTakeData)
                    Connect();
            }            
        }

All this is called in the polling cycle, i.e. wait for the data for N seconds, if there is no response, then by OperationCanceledException we exit the reading block and repeat again.

PROBLEM: I look in the debugger and the system with the cancellation token works, but the data is sent to the port 1 time. Those. the task status is running but there is nothing in the port. Interrupted by OperationCanceledException enters again forms a new task (new id) enters the cancellation check cycle but there is no data in the port. Is this something I am doing wrong or is the library like that?

Answer:

First, you have a busy waiting: loop

while (!task.IsCompleted)
{
    ct.ThrowIfCancellationRequested();
}

should be wasting quite a lot of resources by constantly questioning the state of the Task. It makes sense to rewrite this logic in a more processor-friendly way:

async Task WaitCancellation(CancellationToken ct)
{
    var tcs = new TaskCompletionSource<int>();
    using (ct.Register(() => tcs.SetResult(0)))
        await tcs.Task;
}

public async Task<ushort[]> ReadHoldingRegistersAsync(
    byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct)
{
    var mainTask =
        _master.ReadHoldingRegistersAsync(slaveAddress, startAddress, numberOfPoints);
    var cancellationWaitTask = WaitCancellation(ct);

    var firstFinishedTask = await Task.WhenAny(mainTask, cancellationWaitTask);

    if (firstFinishedTask == cancellationWaitTask)
        ct.ThrowIfCancellationRequested();

    return await mainTask;
}

At the same time, Task.Factory.StartNew not needed.


Now to the point. It is difficult to undo a running operation from the outside, because it must clean up the internal data structures at the end. You essentially leave the running task to the mercy of fate, and in parallel start the next one. This may not be a completely correct idea, since a parallel task will most likely use the same data structures, and the two tasks will interfere with each other. (Well, here it is better, of course, to ask the authors of the library.)

In your case, you seem to be using a library that writes to the port (serial?), So you can “hard” terminate the task by simply closing the port. You can call Dispose SerialPort , and the operation inside ReadHoldingRegistersAsync will ReadHoldingRegistersAsync with an exception, the _master object will be in a bad state, so you have to recreate the _master again. This might not be such a bad solution (I used it to control a USB device via SerialPort ).

In this case, your external code should, upon arrival of an OperationCanceledException (or whatever will be thrown there), close the current _master and recreate it again. Or find the host SerialPort , close it, catch the exception from its last operation, and finally close the current _master and recreate it again.


Of course, the cleanest and most correct solution would be to have support for CancellationToken in ReadHoldingRegistersAsync . But this may not be possible, for example, if the target device fails, after a communication break in the middle of the transfer of information, to restore it without special commands.

Scroll to Top