如何在超时后取消任务等待

Posted

技术标签:

【中文标题】如何在超时后取消任务等待【英文标题】:How to cancel Task await after a timeout period 【发布时间】:2014-04-09 23:12:09 【问题描述】:

我正在使用此方法以编程方式实例化 Web 浏览器,导航到 url 并在文档完成后返回结果。

如果文档加载时间超过 5 秒,我如何能够停止 Task 并让 GetFinalUrl() 返回 null

我见过很多使用 TaskFactory 的示例,但我无法将它应用到这段代码中。

 private Uri GetFinalUrl(PortalMerchant portalMerchant)
    
        SetBrowserFeatureControl();
        Uri finalUri = null;
        if (string.IsNullOrEmpty(portalMerchant.Url))
        
            return null;
        
        Uri trackingUrl = new Uri(portalMerchant.Url);
        var task = MessageLoopWorker.Run(DoWorkAsync, trackingUrl);
        task.Wait();
        if (!String.IsNullOrEmpty(task.Result.ToString()))
        
            return new Uri(task.Result.ToString());
        
        else
        
            throw new Exception("Parsing Failed");
        
    

// by Noseratio - http://***.com/users/1768303/noseratio    

static async Task<object> DoWorkAsync(object[] args)

    _threadCount++;
    Console.WriteLine("Thread count:" + _threadCount);
    Uri retVal = null;
    var wb = new WebBrowser();
    wb.ScriptErrorsSuppressed = true;

    TaskCompletionSource<bool> tcs = null;
    WebBrowserDocumentCompletedEventHandler documentCompletedHandler = (s, e) => tcs.TrySetResult(true);

    foreach (var url in args)
    
        tcs = new TaskCompletionSource<bool>();
        wb.DocumentCompleted += documentCompletedHandler;
        try
        
            wb.Navigate(url.ToString());
            await tcs.Task;
        
        finally
        
            wb.DocumentCompleted -= documentCompletedHandler;
        

        retVal = wb.Url;
        wb.Dispose();
        return retVal;
    
    return null;


public static class MessageLoopWorker

    #region Public static methods

    public static async Task<object> Run(Func<object[], Task<object>> worker, params object[] args)
    
        var tcs = new TaskCompletionSource<object>();

        var thread = new Thread(() =>
        
            EventHandler idleHandler = null;

            idleHandler = async (s, e) =>
            
                // handle Application.Idle just once
                Application.Idle -= idleHandler;

                // return to the message loop
                await Task.Yield();

                // and continue asynchronously
                // propogate the result or exception
                try
                
                    var result = await worker(args);
                    tcs.SetResult(result);
                
                catch (Exception ex)
                
                    tcs.SetException(ex);
                

                // signal to exit the message loop
                // Application.Run will exit at this point
                Application.ExitThread();
            ;

            // handle Application.Idle just once
            // to make sure we're inside the message loop
            // and SynchronizationContext has been correctly installed
            Application.Idle += idleHandler;
            Application.Run();
        );

        // set STA model for the new thread
        thread.SetApartmentState(ApartmentState.STA);

        // start the thread and await for the task
        thread.Start();
        try
        
            return await tcs.Task;
        
        finally
        
            thread.Join();
        
    
    #endregion

【问题讨论】:

很高兴看到有人实际上在使用this code :) 我还有另一个例子,它用超时做类似的事情:***.com/a/21152965/1768303。寻找 var cts = new CancellationTokenSource(30000) 谢谢。您是否有任何机会在控制台应用程序中执行此操作的示例?另外我不认为 webBrowser 可以是一个类变量,因为我正在为每个并行运行整个事情,迭代数千个 URL 我使用了您在我的控制台应用程序中建议的代码并得到: System.Threading.ThreadStateException: ActiveX control '8856f961-340a-11d0-a96b-00c04fd705a2' cannot be instantized because current thread is not in一个单线程的公寓。我猜这是消息循环工作线程在您的其他代码示例中所做的。这是我无法使用cancellationToken的原因。帮助表示赞赏。我会继续努力的。 看来它不仅需要在 STA 线程上运行,还需要一个消息循环工作者,如:***.com/a/19737374/1768303 【参考方案1】:

