Asynchronous operations within a loop - how to keep control of execution?

一曲冷凌霜 提交于 2021-01-29 19:05:26

问题


Follow-on question to this one.

I am trying to generate and save a series of images. Rendering is done by Helix Toolkit, which I am told utilises the WPF composite render thread. This is causing problems because it executes asynchronously.

My original problem was that I couldn't save a given image because it hadn't yet been rendered at that time I was trying to save it. The above answer provides a workaround to this by putting the 'save' operation inside an Action which is called with low priority, thus ensuring that the rendering completes first.

This is fine for one image, but in my application I require multiple images. As it stands I cannot keep control of the sequence of events because they occur asynchronously. I am using a For loop which just continues regardless of the progress of rendering and saving the images. I need the images to be generated one by one, with enough time for rendering and saving before starting the next one.

I have tried putting delays in the loop but that causes its own problems. For instance an async await as commented in the code causes cross-threading issues because the data was created on a different thread from where the rendering is being done. I tried putting in a simple delay but then that just locks everything up - I think in part because the save operation I am waiting on has very low priority.

I cannot simply treat it as a batch of separate unrelated asynchronous tasks because I am using a single HelixViewport3D control in the GUI. The images have to be generated sequentially.

I did try a recursive method where SaveHelixPlotAsBitmap() calls DrawStuff() but that wasn't working out very well, and it doesn't seem a good approach.

I tried setting a flag ('busy') on each loop and waiting for it to be reset before continuing but that wasn't working - again, because of the asynchronous execution. Similarly I tried using a counter to keep the loop in step with the number of images that had been generated but ran into similar problems.

I seem to be going down a rabbit hole of threading and asynchronous operations that I don't want to be in.

How can I resolve this?

class Foo {
    public List<Point3D> points;
    public Color PointColor;
    public Foo(Color col) { // constructor creates three arbitrary 3D points
        points = new List<Point3D>() { new Point3D(0, 0, 0), new Point3D(1, 0, 0), new Point3D(0, 0, 1) };
        PointColor = col;
    }
}

public partial class MainWindow : Window
{
    int i = -1; // counter
    public MainWindow()
    {
        InitializeComponent();
    }
    private void Go_Click(object sender, RoutedEventArgs e) // STARTING POINT
    {
        // Create list of objects each with three 3D points...
        List<Foo> bar = new List<Foo>(){ new Foo(Colors.Red), new Foo(Colors.Green), new Foo(Colors.Blue) };

        foreach (Foo b in bar)
        {

            i++;
            DrawStuff(b, SaveHelixPlotAsBitmap); // plot to helixViewport3D control ('points' = list of 3D points)

            // This is fine the first time but then it runs away with itself because the rendering and image grabbing
            // are asynchronous. I need to keep it sequential i.e.
            // Render image 1 -> save image 1
            // Render image 2 -> save image 2
            // Etc.

        }
    }
    private void DrawStuff(Foo thisFoo, Action renderingCompleted)
    {

        //await System.Threading.Tasks.Task.Run(() =>
        //{

        Point3DCollection dataList = new Point3DCollection();
        PointsVisual3D cloudPoints = new PointsVisual3D { Color = thisFoo.PointColor, Size = 5.0f };
        foreach (Point3D p in thisFoo.points)
        {
            dataList.Add(p);
        }
        cloudPoints.Points = dataList;

        // Add geometry to helixPlot. It renders asynchronously in the WPF composite render thread...
        helixViewport3D.Children.Add(cloudPoints);
        helixViewport3D.CameraController.ZoomExtents();

        // Save image (low priority means rendering finishes first, which is critical)..
        Dispatcher.BeginInvoke(renderingCompleted, DispatcherPriority.ContextIdle);

        //});

    }
    private void SaveHelixPlotAsBitmap()
    {
        Viewport3DHelper.SaveBitmap(helixViewport3D.Viewport, $@"E:\test{i}.png", null, 4, BitmapExporter.OutputFormat.Png);
    }
}

回答1:


Note These examples are just to prove a concept, there is work needed on the TaskCompletionSource to handle errors

Given this test window

<Window x:Class="WpfApp2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel x:Name="StackPanel"/>
    </Grid>
</Window>

Here is an example of how to use events to know when the view is in the state that you want.

using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace WpfApp2
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DoWorkAsync();
        }

        private async Task DoWorkAsync()
        {
            for (int i = 0; i < 10; i++)
            {
                await RenderAndCapture();
            }
        }

        private async Task RenderAndCapture()
        {
            await RenderAsync();
            CaptureScreen();
        }

        private Task RenderAsync()
        {
            var taskCompletionSource = new TaskCompletionSource<object>();
            Dispatcher.Invoke(() =>
            {
                var panel = new TextBlock {Text = "NewBlock"};
                panel.Loaded += OnPanelOnLoaded;

                StackPanel.Children.Add(panel);

                void OnPanelOnLoaded(object sender, RoutedEventArgs args)
                {
                    panel.Loaded -= OnPanelOnLoaded;
                    taskCompletionSource.TrySetResult(null);
                }
            });

            return taskCompletionSource.Task;
        }

        private void CaptureScreen()
        {
            // Capture Image
        }
    }
}

