【问题标题】:Async Web Service call not consistently asynchronous异步 Web 服务调用不一致是异步的
【发布时间】:2019-11-22 16:45:04
【问题描述】:

我在使用带有 ASP.NET Web API 2 的任务并行库创建异步 Web 服务时遇到问题。我对方法 StartAsyncTest 进行了异步调用,并创建了一个取消令牌以中止该方法。我全局存储令牌,然后检索它并从第二种方法CancelAsyncTest 调用它。代码如下:

// Private Global Dictionary to hold text search tokens
private static Dictionary<string, CancellationTokenSource> TextSearchLookup
    = new Dictionary<string, CancellationTokenSource>();

/// <summary>
/// Performs an asynchronous test using a Cancellation Token
/// </summary>
[Route("StartAsyncTest")]
[HttpGet]
public async Task<WsResult<long>> StartAsyncTest(string sSearchId)
{
    Log.Debug("Method: StartAsyncTest; ID: " + sSearchId + "; Message: Entering...");

    WsResult<long> rWsResult = new WsResult<long>
    {
        Records = -1
    };

    try
    {
        var rCancellationTokenSource = new CancellationTokenSource();
        {
            var rCancellationToken = rCancellationTokenSource.Token;

            // Set token right away in TextSearchLookup
            TextSearchLookup.Add("SyncTest-" + sSearchId, rCancellationTokenSource);

            HttpContext.Current.Session["SyncTest-" + sSearchId] =
                rCancellationTokenSource;

            try
            {
                // Start a New Task which has the ability to be cancelled 
                var rHttpContext = (HttpContext)HttpContext.Current;

                await Task.Factory.StartNew(() =>
                {
                    HttpContext.Current = rHttpContext;

                    int? nCurrentId = Task.CurrentId;

                    StartSyncTest(sSearchId, rCancellationToken);

                }, TaskCreationOptions.LongRunning);
            }
            catch (OperationCanceledException e)
            {
                Log.Debug("Method: StartAsyncText; ID: " + sSearchId
                    + "; Message: Cancelled!");
            }
        }
    }
    catch (Exception ex)
    {
        rWsResult.Result = "ERROR";
        if (string.IsNullOrEmpty(ex.Message) == false)
        {
            rWsResult.Message = ex.Message;
        }
    }

    // Remove token from Dictionary
    TextSearchLookup.Remove(sSearchId);
    HttpContext.Current.Session[sSearchId] = null;
    return rWsResult;
}

private void StartSyncTest(string sSearchId, CancellationToken rCancellationToken)
{
    // Spin for 1100 seconds
    for (var i = 0; i < 1100; i++)
    {
        if (rCancellationToken.IsCancellationRequested)
        {
            rCancellationToken.ThrowIfCancellationRequested();
        }

        Log.Debug("Method: StartSyncTest; ID: " + sSearchId
            + "; Message: Wait Pass #" + i + ";");

        Thread.Sleep(1000);
    }

    TextSearchLookup.Remove("SyncTest-" + sSearchId);

    HttpContext.Current.Session.Remove("SyncTest-" + sSearchId);
}

[Route("CancelAsyncTest")]
[HttpGet]
public WsResult<bool> CancelAsyncTest(string sSearchId)
{
    Log.Debug("Method: CancelAsyncTest; ID: " + sSearchId
        + "; Message: Cancelling...");

    WsResult<bool> rWsResult = new WsResult<bool>
    {
        Records = false
    };

    CancellationTokenSource rCancellationTokenSource =
        (CancellationTokenSource)HttpContext.Current.Session["SyncTest-" + sSearchId];

    // Session doesn't always persist values. Use TextSearchLookup as backup
    if (rCancellationTokenSource == null)
    {
        rCancellationTokenSource = TextSearchLookup["SyncTest-" + sSearchId];
    }

    if (rCancellationTokenSource != null)
    {
        rCancellationTokenSource.Cancel();

        TextSearchLookup.Remove("SyncTest-" + sSearchId);
        HttpContext.Current.Session.Remove("SyncTest-" + sSearchId);

        rWsResult.Result = "OK";
        rWsResult.Message = "Cancel delivered successfully!";
    }
    else
    {
        rWsResult.Result = "ERROR";
        rWsResult.Message = "Reference unavailable to cancel task"
            + " (if it is still running)";
    }

    return rWsResult;
}

