Console应用程序/Windows服务中使用Async/Await或Task.Run

7
我一直在研究(包括查看所有其他与此主题相关的SO帖子)最佳方法来实现一个(很可能是)Windows服务工作者,该工作者将从数据库中提取工作项,并以异步并行方式在后台“忘记即可”处理它们(工作项管理将在异步方法中处理)。 工作项将是Web服务调用和数据库查询。 生产这些工作项的生产者将应用一定的限流措施,以确保某种度量的工作安排方法。 以下示例非常基本,仅用于突出while循环和for循环的逻辑。哪种方法更理想或是否无关紧要? 是否有更合适/更高效的方法来实现这一点?
async / await ...
    private static int counter = 1;

    static void Main(string[] args)
    {
        Console.Title = "Async";

        Task.Run(() => AsyncMain());

        Console.ReadLine();            
    }

    private static async void AsyncMain()
    {
        while (true)
        {
            // Imagine calling a database to get some work items to do, in this case 5 dummy items
            for (int i = 0; i < 5; i++)
            {
                var x = DoSomethingAsync(counter.ToString());

                counter++;
                Thread.Sleep(50);
            }

            Thread.Sleep(1000);
        }
    }

    private static async Task<string> DoSomethingAsync(string jobNumber)
    {
        try
        {
            // Simulated mostly IO work - some could be long running
            await Task.Delay(5000);
            Console.WriteLine(jobNumber);
        }
        catch (Exception ex)
        {
            LogException(ex);
        }

        Log("job {0} has completed", jobNumber);

        return "fire and forget so not really interested";
    }

Task.Run...

    private static int counter = 1;

    static void Main(string[] args)
    {
        Console.Title = "Task";

        while (true)
        {
            // Imagine calling a database to get some work items to do, in this case 5 dummy items
            for (int i = 0; i < 5; i++)
            {
                var x = Task.Run(() => { DoSomethingAsync(counter.ToString()); });

                counter++;
                Thread.Sleep(50);
            }

            Thread.Sleep(1000);
        }
    }

    private static string DoSomethingAsync(string jobNumber)
    {
        try
        {
            // Simulated mostly IO work - some could be long running
            Task.Delay(5000);
            Console.WriteLine(jobNumber);
        }
        catch (Exception ex)
        {
            LogException(ex);
        }

        Log("job {0} has completed", jobNumber);

        return "fire and forget so not really interested";
    }

1
你的代码全部都是错误的。不要使用 async void;除非有异步操作,否则不要使用 async;不要在没有等待它们的情况下运行异步任务。 - SLaks
2
你需要学习异步编程的基础知识。请参阅 https://msdn.microsoft.com/zh-cn/magazine/jj991977.aspx。 - SLaks
使用并行LINQ。 - SLaks
那么你可能确实需要使用Task.Run,但是你的问题表述不够清楚。 - SLaks
1
@user2231663,我会坚持使用Task.Run方法。很容易使用SemaphoreSlim进行限流,就像这样,或者您甚至可以使用TPL Dataflow来实现更好的效果。 - noseratio - open to work
显示剩余5条评论
2个回答

7
从数据库中拉取工作项,并以异步并行的方式在后台“点火忘记”的方式处理它们。
技术上,您需要并发性。无论您想要异步并发还是并行并发都有待观察...
工作项将是Web服务调用和数据库查询。
工作是I/O绑定的,因此这意味着异步并发是更自然的方法。
生产者将应用一些限制来确保某种测量方法来安排工作。
这里暗示了生产者/消费者队列的概念。那是一个选择。TPL Dataflow提供了一些不错的生产者/消费者队列,这些队列是异步兼容的,并支持节流。
或者,您可以自己进行限制。对于异步代码,有一个内置的限制机制称为SemaphoreSlim。

TPL 数据流方法,带有限流:

private static int counter = 1;

static void Main(string[] args)
{
    Console.Title = "Async";
    var x = Task.Run(() => MainAsync());
    Console.ReadLine();          
}

private static async Task MainAsync()
{
  var blockOptions = new ExecutionDataflowBlockOptions
  {
    MaxDegreeOfParallelism = 7
  };
  var block = new ActionBlock<string>(DoSomethingAsync, blockOptions);
  while (true)
  {
    var dbData = await ...; // Imagine calling a database to get some work items to do, in this case 5 dummy items
    for (int i = 0; i < 5; i++)
    {
      block.Post(counter.ToString());
      counter++;
      Thread.Sleep(50);
    }
    Thread.Sleep(1000);
  }
}

private static async Task DoSomethingAsync(string jobNumber)
{
  try
  {
    // Simulated mostly IO work - some could be long running
    await Task.Delay(5000);
    Console.WriteLine(jobNumber);
  }
  catch (Exception ex)
  {
    LogException(ex);
  }
  Log("job {0} has completed", jobNumber);
}

手动限流的异步并发处理方法:

private static int counter = 1;
private static SemaphoreSlim semaphore = new SemaphoreSlim(7);

static void Main(string[] args)
{
    Console.Title = "Async";
    var x = Task.Run(() => MainAsync());
    Console.ReadLine();          
}

private static async Task MainAsync()
{
  while (true)
  {
    var dbData = await ...; // Imagine calling a database to get some work items to do, in this case 5 dummy items
    for (int i = 0; i < 5; i++)
    {
      var x = DoSomethingAsync(counter.ToString());
      counter++;
      Thread.Sleep(50);
    }
    Thread.Sleep(1000);
  }
}

private static async Task DoSomethingAsync(string jobNumber)
{
  await semaphore.WaitAsync();
  try
  {
    try
    {
      // Simulated mostly IO work - some could be long running
      await Task.Delay(5000);
      Console.WriteLine(jobNumber);
    }
    catch (Exception ex)
    {
      LogException(ex);
    }
    Log("job {0} has completed", jobNumber);
  }
  finally
  {
    semaphore.Release();
  }
}

作为最后的说明,我很少在SO上推荐我的书,但我确实认为它会对你有很大的帮助。特别是第8.10节(阻塞/异步队列)、第11.5节(限流)和第4.4节(数据流块限流)。

1
首先,让我们来修复一些问题。
在第二个示例中,您正在调用。
Task.Delay(5000);

没有使用await是不好的想法。它会创建一个新的Task实例,该实例运行5秒钟,但没有人在等待它。只有在使用awaitTask.Delay才有用。请注意,不要使用Task.Delay(5000).Wait(),否则会发生死锁。
在第二个示例中,您试图使DoSomethingAsync方法同步,我们称之为DoSomethingSync,并将Task.Delay(5000);替换为Thread.Sleep(5000); 现在,第二个示例几乎就是老式的ThreadPool.QueueUserWorkItem。如果您没有在内部使用某些已异步API,则没有任何问题。在fire-and-forget情况下使用Task.RunThreadPool.QueueUserWorkItem是完全相同的。我会使用后者以获得更清晰的代码。
这会慢慢引导我们回答主要问题。异步或非异步 - 这是个问题!我会说:“如果您的代码中没有使用异步IO,请不要创建异步方法。”但是,如果您必须使用异步API,则那些未来几年需要阅读您代码的人更可能采用第一种方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接