基于异步/等待的Windows服务中的同步I / O

时间:2016-05-03 09:28:11

标签: c# .net asynchronous async-await

假设我有一个Windows服务,它正在做一些工作,然后在很长一段时间内一直睡觉,一遍又一遍(直到服务关闭)。所以在服务的OnStart中,我可以启动一个线程,其入口点类似于:

private void WorkerThreadFunc()
{
    while (!shuttingDown)
    {
        DoSomething();
        Thread.Sleep(10);
    }
}

在服务的OnStop中,我以某种方式设置了shuttingDown标志然后加入线程。实际上可能有几个这样的线程,以及其他线程,都在OnStart中启动并关闭/加入OnStop。

如果我想在基于异步/等待的Windows服务中执行此类操作,似乎我可以让OnStart创建可取消的任务但不等待(或等待)它们,并让OnStop取消这些任务然后Task.WhenAll()。等等()。如果我理解正确,上面显示的“WorkerThreadFunc”的等价物可能是:

private async Task WorkAsync(CancellationToken cancel)
{
    while (true)
    {
        cancel.ThrowIfCancellationRequested();
        DoSomething();
        await Task.Delay(10, cancel).ConfigureAwait(false);
    }
}
问题#1:呃......对吗?我是异步/等待的新手,仍然试图让我的头脑。

假设这是正确的,现在让我们说DoSomething()调用是(或包括)某些硬件的同步写入I / O.如果我理解正确:

问题2:那不好?我不应该在基于async / await的程序中的Task中进行同步I / O操作?因为它在I / O发生时从线程池中占用一个线程,而线程池中的线程是一个非常有限的资源?请注意,我可能会有数十名此类工作人员同时使用不同的硬件。

我不确定我是否理解正确 - 我认为这与Stephen Cleary的“Task.Run Etiquette Examples: Don't Use Task.Run for the Wrong Thing”这样的文章有关,但具体而言,在Task.Run中阻止工作是不好的。我不确定如果我只是直接这样做也不好,就像上面的“私人异步任务工作()”示例一样?

假设这也很糟糕,那么如果我理解正确,我应该使用DoSomething的非阻塞版本(如果它尚不存在则创建非阻塞版本),然后:

private async Task WorkAsync(CancellationToken cancel)
{
    while (true)
    {
        cancel.ThrowIfCancellationRequested();
        await DoSomethingAsync(cancel).ConfigureAwait(false);
        await Task.Delay(10, cancel).ConfigureAwait(false);
    }
}

问题#3:但是......如果DoSomething来自第三方库,我必须使用并且不能改变,并且该库不会暴露DoSomething的非阻塞版本会怎么样?它只是一个黑色的盒子,在某些时候阻止写入一块硬件。

也许我把它包起来并使用TaskCompletionSource?类似的东西:

private async Task WorkAsync(CancellationToken cancel)
{
    while (true)
    {
        cancel.ThrowIfCancellationRequested();
        await WrappedDoSomething().ConfigureAwait(false);
        await Task.Delay(10, cancel).ConfigureAwait(false);
    }
}

private Task WrappedDoSomething()
{
    var tcs = new TaskCompletionSource<object>();
    DoSomething();
    tcs.SetResult(null);
    return tcs.Task;
}

但这似乎只是将问题进一步推进而不是解决问题。 WorkAsync()在调用WrappedDoSomething()时仍然会阻塞,并且在WrappedDoSomething()已经完成阻塞工作后才会到达“await”。正确?

鉴于(如果我理解正确)在一般情况下,async / await应该被允许在程序中一直“扩散”,这是否意味着如果我需要使用这样的库,我本质上不应该使程序异步/等待?我应该回到Thread / WorkerThreadFunc / Thread.Sleep世界?

如果基于异步/等待的程序已经存在,做其他事情怎么办,但是现在需要添加使用这种库的其他功能呢?这是否意味着应该将基于异步/等待的程序重写为基于线程/等等的程序?

