Async Task.Delay overhead in UWP app, is there a better solution?
Asked Answered
M

1

8

Some time ago I built an application to run on my Raspberry Pi 3. Among other things, it controlled a stepper motor using a stepper motor driver board. To make the motor move one "step", I had to set an output pin High and Low. The change in voltage would cause the motor to move into its next position. I needed to have a small time delay between the voltage changes for it to work properly.

My initial research showed that the best-practice way to get a time delay in a UWP app was to use the asynchronous Task.Delay() method. I didn't have access to my Thread.Sleep method in UWP, so I gave it a try. Also, since the method takes an integer as a parameter, 1 millisecond was the shortest delay I could use.

Here's an example of my first attempt, making 1600 successive "steps":

for (int i = 0; i < 1600; i++)
{
    // (stepper driver board makes one step for every low-to-high transition)
    StepperStepPin.Write(GpioPinValue.Low);
    await Task.Delay(1); // wait 1 ms
    StepperStepPin.Write(GpioPinValue.High);
    await Task.Delay(1); // wait 1 ms
}

In theory, with 2ms delay in each iteration of the loop, this should have taken about 3.2 seconds. In reality, it ended up taking about 51 seconds. As best as I can tell, the act of calling this asynchronous delay method adds about 15 ms of overhead to start up the asynchronous thread. If I were using longer delays only once in awhile this wouldn't be noticeable. But when I have to do it hundreds or thousands of times, it adds up quickly.

After lots more digging, I found a solution that worked for me. I ditched the async method and went with a synchronous approach using the System.Diagnostics.Stopwatch class, and it also lets me have sub-millisecond delays:

private readonly Stopwatch _sw = new System.Diagnostics.Stopwatch();

private void ShortDelay(double milliseconds) {
    _sw.Start();
    while ((_sw.Elapsed).TotalMilliseconds < milliseconds) { }
    _sw.Reset();
}

//////////////////////////////////////////

for (int i = 0; i < 1600; i++)
{
    // (stepper driver board makes one step for every low-to-high transition)
    StepperStepPin.Write(GpioPinValue.Low);
    ShortDelay(0.5); // wait 0.5 ms
    StepperStepPin.Write(GpioPinValue.High);
    ShortDelay(0.5); // wait 0.5 ms
}

I would assume the little while loop could cause problems with a UI thread, but my app is headless, so it didn't really affect my particular application. But it still feels like a bit of a hack, like there should be a better solution here to get a reasonably accurate millisecond-or-less time delay.

I admit I feel like I don't fully understand async/await, and I'd like to know if there's a more appropriate solution here. So if you have some expertise here and can explain a better way, or can explain why this method is acceptable, any feedback would be appreciated.

Thanks.

