异步等待线程完成

4

我有一个函数可以从电脑上截取屏幕,但不幸的是它会阻塞主UI,因此我决定将其异步化[线程调用]。然而,我在等待线程返回Bitmap结果时遇到了麻烦。

以下是我的代码:

/// <summary>
/// Asynchronously uses the snapshot method to get a shot from the screen.
/// </summary>
/// <returns> A snapshot from the screen.</returns>
private Bitmap SnapshotAsync()
{
    Bitmap image = null;
    new Thread(() => image = Snapshot()).Start();

    while (image == null)
    {
        new Thread(() => Thread.Sleep(500)).Start(); //Here i create new thread to wait but i don't think this is a good way at all.
    }
    return image;
}

/// <summary>
/// Takes a screen shots from the computer.
/// </summary>
/// <returns> A snapshot from the screen.</returns>
private Bitmap Snapshot()
{
    var sx = Screen.PrimaryScreen.Bounds.Width;
    var sy = Screen.PrimaryScreen.Bounds.Height;
    var shot = new Bitmap(sx, sy, PixelFormat.Format32bppArgb);
    var gfx = Graphics.FromImage(shot);
    gfx.CopyFromScreen(0, 0, 0, 0, new Size(sx, sy));
    return shot;
}

虽然上述方法按照我的要求异步工作,但我相信它可以改进。特别是我执行数百个线程等待结果的方式,这种方式我确定不好。因此,我真的需要有人查看代码并告诉我如何改进它。[注意我使用的是.NET 3.5]提前感谢。
通过Eve和SiLo的帮助解决了问题,以下是最佳的两个答案:
  • 1:
>     private void TakeScreenshot_Click(object sender, EventArgs e)
>     {
>       TakeScreenshotAsync(OnScreenshotTaken);
>     }
>     
>     private static void OnScreenshotTaken(Bitmap screenshot)
>     {
>       using (screenshot)
>         screenshot.Save("screenshot.png", ImageFormat.Png);
>     }
>     
>     private static void TakeScreenshotAsync(Action<Bitmap> callback)
>     {
>       var screenRect = Screen.PrimaryScreen.Bounds;
>       TakeScreenshotAsync(screenRect, callback);
>     }
>     
>     private static void TakeScreenshotAsync(Rectangle bounds, Action<Bitmap> callback)
>     {
>       var screenshot = new Bitmap(bounds.Width, bounds.Height,
>                                   PixelFormat.Format32bppArgb);
>     
>       ThreadPool.QueueUserWorkItem((state) =>
>       {
>         using (var g = Graphics.FromImage(screenshot))
>           g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size);
>     
>         if (callback != null)
>           callback(screenshot);
>       });
>     }

抱歉,我没有翻译HTML标记的能力。请提供纯文本格式的内容。
>     void SnapshotAsync(Action<Bitmap> callback)
>     {
>         new Thread(Snapshot) {IsBackground = true}.Start(callback);
>     }

>     void Snapshot(object callback)
>     {
>         var action = callback as Action<Bitmap>;
>         var sx = Screen.PrimaryScreen.Bounds.Width;
>         var sy = Screen.PrimaryScreen.Bounds.Height;
>         var shot = new Bitmap(sx, sy, PixelFormat.Format32bppArgb);
>         var gfx = Graphics.FromImage(shot);
>         gfx.CopyFromScreen(0, 0, 0, 0, new Size(sx, sy));
>         action(shot);
>     }

Usage, for example, through a button's click:

void button1_Click(object sender, EventArgs e)
{
    SnapshotAsync(bitmap => MessageBox.Show("Copy successful!"));
}

不,先生,我不能使用它,我只是想在 .Net 3.5 上找到一个解决方案。 - Roman Ratskey
4个回答

3
async/await关键字非常优雅地实现了你要做的事情。
以下是我如何将你的方法转换为正确的模式:
private static async Task<Bitmap> TakeScreenshotAsync()
{
  var screenRect = Screen.PrimaryScreen.Bounds;
  return await TakeScreenshotAsync(screenRect);
}

private static async Task<Bitmap> TakeScreenshotAsync(Rectangle bounds)
{
  var screenShot = new Bitmap(bounds.Width, bounds.Height, 
                              PixelFormat.Format32bppArgb);

  // This executes on a ThreadPool thread asynchronously!
  await Task.Run(() =>
  {
    using (var g = Graphics.FromImage(screenShot))
      g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size);

  });

  return screenShot;
}

然后你需要做类似这样的事情:
private async void TakeScreenshot_Click(object sender, EventArgs e)
{
  var button = sender as Button;
  if(button == null) return;

  button.Enabled = false;
  button.Text = "Screenshoting...";

  var bitmap = await TakeScreenshotAsync();
  bitmap.Save("screenshot.png", ImageFormat.Png);

  button.Text = "Take Screenshot";
  button.Enabled = true;
}