更新:基于WebBrowser的控制台网页scraper的最新版本可以是found on Github。

更新:Adding a pool of WebBrowser objects 用于多个并行下载。

您是否有任何示例说明如何在控制台应用程序中执行此操作 机会?另外我不认为 webBrowser 可以是一个类变量,因为 我正在为每个并行运行整个事情,迭代 数以千计的网址

下面是一个或多或少通用的基于 **WebBrowser 的网络 scraper ** 的实现,它用作控制台应用程序。这是我之前与WebBrowser 相关的一些工作的整合,包括问题中引用的代码:

Capturing an image of the web page with opacity

Loading a page with dynamic AJAX content

Creating an STA message loop thread for WebBrowser

Loading a set of URLs, one after another

Printing a set of URLs with WebBrowser

Web page UI automation

几点:

可重用MessageLoopApartment 类用于启动和运行带有自己的消息泵的 WinForms STA 线程。它可以从控制台应用程序中使用,如下所示。此类公开了一个 TPL 任务调度程序 (FromCurrentSynchronizationContext) 和一组 Task.Factory.StartNew 包装器以使用此任务调度程序。

这使得 async/await 成为在单独的 STA 线程上运行 WebBrowser 导航任务的绝佳工具。这样,WebBrowser 对象就会在该线程上创建、导航和销毁。虽然,MessageLoopApartment 并没有特别绑定到 WebBrowser

使用Browser Feature Control 启用html5 渲染很重要,否则WebBrowser 对象默认在IE7 仿真模式下运行。 这就是SetFeatureBrowserEmulation 在下面所做的。

可能无法始终以 100% 的概率确定网页何时完成渲染。有些页面非常复杂,并且使用持续的 AJAX 更新。然而我们 可以非常接近,首先处理DocumentCompleted 事件,然后轮询页面的当前HTML 快照以进行更改并检查WebBrowser.IsBusy 属性。这就是NavigateAsync 在下面所做的。

在上述之上存在超时逻辑,以防页面呈现永无止境(注意CancellationTokenSourceCreateLinkedTokenSource)。

