c# - 循环内的异步操作 - 如何保持对执行的控制?
问题描述
这个问题的后续问题。
我正在尝试生成并保存一系列图像。渲染由Helix Toolkit完成,我听说它使用 WPF 复合渲染线程。这会导致问题,因为它是异步执行的。
我最初的问题是我无法保存给定的图像,因为当时我试图保存它尚未渲染。上面的答案通过将“保存”操作放在Action
以低优先级调用的方法中来解决此问题,从而确保渲染首先完成。
这对于一张图片来说很好,但在我的应用程序中,我需要多张图片。就目前而言,我无法控制事件的顺序,因为它们是异步发生的。我正在使用一个For
循环,无论渲染和保存图像的进度如何,它都会继续。我需要一张一张生成图像,在开始下一张之前有足够的时间进行渲染和保存。
我曾尝试将延迟放入循环中,但这会导致其自身的问题。例如async await
,代码中的注释会导致跨线程问题,因为数据是在与进行渲染的线程不同的线程上创建的。我尝试了一个简单的延迟,但是这只会锁定所有内容-我认为部分原因是我正在等待的保存操作的优先级很低。
我不能简单地将其视为一批独立的不相关的异步任务,因为我HelixViewport3D
在 GUI 中使用单个控件。图像必须按顺序生成。
我确实尝试了一种递归方法,其中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 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
}
}
}
您当然需要扩展它,以便聆听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;
}
推荐阅读
- xml - 查询一个根 Element 下的多个 XML 节点
- notifications - PWA 中的 ServiceWorkerRegistration.showNotification,将应用程序带到前台
- python - 使用 Pandas 绘制箱线图
- ios - ld:找不到 -lCocoaAsyncSocket 的库 - React Native
- kotlin - 如何从一个地方收集流量并从另一个地方取消?
- sql-server - 关键字 'key' pyodbc 错误 42000 附近的语法不正确
- apache-flink - 在 ProcessWindowFunction 上实现 onTimer 函数
- sql - 如何将年份(带逗号)转换为年、月和日
- h2 - Rundeck H2 数据库清理
- css - 反应组件中的CSS样式