Parallel using of WebView2 in async Task

1

I have simple default Windows desktop form Form1 with one button btn_Go as a test.
I want to run multiple parallel WebView2 instances and process html code from rendered page. To run WebView2 in parallel I use SemaphoreSlim (set to parallel 2). Another SemaphoreSlim is used for wait until WebView2 render the document (with some time delay).

But my code falls on await webBrowser.EnsureCoreWebView2Async();. Inner exception from debuger in WebView2 instance webBrowser is:

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

How can I call WebView2 multiple times in parallel and process all urls?

Full demo code:

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;
    }
  }
}

I have WebView2 runtime installed.

-- Tests

Prepare WebView2 in main thread and send it into child threads.

I have already tried to create list of WebView2 in Start method from main Thread var bro = new List<WebView2>() { new WebView2(), new WebView2(), new WebView2() }; and send WebView2 instance into await StartBrowser(bro[index], url); ... but this ends with the same error.

c#
multithreading
parallel-processing
semaphore
webview2
asked on Stack Overflow Feb 26, 2021 by Atiris • edited Feb 27, 2021 by Atiris

1 Answer

2

You could try replacing the Task.Run in your code with the custom TaskRunSTA method below:

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();
    }
}

This method starts a new STA thread, and runs a dedicated application message loop inside this thread. The asynchronous delegate that you pass as argument will run on this message loop, with the appropriate synchronization context installed. As long as your asynchronous delegate does not contain any await configured with .ConfigureAwait(false), all your code, including the events raised by the WebView2 component, should run on this thread.


Note: TBH I don't know if handling the first occurrence of the 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), and this class has a dedicated instance per thread. So each thread receives the Idle events that are associated with the message loop running on that thread. In other words there is no risk of receiving an event that is originated from some other unrelated message loop, that runs concurrently on another thread.

answered on Stack Overflow Feb 27, 2021 by Theodor Zoulias • edited Feb 27, 2021 by Theodor Zoulias

User contributions licensed under CC BY-SA 3.0