循环中的异步操作 - 如何保持对执行的控制?

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

的后续问题。

我正在尝试生成并保存一系列图像。渲染由 Helix Toolkit 完成,我被告知它使用 WPF 复合渲染线程。这会导致问题,因为它是异步执行的。

我最初的问题是我无法保存给定的图像,因为在我尝试保存它时它还没有被渲染。上面的答案通过将 'save' 操作放在低优先级调用的 Action 中提供了解决方法,从而确保渲染首先完成。

这对于一张图片来说没问题,但在我的应用程序中我需要多张图片。就目前而言,我无法控制事件的顺序,因为它们是异步发生的。我正在使用 For 循环,无论渲染和保存图像的进度如何,它都会继续。我需要一张一张生成图像,在开始下一张之前有足够的时间进行渲染和保存。

我曾尝试在循环中设置延迟,但这会导致其自身出现问题。例如,代码中注释的 async await 会导致跨线程问题,因为数据是在与完成渲染的线程不同的线程上创建的。我尝试了一个简单的延迟,但是这只会锁定所有内容 - 我认为部分原因是我正在等待的保存操作的优先级非常低。

我不能简单地将其视为一批独立的不相关异步任务,因为我在 GUI 中使用单个 HelixViewport3D 控件。必须按顺序生成图像。

我确实尝试过 SaveHelixPlotAsBitmap() 调用 DrawStuff() 的递归方法,但效果不是很好,而且这似乎不是一个好方法。

我尝试在每个循环上设置一个标志 ('busy') 并在继续之前等待它被重置,但那没有用 - 再次,因为异步执行。同样,我尝试使用计数器使循环与已生成的图像数量同步,但 运行 陷入类似问题。

我似乎陷入了我不想陷入的线程和异步操作的困境。

我该如何解决这个问题?

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);
    }
}

注意这些例子只是为了证明一个概念,TaskCompletionSource还需要处理错误

鉴于此测试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>

下面是一个示例,说明如何使用事件来了解视图何时处于您想要的状态。

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
        }
    }
}

如果你想从外部调用你的同步方法,你可以实现一个任务队列。

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
        }
    }
}

这将确保 UI 处于每次迭代所需的状态

您当然需要扩展它,以便收听您希望渲染的每个点的 Loaded 事件。

编辑: 由于 PointsVisual3D 没有 Loaded 事件,您可以通过挂钩到您之前使用的事件来完成任务。不理想,但应该可以。

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;
}

下面的解决方案。这是我对 Jason 的回答中提供的代码的实现。所有重要的部分都归功于 Jason。

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);
        }

    }