.net 支持取消的GetContextAsync()

but5z9lq  于 2023-03-20  发布在  .NET
关注(0)|答案(4)|浏览(147)

所以我启动了一个HttpListener来等待OAuth2的响应,在理想的情况下,它只会在用户登录浏览器时存在几秒钟,然后我们会收到令牌。
我还希望这有一个CancellationToken,以便用户可以停止收听后,延迟,如果他们希望这样做。
我最初的想法是使用沿着以下的东西:

_listener.Start();
Task<HttpListenerContext> t = _listener.GetContextAsync();            
while (!cancelled.IsCancellationRequested)
{
     if (t.IsCompleted)
     {
          break;
     }
     await Task.Run(() => Thread.Sleep(100));
}
HttpListenerContext ctx = t.Result;
//...
_listener.Stop();

但这并不适合我,原因有很多(奇怪的async用法、轮询等)。
所以我想我可以将同步版本_listener.GetContext()Task.Run(func<T>, CancellationToken)结合使用:

_listener.Start()
HttpListenerContext ctx = await Task.Run(() => _listener.GetContext(), cancelled);
//...
_listener.Stop();

这稍微好一点,代码至少更整洁了,尽管使用方法的同步版本与Task异步似乎有些笨拙...
然而,这并不像我所期望的那样(当令牌被取消时中止正在运行的任务)。
这让我觉得这应该是一件很简单的事情,所以我想我错过了什么。
因此我的问题是...如何以可取消的方式异步侦听HttpListener

r55awzrz

r55awzrz1#

因为GetContextAsync方法不支持取消,这基本上意味着您不可能取消实际的IO操作,也不可能取消该方法返回的Task,除非您使用AbortStopHttpListener。因此,这里的主要焦点始终是将控制流返回到代码的 hack
虽然@guru-stron和@peter-csala的答案应该都能解决问题,但我只是想分享另一种不必使用Task.WhenAny的方法。
您可以使用TaskCompletionSource来 Package 任务,如下所示:

public static class TaskExtensions
{
    public static Task<T> AsCancellable<T>(this Task<T> task, CancellationToken token)
    {
        if (!token.CanBeCanceled)
        {
            return task;
        }

        var tcs = new TaskCompletionSource<T>();
        // This cancels the returned task:
        // 1. If the token has been canceled, it cancels the TCS straightaway
        // 2. Otherwise, it attempts to cancel the TCS whenever
        //    the token indicates cancelled
        token.Register(() => tcs.TrySetCanceled(token), 
            useSynchronizationContext: false);

        task.ContinueWith(t =>
            {
                // Complete the TCS per task status
                // If the TCS has been cancelled, this continuation does nothing
                if (task.IsCanceled)
                {
                    tcs.TrySetCanceled();
                }
                else if (task.IsFaulted)
                {
                    tcs.TrySetException(t.Exception);
                }
                else
                {
                    tcs.TrySetResult(t.Result);
                }
            },
            CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);

        return tcs.Task;
    }
}

并按如下方式传递控件:

var cts = new CancellationTokenSource();
cts.CancelAfter(3000);

try
{
    var context = await listener.GetContextAsync().AsCancellable(cts.Token);
}
catch (TaskCanceledException)
{
    // ...
}
czfnxgou

czfnxgou2#

在net Core / Net 6中,您可以通过WaitAsync()方法设置cancellationToken和/或超时。

await http.GetContextAsync().WaitAsync(cancelToken.Token);
await http.GetContextAsync().WaitAsync(TimeSpan.FromMinutes(1));
await http.GetContextAsync().WaitAsync(TimeSpan.FromMinutes(1),cancelToken.Token);

它将在取消和/或超时时引发异常。
示例:

var http = new HttpListener();
http.Prefixes.Add("http://+:80/");
http.Start();
CancellationTokenSource cancelToken = new CancellationTokenSource();
try {                
    var context = await http.GetContextAsync().WaitAsync(TimeSpan.FromMinutes(1) , cancelToken.Token);
} catch (TimeoutException ex) {
    Log("Timed out!");
} catch (TaskCanceledException ex) {
    Log("Cancelled!");
}
wwtsj6pe

wwtsj6pe3#

我建议创建可取消的无限任务(例如Task.Delay(Timeout.Infinite, token))并使用Task.WhenAny

var cts = new CancellationTokenSource(); // token source controled by consumer "outside"

var token = cts.Token;    
var httpListener = new HttpListener();
httpListener.Start();
var t = httpListener.GetContextAsync();
// to cancel the infinite delay task if listener finishes first
var localCts = CancellationTokenSource.CreateLinkedTokenSource(token);
var completed = await Task.WhenAny(t, Task.Delay(Timeout.Infinite, localCts.Token));
if (completed == t) // check that completed task is one from listener
{
    localCts.Cancel(); // cancel the infinite task
    HttpListenerContext ctx = t.Result;
    //...
}
httpListener.Stop();
zzoitvuj

zzoitvuj4#

这里还有另一个解决方案:

var cancellationSignal = new TaskCompletionSource<object>();
var contextTask = _listener.GetContextAsync();
using (cancelled.Register(state => ((TaskCompletionSource<object>)state).TrySetResult(null), cancellationSignal))
{
    if (contextTask != await Task.WhenAny(contextTask, cancellationSignal.Task).ConfigureAwait(false))
        break; //task is cancelled
}

因为我们不能等待CancellationToken,这就是为什么必须应用以下技巧

  • CancellationToken确实公开了一个Register方法,我们可以在其中定义一个回调函数,每当发生取消操作时都会调用该函数
  • 这里我们可以提供一个委托,它将一个等待的设置为已完成
  • 因此,我们可以await该任务

为了创建一个Task,它被设置为在取消发生时完成,我使用了TaskCompletionSource,你也可以使用SemaphoreSlim或任何其他有异步等待的信令对象,比如AsyncManualResetEvent

  • 因此,我们将cancellationSignal作为state参数传递给Register
  • 在委托内部,我们必须将其强制转换回TCS,以便能够在其上调用TrySetResult
  • 在using块中,我们等待Task.WhenAny
  • 它将返回最先完成的Task
  • 如果那个Task是抵消,那么我们可以break/return/throw...
  • 如果Task就是contextTask,则我们可以继续正常流程

相关问题