3 个答案:

答案 0 :(得分:11)

  

实际上可能有几个这样的线程,以及其他线程,都是在OnStart中启动并在OnStop中关闭/加入。

另一方面,拥有一个&#34;主人&#34;通常更简单。将启动/加入所有其他人的线程。然后OnStart / OnStop只处理主线程。

  

如果我想在基于异步/等待的Windows服务中执行此类操作,似乎我可以让OnStart创建可取消的任务但不等待(或等待)它们,并让OnStop取消这些任务然后Task.WhenAll()。Wait()on。

这是一种完全可以接受的方法。

  

如果我理解正确,相当于&#34; WorkerThreadFunc&#34;如上所示可能是这样的:

可能想要传递CancellationToken;取消也可以由同步代码使用:

private async Task WorkAsync(CancellationToken cancel)
{
  while (true)
  {
    DoSomething(cancel);
    await Task.Delay(10, cancel).ConfigureAwait(false);
  }
}
  问题#1:呃......对吗?我是异步/等待的新手,仍然试图让我的头脑。

它不是错误的,但它只会在Win32服务上为您节省一个帖子,这对您没有多大帮助。

  问题2:那不好?我不应该在基于async / await的程序中的Task中进行同步I / O操作吗?因为它在I / O发生时从线程池中占用一个线程,而线程池中的线程是一个非常有限的资源?请注意,我可能会有数十名此类工作人员同时使用不同的硬件。

数十个主题并不多。通常,异步I / O更好,因为它根本不使用任何线程,但在这种情况下,您在桌面上,因此线程不是非常有限的资源。 async对UI应用程序(UI线程特殊且需要释放)以及需要扩展的ASP.NET应用程序(线程池限制可伸缩性)最有益。

底线:从异步方法调用阻塞方法不是错误,但它也不是最佳。如果有异步方法,请调用它。但是如果没有,那么只需保留阻塞调用并将其记录在该方法的XML注释中(因为异步方法阻塞是相当令人惊讶的行为)。

  

我认为斯蒂芬克莱里的文章不好,以及#34;任务。运行礼仪示例:不要使用任务。运行错误的事情&#34;&#34;但是,具体而言,在Task.Run中阻止工作是不好的。

是的,具体是关于使用Task.Run来包装同步方法并假装它们是异步的。这是一个常见的错误;它所做的只是将一个线程池线程换成另一个线程。

  

假设它也很糟糕,那么如果我理解正确,我应该使用DoSomething的非阻塞版本(如果它还不存在则创建非阻塞版本)

异步更好(在资源利用率方面 - 也就是说,使用的线程更少),因此如果你想/需要减少线程数,你应该使用异步。

  

问题#3:但是......如果DoSomething来自第三方库,我必须使用并且不能改变,并且该库没有公开DoSomething的非阻塞版本会怎么样?它只是一个黑色的盒子,在某些时候阻止写入一块硬件。

然后直接调用它。

  

也许我把它包起来并使用TaskCompletionSource?

不,这并没有做任何有用的事情。它只是同步调用它然后返回一个已经完成的任务。

  

但这似乎只是将问题进一步推进而不是解决问题。 WorkAsync()在调用WrappedDoSomething()时仍会阻塞,只能到达&#34; await&#34;为此,WrappedDoSomething()已经完成了阻止工作。正确?

烨。

  

鉴于(如果我理解正确)在一般情况下,async / await应该被允许&#34;传播&#34;在程序中一直上下,这是否意味着如果我需要使用这样的库,我基本上不应该使程序async / await-based?我应该回到Thread / WorkerThreadFunc / Thread.Sleep世界吗?

假设您已经拥有阻止的Win32服务,那么保持原样可能很好。如果你正在写一个新的,我个人会让它async来减少线程并允许异步API,但你不能 做任何一种方式。我更喜欢Task s而不是Thread s,因为从Task获得结果(包括例外)要容易得多。

