为什么在应该异步的情况下阻止此ASP.NET方法?

时间:2018-11-17 19:13:47

标签: c# asp.net

在WCF服务中提供以下方法签名:

public string Query(string request)
{
    using (Log.BeginTimedOperation("Persist request"))
    {
        var messageCorrelationId = Guid.NewGuid().ToString();

        var payloadURI = PayloadHelper.GetFullPath(messageCorrelationId);

        PayloadHelper.PersistPayloadWithPath(request, payloadURI);

        Log.Information("My service request {MessageCorrelationId} {RequestPayloadPath}", messageCorrelationId, payloadURI);
    }

    // DoWork here, code removed for brevity

    return result;
}

以及相应的扩展方法:

public static string GetFullPath(string messageCorrelationId)
{            
    var folderDate = DateTime.Now.ToString("yyyyMMdd");
    var folderHour = DateTime.Now.ToString("HH");
    var logFolder = Path.Combine(ConfigurationManager.AppSettings["NetworkFiler"], "Payloads", folderDate, folderHour);
    Directory.CreateDirectory(logFolder);
    var fileName = $"{messageCorrelationId}-{"MyWCFService"}-{$"{DateTime.Now:yyyy-MM-dd-HH-mm-ss-fff}-{Guid.NewGuid():N}"}.{"xml"}";
    return Path.Combine(logFolder, fileName);
}

public static void PersistPayloadWithPath(string text, string path)
{
    var task = WriteFileAsync(path, text);
    task.GetAwaiter().GetResult();
}

private static async Task WriteFileAsync(string path, string text)
{
    try
    {
        byte[] buffer = Encoding.Unicode.GetBytes(text);
        using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
        {
            await stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
        }
    }
    catch (Exception ex)
    {
        Serilog.Log.Error(ex, "WriteFileAsync");
    }
}

例如,如果文件正受到反病毒(猜测)或文件管理器IO速度慢的询问,则此代码将被阻止。

因此,这是一个很大的争论,即从ASP.NET中的同步方法中调用异步方法。直到今天,我仍然不知道是否存在一种可靠的方法来制造起火和遗忘机制。不是我不在乎失败,而是在catch语句和静态Serilog实例中处理“应该”。

在撰写此版本的帖子时,我突然意识到,问题之一可能实际上是记录程序,并且它是围绕文件接收器的异步包装程序。将在少数情况下进行测试。

此虫虫有任何帮助。

谢谢你, 斯蒂芬

UPDATE-ASYNC

public async Task<string> QueryAsync(string request)
{
    using (Log.BeginTimedOperation("PersistAsync-Request"))
    {
        var messageCorrelationId = Guid.NewGuid().ToString();

        var payloadURI = PayloadHelper.GetFullPath(messageCorrelationId);

        await PayloadHelper.PersistPayloadWithPathAsync(request, payloadURI).ConfigureAwait(false);

        Log.Information("My service request {MessageCorrelationId} {RequestPayloadPath}", messageCorrelationId, payloadURI);
    }

    // DoWork here, code removed for brevity

    return result;
}

public static string GetFullPath(string messageCorrelationId)
{            
    var folderDate = DateTime.Now.ToString("yyyyMMdd");
    var folderHour = DateTime.Now.ToString("HH");
    var logFolder = Path.Combine(ConfigurationManager.AppSettings["NetworkFiler"], "Payloads", folderDate, folderHour);
    Directory.CreateDirectory(logFolder);
    var fileName = $"{messageCorrelationId}-MyWCFService-{DateTime.Now:yyyy-MM-dd-HH-mm-ss-fff}-{Guid.NewGuid():N}.xml";
    return Path.Combine(logFolder, fileName);
}

public static async Task PersistPayloadWithPathAsync(string text, string path)
{
    await WriteFileAsync(path, text).ConfigureAwait(false);
}

private static async Task WriteFileAsync(string path, string text)
{
    try
    {
        byte[] buffer = Encoding.Unicode.GetBytes(text);
        using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
        {
            await stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
        }
    }
    catch
    {
        // ignored
    }
}

仍然随机阻止,每20-30个请求点击一次

4 个答案:

答案 0 :(得分:2)

因此它在new FileStream()上受阻。 Looking at the source code,构造函数调用一个名为Init()的方法,该方法实际上最终打开了文件。因此,它正在构造函数中执行I / O,实际上不应该这样做,因为您不能await

设置useAsync 应该使它异步运行,如果可以。但是也许它不能从网络驱动器异步打开文件。

所以最好的选择是在Task.Run()内部运行它,以确保它不会被阻塞。

(不幸的是)以下内容仅适用于.NET Core:

使用File.WriteAllTextAsync会更好,这实际上会使您的生活更轻松:

await File.WriteAllTextAsync(path, text);

由于某些原因,它的文档并没有真正解释任何内容,但其工作原理与File.WriteAllText相同(只是异步的):

  

创建一个新文件,将内容写入该文件,然后关闭该文件。如果目标文件已经存在,它将被覆盖。

因此,正是您在代码中所做的事情,但是通过这种方式,您可以等待整个操作(包括打开文件)。

答案 1 :(得分:1)

问题是您的WriteFileAsync方法。它实际上是同步运行的,直到到达第一个await为止(这就是异步/等待的工作方式)。我相信它会挂在new FileStream(...)上。

如果您只想结束表单同步代码,那么就足够了:

    public static void PersistPayloadWithPath(string text, string path)
    {
        Task.Run(async () => await WriteFileAsync(path, text));
    }

当您没有异步替代方法时,以上代码将为您提供帮助。但是,正如加布里埃尔·露西(Gabriel Luci)在他的回答中建议的那样,您应该选择await File.WriteAllTextAsync(path, text);,因为它可能已针对异步工作进行了优化。

在即发即弃方案中,您仍然可以将Task.Run(...);await File.WriteAllTextAsync(path, text);一起使用。

请注意,任务(WriteFileAsync)内部的异常不会传播到调用线程。在您的情况下,这将不是问题,因为整个WriteFileAsync方法的主体位于try-catch块中,您可以在其中记录异常。

编辑

要说明线程在异步/等待方法中的行为,请尝试以下示例(尝试运行Bar函数的所有3种方式):

using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    class Program
    {

        static void Main(string[] args)
        {
            Console.WriteLine($"Main thread: {Thread.CurrentThread.ManagedThreadId}");

            Task.Run(async () => await Bar());
//            Task.Run(() => Bar());
//            Bar();

            Console.ReadLine();
        }

        static async Task Bar()
        {
            Console.WriteLine($"Bar thread before await: {Thread.CurrentThread.ManagedThreadId}");
            await Foo();
            Console.WriteLine($"Bar thread after await: {Thread.CurrentThread.ManagedThreadId}");
        }

        static async Task Foo()
        {
            Console.WriteLine($"Foo thread before await: {Thread.CurrentThread.ManagedThreadId}");

            var c = new WebClient();
            var source = await c.DownloadStringTaskAsync("http://google.com");

            Console.WriteLine($"Foo thread after await: {Thread.CurrentThread.ManagedThreadId}");
        }
    }
}

答案 2 :(得分:1)

我认为这是等待中,因为您使用

task.GetAwaiter().GetResult();

如果不需要结果,只需删除此行。它应该工作正常。

如果需要结果,则应使PersistPayloadWithPath函数也异步。

答案 3 :(得分:0)

确定导致的根是配置错误的StealthAudit,这是我们用来审核文件管理器的工具。调整配置后,一切都会按预期进行。