If you want to have your sync method called from outside you can implement a task queue.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace WpfApp2
{
    public class TaskQueue
    {
        private readonly SemaphoreSlim _semaphore;
        public TaskQueue()
        {
            _semaphore = new SemaphoreSlim(1);
        }

        public async Task Enqueue(Func<Task> taskFactory)
        {
            await _semaphore.WaitAsync();
            try
            {
                await taskFactory();
            }
            finally
            {
                _semaphore.Release();
            }
        }
    }

    public partial class MainWindow : Window
    {
        private readonly TaskQueue _taskQueue;

        public MainWindow()
        {
            _taskQueue = new TaskQueue();
            InitializeComponent();
            DoWork();
        }

        private void DoWork()
        {
            for (int i = 0; i < 10; i++)
            {
                QueueRenderAndCapture();
            }
        }

        private void QueueRenderAndCapture()
        {
            _taskQueue.Enqueue(() => RenderAndCapture());
        }

        private async Task RenderAndCapture()
        {
            await RenderAsync();
            CaptureScreen();
        }

        private Task RenderAsync()
        {
            var taskCompletionSource = new TaskCompletionSource<object>();
            Dispatcher.Invoke(() =>
            {
                var panel = new TextBlock {Text = "NewBlock"};
                panel.Loaded += OnPanelOnLoaded;

                StackPanel.Children.Add(panel);

                void OnPanelOnLoaded(object sender, RoutedEventArgs args)
                {
                    panel.Loaded -= OnPanelOnLoaded;
                    taskCompletionSource.TrySetResult(null);
                }
            });

            return taskCompletionSource.Task;
        }

        private void CaptureScreen()
        {
            // Capture Screenshot
        }
    }
}

This will make sure the UI is in the state required for each iteration

You will of course need to expand this so that you listen to the Loaded event of each point that you wish to render.

Edit: As PointsVisual3D does not have the Loaded event you can complete the task by hooking onto the event you had previously used. Not ideal, but it should work.

private Task RenderAsync()
{
    var taskCompletionSource = new TaskCompletionSource<object>();
    Dispatcher.Invoke(() =>
    {
        var panel = new TextBlock {Text = "NewBlock"};

        StackPanel.Children.Add(panel);

        Dispatcher.BeginInvoke(new Action(() =>
        {
            taskCompletionSource.TrySetResult(null);
        }), DispatcherPriority.ContextIdle);
    });

    return taskCompletionSource.Task;
}



回答2:


Solution below. This is my implementation of the code provided in Jason's answer. All credit to Jason for the important bits.

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Go_Click(object sender, RoutedEventArgs e) // STARTING POINT
        {
            DoWorkAsync();
        }

        private async Task DoWorkAsync()
        {

            // Create list of objects each with three 3D points...
            List<Foo> bar = new List<Foo>() { new Foo(Colors.Red), new Foo(Colors.Green), new Foo(Colors.Blue) };
            
            int i = -1; // init counter
            foreach (Foo b in bar)
            {
                i++;
                await RenderAndCapture(b, i);
            }

        }

        private async Task RenderAndCapture(Foo b, int i)
        {
            await RenderAsync(b);
            SaveHelixPlotAsBitmap(i);
        }

        private Task RenderAsync(Foo b)
        {
            var taskCompletionSource = new TaskCompletionSource<object>();
            Dispatcher.Invoke(() =>
            {

                DrawStuff(b);

                Dispatcher.BeginInvoke(new Action(() =>
                {
                    taskCompletionSource.TrySetResult(null);
                }), DispatcherPriority.ContextIdle);
            });

            return taskCompletionSource.Task;
        }

        private void DrawStuff(Foo thisFoo)
        {

            Point3DCollection dataList = new Point3DCollection();
            PointsVisual3D cloudPoints = new PointsVisual3D { Color = thisFoo.PointColor, Size = 5.0f };
            
            foreach (Point3D p in thisFoo.points)
            {
                dataList.Add(p);
            }
            cloudPoints.Points = dataList;
            
            // Add geometry to helixPlot. It renders asynchronously in the WPF composite render thread...
            helixPlot.Children.Add(cloudPoints);
            helixPlot.CameraController.ZoomExtents();
            
        }
        private void SaveHelixPlotAsBitmap(int i) // screenshot
        {
            Viewport3DHelper.SaveBitmap(helixPlot.Viewport, $@"E:\test{i}.png", null, 4, BitmapExporter.OutputFormat.Png);
        }

    }


来源:https://stackoverflow.com/questions/63417050/asynchronous-operations-within-a-loop-how-to-keep-control-of-execution

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!