&#34; async一路&#34;规则只有一种方式。也就是说,一旦您调用async方法,其调用者应为async,其调用者应为async等。 意味着async方法调用的每个方法必须为async

因此,拥有异步Win32服务的一个很好的理由是,如果您需要使用仅支持异步的API。这会导致您的DoSomething方法变为async DoSomethingAsync

  

如果基于异步/等待的程序已经存在,做其他事情怎么办,但是现在需要添加使用这种库的其他功能呢?这是否意味着应该将基于异步/等待的程序重写为基于线程/等等的程序?

没有。您始终可以阻止async方法。有了适当的文档,所以当你在一年后重复/维护这段代码时,你不会发誓过去的自我。 :)

答案 1 :(得分:1)

如果你仍然产生你的线程,那么,是的,它很糟糕。因为它仍然没有给你任何好处,因为仍然为运行你的worker函数的特定目的分配了线程并消耗了资源。运行几个线程以便能够在服务中并行工作对应用程序的影响最小。

如果DoSomething()是同步的,您可以切换到Timer类。它允许多个计时器使用较少量的线程。

如果作业可以完成很重要,您可以像这样修改您的工人类:

SemaphoreSlim _shutdownEvent = new SemaphoreSlim(0,1);

public async Task Stop()
{
    return await _shutdownEvent.WaitAsync();
}

private void WorkerThreadFunc()
{
    while (!shuttingDown)
    {
        DoSomething();
        Thread.Sleep(10);
    }
    _shutdownEvent.Release();
}

..这意味着在关机期间你可以这样做:

var tasks = myServices.Select(x=> x.Stop());
Task.WaitAll(tasks);

答案 2 :(得分:1)

线程一次只能做一件事。虽然它正在处理你的DoSomething,但它无法做任何事情。

Eric Lippert在采访中描述async-await in a restaurant metaphor。他建议使用async-await仅用于线程可以执行其他操作而不是等待进程完成的功能,例如响应操作员输入。

唉,你的线程没有等待,它在DoSomething中正在努力工作。只要DoSomething没有等待,你的线程就不会从DoSomething返回来做下一件事。

因此,如果您的线程在执行DoSomething过程时有一些有意义的事情,那么让另一个线程执行DoSomething是明智的,而您的原始线程正在执行有意义的操作。 Task.Run(()=&gt; DoSomething())可以为您完成此任务。只要调用Task.Run的线程没有等待此任务,就可以自由地执行其他操作。

您还想取消您的流程。 DoSomething无法取消。因此,即使要求取消,您也必须等到DoSomething完成。

下面是带有“开始”按钮和“取消”按钮的表单中的DoSomething。当你的线程是DoingSomething时,你的GUI线程可能想要做的一件有意义的事情是响应按下取消按钮:

void CancellableDoSomething(CancellationToken token)
{
    while (!token.IsCancellationRequested)
    {
        DoSomething()
    }
}

async Task DoSomethingAsync(CancellationToken token)
{
    var task = Task.Run(CancellableDoSomething(token), token);
    // if you have something meaningful to do, do it now, otherwise:
    return Task;
}

CancellationTokenSource cancellationTokenSource = null;

private async void OnButtonStartSomething_Clicked(object sender, ...)
{
    if (cancellationTokenSource != null)
        // already doing something
        return

    // else: not doing something: start doing something
    cancellationTokenSource = new CancellationtokenSource()
    var task = AwaitDoSomethingAsync(cancellationTokenSource.Token);
    // if you have something meaningful to do, do it now, otherwise:
    await task;
    cancellationTokenSource.Dispose();
    cancellationTokenSource = null;
}

private void OnButtonCancelSomething(object sender, ...)
{
    if (cancellationTokenSource == null)
        // not doing something, nothing to cancel
        return;

    // else: cancel doing something
    cancellationTokenSource.Cancel();
}