他明确表示他不能使用.NET 4.5。 - e_ne
当我发布时,他的编辑还没有出现。别担心,我已经在下面发布了一个3.5的解决方案。 - Erik

2
您可以使用基于事件的异步模式来解决这个问题:
void SnapshotAsync(Action<Bitmap> callback)
{
    new Thread(Snapshot) {IsBackground = true}.Start(callback);
}

void Snapshot(object callback)
{
    var action = callback as Action<Bitmap>;
    var sx = Screen.PrimaryScreen.Bounds.Width;
    var sy = Screen.PrimaryScreen.Bounds.Height;
    var shot = new Bitmap(sx, sy, PixelFormat.Format32bppArgb);
    var gfx = Graphics.FromImage(shot);
    gfx.CopyFromScreen(0, 0, 0, 0, new Size(sx, sy));
    action(shot);
}

例如,通过单击按钮来使用:

void button1_Click(object sender, EventArgs e)
{
    SnapshotAsync(bitmap => MessageBox.Show("Copy successful!"));
}

根据作者的要求,它不会阻塞原始线程。但是,如果您必须通过回调操作UI,请小心记住使用Invoke及其等效项。

编辑:请阅读SiLo的评论,了解您可以应用于上述代码的一些良好实践和优化。


1
使用 ThreadPool 线程比每次创建新的 Thread 更可取。同时,调用 Graphics.Dispose()Bitmap.Dispose() 是良好的实践,以避免沉重的内存泄漏,特别是在处理可能达到数百万像素的 32bpp 图像时。 - Erik
@SiLo,我同意你提出的所有观点。这个例子的目的只是为了让作者在不太改变他的代码的情况下介绍EAP。我会编辑我的帖子并指向你的评论。 - e_ne
@SiLo,你能否提供一个在我的情况下使用ThreadPool的示例,因为我对此完全不了解。 - Roman Ratskey
1
@RuneS 你只需要将 SnapshotAsync 的主体替换为:ThreadPool.QueueUserWorkItem(Snapshot, callback); - e_ne

2

我刚刚看到你的修改,建议使用3.5而不是4.5版本。这很遗憾,但肯定仍然可以实现。我创建了这个第二个答案,以便使用async/await的人可以将第一个答案作为示例。

现在说说你的解决方案,其实并没有太大区别:

private void TakeScreenshot_Click(object sender, EventArgs e)
{
  TakeScreenshotAsync(OnScreenshotTaken);
}

private static void OnScreenshotTaken(Bitmap screenshot)
{
  using (screenshot)
    screenshot.Save("screenshot.png", ImageFormat.Png);
}

private static void TakeScreenshotAsync(Action<Bitmap> callback)
{
  var screenRect = Screen.PrimaryScreen.Bounds;
  TakeScreenshotAsync(screenRect, callback);
}

private static void TakeScreenshotAsync(Rectangle bounds, Action<Bitmap> callback)
{
  var screenshot = new Bitmap(bounds.Width, bounds.Height,
                              PixelFormat.Format32bppArgb);

  ThreadPool.QueueUserWorkItem((state) =>
  {
    using (var g = Graphics.FromImage(screenshot))
      g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size);

    if (callback != null)
      callback(screenshot);
  });
}

0

抱歉,但您尝试的逻辑并不太明智。

  • 您想要截屏。
  • 您不希望UI线程被阻塞,因此您使用异步操作。

恭喜。到目前为止,这是有意义的。

现在是您不想告诉任何人您尝试过的部分:

  • 现在您想在UI线程中等待异步操作完成。

然后我们回到起点-您阻塞了UI线程。什么也没实现。从逻辑上讲,您最终处于与开始时完全相同的位置。

好的,解决方案:

  • 首先,摆脱线程,使用任务(Task)。更高效。
  • 其次,认识到在UI线程中等待没有意义。禁用UI元素,然后在处理结束时再重新启用它们。

将此视为状态机问题(UI处于“工作”或“等待命令”状态),以便您不会阻塞。这是唯一的处理方式-因为如果您然后等待执行完成,则整个异步操作就毫无用处。

你不能启动一个方法,然后等待处理完成阻塞线程 - 如果你尝试这样做,那么整个异步操作就是毫无意义的。


1
汤姆先生,我真正想做的是在不阻塞主UI线程的情况下截屏,特别是我每分钟要截取数百张屏幕截图,这当然会使UI完全冻结,所以我希望以一种有效的方式在后台完成这项工作。我考虑过使用后台工作者,但我不喜欢与它一起工作。因此,简单地说,我想等待一个线程完成它的工作,然后返回结果... - Roman Ratskey
就像我说的那样 - 你不能在UI线程中这样做。你必须采取状态机的方法。就是这么简单,现实并不关心你是否喜欢它。 - TomTom

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接