Mozell answered 4/5, 2017 at 13:28 Comment(16)
Basically the same as here: https://mcmap.net/q/144060/-high-resolution-timer-duplicate/993547Misunderstanding
Is there any kind of timer in UWP? Though it might also suffer from inaccuracy.Sarina
@evk looks like System.Threading.Timer is available, might be worth experimenting with.Mozell
there should be a better solution here to get a reasonably accurate millisecond-or-less time delay. - only if you think .NET is a realtime environment, which it is not.Adenectomy
@StephenCleary Good point, there's already overhead from other stuff like garbage collection, operating system, etc... You don't get the same low-level control like you get with an arduino where the only code running is your code. I'm sure my solution has small variations each time, but it's close enough for what I'm doing.Mozell
Not sure if this makes any differenceLodovico
Not much you can do about that unfortunately. If you need sub-millisecond precision on Windows, you can't put your threads to sleep. Usually you'd use Thread.SpinWait instead of a loop, unfortunately it's not available on UWPPreach
Oh, actually it is: msdn.microsoft.com/en-us/library/system.threading.spinwait.aspxPreach
@KevinGosse I believe this was improved in a recent Windows 10 update. Before, Thread.Sleep(1) would sleep for 15 ms, after, it is actually able to sleep for roughly 1 ms. That won't help you if you need a smaller delay, but it's still an improvement.Bravo
"the act of calling this asynchronous delay method adds about 15 ms of overhead to start up the asynchronous thread" -- unlikely it's "overhead" per se. Rather, the Windows thread scheduler simply does not schedule a thread for immediate execution in your scenario. That thread is competing with other threads, and is low man on the totem pole, unless you increase the thread priority.Lucio
Lack of fine resolution for thread-scheduling-based timing is a well-known limitation of Windows and any other mainstream OS, none of which are real-time OSs. You can improve things with spin-wait, but the thread scheduler will always, eventually pre-empt the thread and cause problems.Lucio
@svick: "it is actually able to sleep for roughly 1 ms" -- do you have a citation for this? First I've heard of it. In the past, a non-zero wait time is used to force yielding (using 0, the thread yields only if there is at least one other same-priority thread ready to run), but the thread is resumed according to lower resolution timing. A quick test reveals that, with no other threads in the process running, I see better resolution than I remember in the past, but still highly variable sleep times, ranging from 1 to 4 ms when calling Thread.Sleep(1).Lucio
@svick: for what it's worth, I ran the same test program on a Windows 7 box, and it consistently sleeps for less than 1 ms, typically around 0.3 to 0.4 ms. So, Windows 10 might be trying to be more accurate (i.e. ensure the sleep really is the minimum duration), but it's not as though scheduling in absence of competing threads has been improved in terms of precision. Min was 0.01 ms, max was 1.01 msLucio
@PeterDuniho That's really surprising. The guarantee of Thread.Sleep(n) should be that it sleeps for at least n ms. If it's less (and you're measuring time correctly), then that sounds like a bug to me. And I tried to find a citation, but couldn't find anything.Bravo
@svick: "The guarantee of Thread.Sleep(n) should be that it sleeps for at least n ms" -- no disagreement there. I'm just reporting what I see. 😊 It is likely that on earlier versions of .NET (which is what I have installed on my Win7 box), Thread.Sleep() simply delegated to the native Sleep() function (or SleepEx()). The documentation specifically says there: "If dwMilliseconds is less than the resolution of the system clock, the thread may sleep for less than the specified length of time". That Win10 behaves better could reflect a change in the .NET implementation or in the OS.Lucio
@svick: other useful and relevant links: Sleep function, Thread.Sleep is a sign of a poorly designed program, Priority-induced starvation: Why Sleep(1) is better than Sleep(0) and the Windows balance set managerLucio
C
0

Personally, I don't think adding Delays in your code is the right way to get things right. If persay you need to apply a delay to get some UWP (Windows 10 IOT core preferably) app to perform properly then there might be something that can be made in a better way to avoid delays as delays not only are inaccurate and Unreliable ways of knowing that the job is done especially when it comes to an IoT project. Things can go wrong at any moment and the operation can take longer. In such a case your delay breaks and your IoT set up starts to mess it up.

That Being said: I've written up a class that can help you control your stepper motor without delays this was a quick thing so if there are any issues do let me know but I've tested it thoroughly and I didn't seem to find any issues on the functioning or on the performance sides. My Code is as below:

public class Uln2003Driver : IDisposable
{
    private readonly GpioPin[] _gpioPins = new GpioPin[4];

    private readonly GpioPinValue[][] _waveDriveSequence =
    {
        new[] {GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low},
        new[] {GpioPinValue.Low, GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low},
        new[] {GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High, GpioPinValue.Low},
        new[] {GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High}
    };

    private readonly GpioPinValue[][] _fullStepSequence =
    {
        new[] {GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High},
        new[] {GpioPinValue.High, GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low},
        new[] {GpioPinValue.Low, GpioPinValue.High, GpioPinValue.High, GpioPinValue.Low},
        new[] {GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High, GpioPinValue.High }

    };

    private readonly GpioPinValue[][] _haveStepSequence =
    {
        new[] {GpioPinValue.High, GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High},
        new[] {GpioPinValue.Low, GpioPinValue.High, GpioPinValue.High, GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low},
        new[] {GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High, GpioPinValue.High, GpioPinValue.High, GpioPinValue.Low, GpioPinValue.Low},
        new[] {GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.Low, GpioPinValue.High, GpioPinValue.High, GpioPinValue.High }
    };

    public Uln2003Driver(int blueWireToGpio, int pinkWireToGpio, int yellowWireToGpio, int orangeWireToGpio)
    {
        var gpio = GpioController.GetDefault();

        _gpioPins[0] = gpio.OpenPin(blueWireToGpio);
        _gpioPins[1] = gpio.OpenPin(pinkWireToGpio);
        _gpioPins[2] = gpio.OpenPin(yellowWireToGpio);
        _gpioPins[3] = gpio.OpenPin(orangeWireToGpio);

        foreach (var gpioPin in _gpioPins)
        {
            gpioPin.Write(GpioPinValue.Low);
            gpioPin.SetDriveMode(GpioPinDriveMode.Output);
        }
    }

    public async Task TurnAsync(int degree, TurnDirection direction,
        DrivingMethod drivingMethod = DrivingMethod.FullStep)
    {
        var steps = 0;
        GpioPinValue[][] methodSequence;
        switch (drivingMethod)
        {
            case DrivingMethod.WaveDrive:
                methodSequence = _waveDriveSequence;
                steps = (int) Math.Ceiling(degree/0.1767478397486253);
                break;
            case DrivingMethod.FullStep:
                methodSequence = _fullStepSequence;
                steps = (int) Math.Ceiling(degree/0.1767478397486253);
                break;
            case DrivingMethod.HalfStep:
                methodSequence = _haveStepSequence;
                steps = (int) Math.Ceiling(degree/0.0883739198743126);
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(drivingMethod), drivingMethod, null);
        }
        var counter = 0;
        while (counter < steps)
        {
            for (var j = 0; j < methodSequence[0].Length; j++)
            {
                for (var i = 0; i < 4; i++)
                {
                    _gpioPins[i].Write(methodSequence[direction == TurnDirection.Left ? i : 3 - i][j]);
                }
                await Task.Delay(5);
                counter ++;
                if (counter == steps)
                    break;
            }
        }

        Stop();
    }

    public void Stop()
    {
        foreach (var gpioPin in _gpioPins)
        {
            gpioPin.Write(GpioPinValue.Low);
        }
    }

    public void Dispose()
    {
        foreach (var gpioPin in _gpioPins)
        {
            gpioPin.Write(GpioPinValue.Low);
            gpioPin.Dispose();
        }
    }
}

public enum DrivingMethod
{
    WaveDrive,
    FullStep,
    HalfStep
}

public enum TurnDirection
{
    Left,
    Right
}`

Put this as a class and you can interact with it from any CodeBehind or ViewModel like below:

 private readonly Uln2003Driver _uln2003Driver;  //The Declaration on top of the Class to make it global.


//In the constructor of the Page CodeBehind or ViewModel. The arguments are the GPIO pins to which your stepper is connected.
     _uln2003Driver = new Uln2003Driver(26, 13, 6, 5);

Now that you have the set up done, use the above as below:

 Task.Run(async () =>
             {
                 await _uln2003Driver.TurnAsync(180, TurnDirection.Left, DrivingMethod.FullStep);
                 await _uln2003Driver.TurnAsync(180, TurnDirection.Right, DrivingMethod.WaveDrive);
             });            

The above code just rotates in clockwise and anti-clockwise direction but feel free to tweek it,

Note: Please do remember to call _uln2003Driver?.Dispose(); once the page is unloaded or the job is done to free up the resources. Also the ? is a null conditional operator available in c#6.0, i know it's obvious but ran into a similar problem on another answer.

Feel free to use the comments section if you need anything

Carranza answered 10/5, 2017 at 5:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.