循环中的异步操作 - 如何保持对执行的控制?
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);
}
}
我正在尝试生成并保存一系列图像。渲染由 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);
}
}