在异步任务中并行使用 WebView2

Parallel using of WebView2 in async Task

我有简单的默认 Windows 桌面表单 Form1 和一个按钮 btn_Go 作为测试。
我想 运行 多个并行 WebView2 实例并处理来自渲染页面的 html 代码。 要并行 运行 WebView2,我使用 SemaphoreSlim(设置为并行 2)。另一个 SemaphoreSlim 用于等待 WebView2 呈现文档(有一些时间延迟)。

但我的代码落在 await webBrowser.EnsureCoreWebView2Async(); 上。 WebView2 实例 webBrowser 中调试器的内部异常是:

{"Cannot change thread mode after it is set. (Exception from HRESULT: 0x80010106 (RPC_E_CHANGED_MODE))"} System.Exception {System.Runtime.InteropServices.COMException}

如何并行多次调用 WebView2 并处理所有 url?

完整的演示代码:

using Microsoft.Web.WebView2.WinForms;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WebView2Test
{
  public partial class Form1 : Form
  {
    public Form1() { InitializeComponent(); }

    private void btn_Go_Click(object sender, EventArgs e) { Start(); }

    private async void Start()
    {
        var urls = new List<string>() { "https://www.google.com/search?q=test1", "https://www.google.com/search?q=test2", "https://www.google.com/search?q=test3" };
        var tasks = new List<Task>();
        var semaphoreSlim = new SemaphoreSlim(2);
        foreach (var url in urls)
        {
            await semaphoreSlim.WaitAsync();
            tasks.Add(
            Task.Run(async () => {
                try { await StartBrowser(url); }
                finally { semaphoreSlim.Release(); }
            }));
        }
        await Task.WhenAll(tasks);
    }

    public async Task<bool> StartBrowser(string url)
    {
        SemaphoreSlim semaphore = new System.Threading.SemaphoreSlim(0, 1);

        System.Timers.Timer wait = new System.Timers.Timer();
        wait.Interval = 500;
        wait.Elapsed += (s, e) =>
        {
            semaphore.Release();
        };
        WebView2 webBrowser = new WebView2();
        webBrowser.NavigationCompleted += (s, e) =>
        {
            if (wait.Enabled) { wait.Stop(); }
            wait.Start();
        };
        await webBrowser.EnsureCoreWebView2Async();
        webBrowser.CoreWebView2.Navigate(url);

        await semaphore.WaitAsync();
        if (wait.Enabled) { wait.Stop(); }

        var html = await webBrowser.CoreWebView2.ExecuteScriptAsync("document.documentElement.outerHTML");
        return html.Length > 10;
    }
  }
}

我已经安装了WebView2 runtime

-- 测试

在主线程中准备WebView2并发送到子线程中

我已经尝试在 Start 方法中从主线程 var bro = new List<WebView2>() { new WebView2(), new WebView2(), new WebView2() }; 创建 WebView2 列表并将 WebView2 实例发送到 await StartBrowser(bro[index], url); ... 但这以同样的错误。

您可以尝试用下面的自定义 TaskRunSTA 方法替换代码中的 Task.Run

public static Task TaskRunSTA(Func<Task> action)
{
    var tcs = new TaskCompletionSource<object>(
        TaskCreationOptions.RunContinuationsAsynchronously);
    var thread = new Thread(() =>
    {
        Application.Idle += Application_Idle;
        Application.Run();
    });
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    return tcs.Task;

    async void Application_Idle(object sender, EventArgs e)
    {
        Application.Idle -= Application_Idle;
        try
        {
            await action();
            tcs.SetResult(null);
        }
        catch (Exception ex) { tcs.SetException(ex); }
        Application.ExitThread();
    }
}

此方法启动一个新的 STA 线程,并且 运行在此线程内有一个专用的应用程序消息循环。作为参数传递的异步委托将在此消息循环中 运行,并安装适当的同步上下文。只要您的异步委托不包含任何配置有 .ConfigureAwait(false)await,您的所有代码(包括 WebView2 组件引发的事件)都应该在此线程上 运行。


注意: TBH 我不知道是否处理第一次出现的 Application.Idle event is the best way to embed custom code in a message loop, but it seems to work quite well. It is worth noting that this event is attached and detached from an internal class ThreadContext (source code),并且这个 class 每个线程都有一个专用实例.因此每个线程都会收到与该线程上的消息循环 运行ning 关联的 Idle 事件。换句话说,没有接收来自其他不相关消息循环的事件的风险,运行s 在另一个线程上并发。