在我将它部署到 IIS 后,我第一次调用 StartAsyncTest,然后调用 CancelAsyncTest(通过 REST 端点),两个请求都通过并按预期取消。但是,第二次,CancelAsyncTest 请求只是挂起,并且仅在 StartAsyncTest 完成后(1100 秒后)调用该方法。我不知道为什么会这样。 StartAsyncTest 似乎在调用一次后劫持了所有线程。感谢任何人提供的任何帮助!

【问题讨论】:

  • 首先,您介意我问为什么您要执行的任务是异步的吗?它是否占用了大量的处理器能力,还是因为您必须等待 I/O 之类的东西?
  • 另外,虽然我不能说这是一个明确的答案,但如果将 HttpContext 传递给不同的线程,您可能会出错 - 您只是使用它来跟踪线程吗?使用并发字典来跟踪它。
  • 每个请求是否都有唯一的sSearchId?不然会出问题。您可能会放弃 Session 的用法,因为我怀疑 CTS 是否会按照通常的方式正确序列化/反序列化。另外,将同步等待更改为异步;您可以将整个Task.Factory.StartNew... 替换为简单的await Task.Delay(TimeSpan.FromSeconds(1100.0), rCancellationToken);,如果CTS 被取消,则会抛出TaskCanceledException。
  • 是的,真正的用例(这只是一个显示问题的示例)是在文本文件中搜索字符串,这会占用大量处理器资源。 HttpContext 被传递给线程,因为调用 Task.Factory.StartNew 不会持久化它。此外,每个sSearchId 都是独一无二的
  • 关于将取消源保存在静态变量中的Dictionary,请看一下:Lifetime of ASP.NET Static Variable

标签: c# asp.net-web-api2 task-parallel-library


【解决方案1】:

我全局存储令牌,然后检索它并从第二个方法 CancelAsyncTest 调用它。

这可能不是一个好主意。您可以“全局”存储这些令牌,但这只是“全局”到单个服务器。一旦第二台服务器进入画面,这种方法就会中断。

也就是说,永远不应该分配HttpContext.Current。这很可能是您看到的奇怪行为的原因。此外,如果您的真实代码比ThrowIfCancellationRequested 更复杂 - 即,如果它实际上是侦听 CancellationToken - 那么对Cancel 的调用可以从在对Cancel的调用中,这将导致HttpContext.Current的值相当混乱。

总结一下:

  • 我建议完全取消这种方法;它在网络农场根本不起作用。相反,请将您的“任务状态”保存在数据库等外部存储系统中。
  • 不要跨线程传递HttpContext。

【讨论】:

  • 我不确定如何将CancellationToken 存储在外部数据库中?
  • @vinays84:你不能直接。您必须编写自己的系统。例如,您可以有一个由正在执行的任务轮询的 CancellationRequested 列。或者你可以使用像 SignalR 这样的发布/订阅系统。
【解决方案2】:

一位同事向Task.Factory.StartNew(在StartAsyncTest 内)提供了一个替代电话:

await Task.Factory.StartNew(() =>
{
    StartSyncTest(sSearchId, rCancellationToken);
},
rCancellationToken, 
TaskCreationOptions.LongRunning,
TaskScheduler.FromCurrentSynchronizationContext());

这个实现似乎解决了异步问题。现在将来对CancelAsyncTest 的调用成功并按预期取消任务。

【讨论】:

    猜你喜欢
    • 2023-03-15
    • 2011-09-03
    • 1970-01-01
    • 1970-01-01
    • 2014-04-25
    • 1970-01-01
    相关资源
    最近更新 更多