异步 Web 服务调用不一致异步

Async Web Service call not consistently asynchronous

我在使用带有 ASP.NET Web API 的任务并行库创建异步 Web 服务时遇到问题 2. 我对方法 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 store the token globally and then retrieve it and call it from a second method CancelAsyncTest.

这可能不是个好主意。您可以将这些令牌 "globally" 存储到单个服务器,但那只是 "global"。一旦第二台服务器进入画面,这种方法就会失效。

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

的值的相当大的混淆

总结一下:

  • 我建议完全放弃这种方法;它在网络农场上根本不起作用。相反,将 "task state" 保存在外部存储系统中,例如数据库。
  • 不要跨线程传递 HttpContext

一位同事提供了对 Task.Factory.StartNew 的替代调用(在 StartAsyncTest 内):

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

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