using Microsoft.Win32;
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Console_22239357

    class Program
    
        // by Noseratio - https://***.com/a/22262976/1768303

        // main logic
        static async Task ScrapeSitesAsync(string[] urls, CancellationToken token)
        
            using (var apartment = new MessageLoopApartment())
            
                // create WebBrowser inside MessageLoopApartment
                var webBrowser = apartment.Invoke(() => new WebBrowser());
                try
                
                    foreach (var url in urls)
                    
                        Console.WriteLine("URL:\n" + url);

                        // cancel in 30s or when the main token is signalled
                        var navigationCts = CancellationTokenSource.CreateLinkedTokenSource(token);
                        navigationCts.CancelAfter((int)TimeSpan.FromSeconds(30).TotalMilliseconds);
                        var navigationToken = navigationCts.Token;

                        // run the navigation task inside MessageLoopApartment
                        string html = await apartment.Run(() =>
                            webBrowser.NavigateAsync(url, navigationToken), navigationToken);

                        Console.WriteLine("HTML:\n" + html);
                    
                
                finally
                
                    // dispose of WebBrowser inside MessageLoopApartment
                    apartment.Invoke(() => webBrowser.Dispose());
                
            
        

        // entry point
        static void Main(string[] args)
        
            try
            
                WebBrowserExt.SetFeatureBrowserEmulation(); // enable HTML5

                var cts = new CancellationTokenSource((int)TimeSpan.FromMinutes(3).TotalMilliseconds);

                var task = ScrapeSitesAsync(
                    new[]  "http://example.com", "http://example.org", "http://example.net" ,
                    cts.Token);

                task.Wait();

                Console.WriteLine("Press Enter to exit...");
                Console.ReadLine();
            
            catch (Exception ex)
            
                while (ex is AggregateException && ex.InnerException != null)
                    ex = ex.InnerException;
                Console.WriteLine(ex.Message);
                Environment.Exit(-1);
            
        
    

    /// <summary>
    /// WebBrowserExt - WebBrowser extensions
    /// by Noseratio - https://***.com/a/22262976/1768303
    /// </summary>
    public static class WebBrowserExt
    
        const int POLL_DELAY = 500;

        // navigate and download 
        public static async Task<string> NavigateAsync(this WebBrowser webBrowser, string url, CancellationToken token)
        
            // navigate and await DocumentCompleted
            var tcs = new TaskCompletionSource<bool>();
            WebBrowserDocumentCompletedEventHandler handler = (s, arg) =>
                tcs.TrySetResult(true);

            using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: true))
            
                webBrowser.DocumentCompleted += handler;
                try
                
                    webBrowser.Navigate(url);
                    await tcs.Task; // wait for DocumentCompleted
                
                finally
                
                    webBrowser.DocumentCompleted -= handler;
                
            

            // get the root element
            var documentElement = webBrowser.Document.GetElementsByTagName("html")[0];

            // poll the current HTML for changes asynchronosly
            var html = documentElement.OuterHtml;
            while (true)
            
                // wait asynchronously, this will throw if cancellation requested
                await Task.Delay(POLL_DELAY, token);

                // continue polling if the WebBrowser is still busy
                if (webBrowser.IsBusy)
                    continue;

                var htmlNow = documentElement.OuterHtml;
                if (html == htmlNow)
                    break; // no changes detected, end the poll loop

                html = htmlNow;
            

            // consider the page fully rendered 
            token.ThrowIfCancellationRequested();
            return html;
        

        // enable HTML5 (assuming we're running IE10+)
        // more info: https://***.com/a/18333982/1768303
        public static void SetFeatureBrowserEmulation()
        
            if (System.ComponentModel.LicenseManager.UsageMode != System.ComponentModel.LicenseUsageMode.Runtime)
                return;
            var appName = System.IO.Path.GetFileName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
            Registry.SetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION",
                appName, 10000, RegistryValueKind.DWord);
        
    

    /// <summary>
    /// MessageLoopApartment
    /// STA thread with message pump for serial execution of tasks
    /// by Noseratio - https://***.com/a/22262976/1768303
    /// </summary>
    public class MessageLoopApartment : IDisposable
    
        Thread _thread; // the STA thread

        TaskScheduler _taskScheduler; // the STA thread's task scheduler

        public TaskScheduler TaskScheduler  get  return _taskScheduler;  

        /// <summary>MessageLoopApartment constructor</summary>
        public MessageLoopApartment()
        
            var tcs = new TaskCompletionSource<TaskScheduler>();

            // start an STA thread and gets a task scheduler
            _thread = new Thread(startArg =>
            
                EventHandler idleHandler = null;

                idleHandler = (s, e) =>
                
                    // handle Application.Idle just once
                    Application.Idle -= idleHandler;
                    // return the task scheduler
                    tcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext());
                ;

                // handle Application.Idle just once
                // to make sure we're inside the message loop
                // and SynchronizationContext has been correctly installed
                Application.Idle += idleHandler;
                Application.Run();
            );

            _thread.SetApartmentState(ApartmentState.STA);
            _thread.IsBackground = true;
            _thread.Start();
            _taskScheduler = tcs.Task.Result;
        

        /// <summary>shutdown the STA thread</summary>
        public void Dispose()
        
            if (_taskScheduler != null)
            
                var taskScheduler = _taskScheduler;
                _taskScheduler = null;

                // execute Application.ExitThread() on the STA thread
                Task.Factory.StartNew(
                    () => Application.ExitThread(),
                    CancellationToken.None,
                    TaskCreationOptions.None,
                    taskScheduler).Wait();

                _thread.Join();
                _thread = null;
            
        

        /// <summary>Task.Factory.StartNew wrappers</summary>
        public void Invoke(Action action)
        
            Task.Factory.StartNew(action,
                CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Wait();
        

        public TResult Invoke<TResult>(Func<TResult> action)
        
            return Task.Factory.StartNew(action,
                CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Result;
        

        public Task Run(Action action, CancellationToken token)
        
            return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
        

        public Task<TResult> Run<TResult>(Func<TResult> action, CancellationToken token)
        
            return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
        

        public Task Run(Func<Task> action, CancellationToken token)
        
            return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
        

        public Task<TResult> Run<TResult>(Func<Task<TResult>> action, CancellationToken token)
        
            return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
        
    

【讨论】:

谢谢 Noseratio。该代码按原样完美运行,我可以轻松地对其进行调整以适应我的需求。我在并行 foreach 中使用它,它非常稳定。如果您需要使用 Web 浏览器在控制台应用程序中解析多个 URL,请不要再犹豫了。谢谢! @DanCook,不用担心,很高兴它有帮助。如果您并行执行此操作,只需确保将 WebBrowser 实例的数量限制在合理的数字,例如 3-4。您可以为此使用SemaphoreSlim.WaitAsync(这里有很多使用示例)。还有一点要记住,所有WebBrowser 实例共享同一个HTTP 会话(包括cookie)。 Parallel.ForEach(myList, new ParallelOptions MaxDegreeOfParallelism = 20 , myItem => 这应该将 WebBrowser 实例保持在 20 的最大值,对吧?生产服务器有不错的 RAM 和 SSD,所以希望 20 会没关系。在我的情况下,会话无关紧要,但这对其他人来说是一个很好的提示。 我在这里使用了 Stephen Toub 的示例:blogs.msdn.com/b/pfxteam/archive/2011/11/10/10235834.aspx 来尝试该任务。基于 Rx 的解决方案也非常有趣。实际上,每个使用 MaxDegreeOfParallelism 方式的并行似乎工作正常。解析了 15000 条记录,同时解析了 20 条,而且还没有崩溃(还没有?)。让我们结束这个问题,但如果您对我的电子邮件感兴趣,请随时回复我的电子邮件。荣誉 @Noseratio,您的代码在控制台应用程序中工作。我在 WinForms 应用程序中几乎尝试了您的确切代码,但我得到的唯一输出是URL: http://example.com。 WinForms 是什么导致了问题?差异:我为您的代码创建了一个新类-Program2。我在表单中添加了一个按钮,该按钮调用Program2.Start(new string[1]);。我班上的Start 取代了你们班上的Main。我还尝试了另一个使用默认public partial class Form1 : Form 的版本,将Main 替换为包含Main 正文的button1_Click。没有运气。想法?【参考方案2】:

我怀疑在另一个线程上运行处理循环不会很好,因为WebBrowser 是一个承载 ActiveX 控件的 UI 组件。

当你写TAP over EAP wrappers时,我建议使用扩展方法来保持代码干净:

public static Task<string> NavigateAsync(this WebBrowser @this, string url)

  var tcs = new TaskCompletionSource<string>();
  WebBrowserDocumentCompletedEventHandler subscription = null;
  subscription = (_, args) =>
  
    @this.DocumentCompleted -= subscription;
    tcs.TrySetResult(args.Url.ToString());
  ;
  @this.DocumentCompleted += subscription;
  @this.Navigate(url);
  return tcs.Task;

现在您的代码可以轻松应用超时:

async Task<string> GetUrlAsync(string url)

  using (var wb = new WebBrowser())
  
    var navigate = wb.NavigateAsync(url);
    var timeout = Task.Delay(TimeSpan.FromSeconds(5));
    var completed = await Task.WhenAny(navigate, timeout);
    if (completed == navigate)
      return await navigate;
    return null;
  

可以这样消费:

private async Task<Uri> GetFinalUrlAsync(PortalMerchant portalMerchant)

  SetBrowserFeatureControl();
  if (string.IsNullOrEmpty(portalMerchant.Url))
    return null;
  var result = await GetUrlAsync(portalMerchant.Url);
  if (!String.IsNullOrEmpty(result))
    return new Uri(result);
  throw new Exception("Parsing Failed");

【讨论】:

谢谢,我尝试了您的解决方案,但 Web 浏览器必须在 STA 线程上使用并具有消息循环工作器(就像在我的(Noseratio 的)原始代码中一样。我不知道如何考虑这进入你的解决方案 我写的代码打算从 UI 线程调用。可以创建一个单独的 STA 线程,但除非真的有必要,否则我不会。 WebBrowser 必须在 STA 线程上运行,因为 ActiveX 的工作方式。非常感谢您的回答。对于不需要使用网络浏览器的任何人 - 这确实有效,我对其进行了测试。 我知道这已经很老了,但我无法避免静态 Task NavigateAsync 代码中的“并非所有代码路径都返回值”。为了避免另一个问题,我在“TrySetResult”行中添加了“ToString()”。感谢您的帮助。 @Stephen:谢谢斯蒂芬。这就是我在您的博客中看到的内容:blog.stephencleary.com/2012/02/async-and-await.html【参考方案3】:

我正在尝试从 Noseratio 的解决方案以及 Stephen Cleary 的建议中获益。

这是我更新的代码,包含在 Stephen 的代码中,以及来自 Noseratio 的关于 AJAX 技巧的代码。

第一部分:斯蒂芬建议的Task NavigateAsync

public static Task<string> NavigateAsync(this WebBrowser @this, string url)

  var tcs = new TaskCompletionSource<string>();
  WebBrowserDocumentCompletedEventHandler subscription = null;
  subscription = (_, args) =>
  
    @this.DocumentCompleted -= subscription;
    tcs.TrySetResult(args.Url.ToString());
  ;
  @this.DocumentCompleted += subscription;
  @this.Navigate(url);
  return tcs.Task;

第二部分:一个新的Task NavAjaxAsync 运行 AJAX 的提示(基于 Noseratio 的代码)

public static async Task<string> NavAjaxAsync(this WebBrowser @this)

  // get the root element
  var documentElement = @this.Document.GetElementsByTagName("html")[0];

  // poll the current HTML for changes asynchronosly
  var html = documentElement.OuterHtml;

  while (true)
  
    // wait asynchronously
    await Task.Delay(POLL_DELAY);

    // continue polling if the WebBrowser is still busy
    if (webBrowser.IsBusy)
      continue;

    var htmlNow = documentElement.OuterHtml;
    if (html == htmlNow)
      break; // no changes detected, end the poll loop

    html = htmlNow;
  

  return @this.Document.Url.ToString();

第三部分:一个新的Task NavAndAjaxAsync 来获取导航和AJAX

public static async Task NavAndAjaxAsync(this WebBrowser @this, string url)

  await @this.NavigateAsync(url);
  await @this.NavAjaxAsync();

第四部分和最后一部分:来自 Stephen 的更新 Task GetUrlAsync 以及 Noseratio 的 AJAX 代码

async Task<string> GetUrlAsync(string url)

  using (var wb = new WebBrowser())
  
    var navigate = wb.NavAndAjaxAsync(url);
    var timeout = Task.Delay(TimeSpan.FromSeconds(5));
    var completed = await Task.WhenAny(navigate, timeout);
    if (completed == navigate)
      return await navigate;
    return null;
  

我想知道这是否是正确的方法。

【讨论】:

对不起,这无法在一条评论中解决(或者至少我不知道如何解决)。

以上是关于如何在超时后取消任务等待的主要内容,如果未能解决你的问题,请参考以下文章

使用Promise.race实现超时机制取消XHR请求

CompletableFuture 异步超时 和取消

量角器:失败:超时等待异步角度任务在11秒后完成

HttpClient - 任务被取消 - 如何获得确切的错误消息?

如何使用 Bash 读取超时?

如何使用 Bash 读取超时?