在异步任务中并行使用 WebView2

Posted

技术标签:

【中文标题】在异步任务中并行使用 WebView2【英文标题】:Parallel using of WebView2 in async Task 【发布时间】:2021-05-29 07:46:19 【问题描述】:

我有一个简单的默认 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并将其发送到子线程中。

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

【问题讨论】:

您可能会发现这很有用:Set ApartmentState on a Task,WebView2 组件可能不喜欢在其生命周期内被多个线程访问。 【参考方案1】:

您可以尝试用下面的自定义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 事件的第一次出现是否是在消息循环中嵌入自定义代码的最佳方式,但它似乎工作得很好。值得注意的是,该事件与内部类ThreadContext (source code) 进行了附加和分离,并且该类每个线程都有一个专用实例。因此,每个线程都会收到与在该线程上运行的消息循环相关联的 Idle 事件。换句话说,不存在接收源自其他不相关消息循环的事件的风险,该事件在另一个线程上并发运行。

【讨论】:

以上是关于在异步任务中并行使用 WebView2的主要内容,如果未能解决你的问题,请参考以下文章

在 iOS swift 中异步/并发/并行运行任务

在后台运行长时间运行的并行任务,同时允许小型异步任务更新前台

异步并行任务实现的几种方案

并行运行两个异步任务并在 .NET 4.5 中收集结果

同步 ,异步,并发/并行,串行

iOS多线程——同步异步串行并行