每次只能使用async await执行一个任务

7
在我的面试中,我被要求创建一个异步包装器来处理一些长时间运行的方法,处理一些数据,但是要创建它,以便只能同时运行一个任务。我对async/await模式不太熟悉,所以我尽力写了一些混合了任务样式和事件样式的代码,因此我的包装器正在执行当前任务,并公开了一个公共方法和一个公共事件。该方法将要处理的数据作为参数传入,如果没有正在运行的任务,则启动一个任务,如果有任务,则将数据加入队列。任务完成后会触发公共事件,将处理结果发送给订阅者,并在有任何排队等待的情况下启动一个新任务。
所以,你可能已经猜到了,我在面试中失败了,但现在我做了一些研究,正在尝试弄清楚如何正确地完成它(它也应该是线程安全的,但我太忙于担心这个问题)。所以我的问题是,如果我有...
public class SynchronousProcessor
{
    public string Process(string arg)
    {
        Thread.Sleep(1500); //Work imitation
        return someRandomString;
    }
}

public class AsynchronousWrapper
{
    SynchronousProcessor proc = new SynchronousProcessor();

    public async Task<string> ProcessAsync(string arg)
    {
        return Task.Run(() => proc.Process(arg));
    } 
}

如果已经有一个任务正在执行,我应该如何正确处理对ProcessAsync(string)的调用?

1
@MickyD,确实需要一个包装器,因为我的任务是这样制定的:“编写一个包装器…”。 而且SynchronousProcessor类应该被视为库类,因此其代码保持不变。 - Dmitry Pavlushin
1
@MickyD,你怎么能说不需要AsyncWrapper呢?如果这个方法是从UI调用的,显然你不想阻塞它吧? - MistyK
@MickyD 这真的取决于情况。如果只是在新线程中执行工作负载,我会同意。然而,在这里,它是关于提供一个包装器,确保一次只执行一个调用。在其中放置 Task.Run 是完全可以的,因为它周围也会有一些非平凡的逻辑。 - Kevin Gosse
1
不要在常规方法调用中使用lock,而是使用带有异步调用的SemaphoreSlimlock无法包装异步调用,但SemaphoreSlim可以。https://msdn.microsoft.com/zh-cn/library/system.threading.semaphoreslim(v=vs.110).aspx,https://gist.github.com/tugberkugurlu/5917020 - ps2goat
@MickyD 感谢您的澄清。我几乎确定您是指这篇文章。尽管如Kevin所提到的,并非总是如此。 - MistyK
显示剩余7条评论
3个回答

4
许多面试问题问的目的不是为了看你编写代码,通常问题有点模糊,特别是为了看看你会问什么澄清问题 - 你的问题决定你的表现。在白板上编写代码最多是次要的。
给我一个任务创建异步包装器,处理一些数据
第一个问题:这个长时间运行的方法是异步的吗?如果是的话,则不需要使用Task.Run。但如果不是...
跟进问题:如果它不是异步的,是否应该是异步的?例如,它是否基于I/O?如果是这样,那么我们可以投入时间使其正常异步化。但如果不是...
跟进问题:如果我们需要一个任务包装器(围绕CPU代码或阻塞I/O代码),环境是否同意使用包装器?也就是说,这是一个桌面/移动应用程序,而不是将在ASP.NET中使用的代码?
创建它以便每次只能运行一个任务。
澄清问题:当一个请求已经在运行时,如果第二个请求进来,第二个请求会“排队”吗?还是会与现有请求“合并”?如果合并,它们是否需要“键入”输入数据 - 还是某些输入数据的子集?
每个问题都会改变答案结构。
公开方法和公开事件。
这可能是引起困惑的地方。在Task<T> / IProgress<T>和Rx之间,事件很少需要使用。如果你的团队不会学习Rx,那么它们确实只应该在必要时使用。
哦,不用担心“失败”的面试。在我职业生涯中我“失败”了超过三分之二的面试。我只是不善于面试。

1

这取决于你想要多么复杂。一个简单的方法是存储任务,并链接后续任务(稍微同步一下):

public class AsynchronousWrapper
{
    private Task previousTask = Task.CompletedTask;     
    private SynchronousProcessor proc = new SynchronousProcessor();

    public Task<string> ProcessAsync(string arg)
    {
        lock (proc)
        {
            var task = previousTask.ContinueWith(_ => proc.Process(arg));

            previousTask = task;

            return task;
        }           
    } 
}

有趣的是,与我的答案相比有什么不同呢? - MistyK
1
previousTask 不是静态变量,因此使用其他 AsynchronousWrapper 实例的其他线程将并行执行。任务链接意味着最后一个任务必须等待所有先前的任务完成,这可能会让消费者感到惊讶。而且它使用了 lock 阻塞线程,这实际上并不是异步的。 - VMAtm
@VMAtm 这个锁不是问题,因为它只是为了保护分配操作而存在的一个小开销。方法仍然是异步的,因为实际的工作负载将在锁之外运行。 - Kevin Gosse
1
@VMAtm “任务链”意味着最后一个任务必须等待所有之前的任务完成,这可能会让消费者感到惊讶。但我认为这正是这个类的目的所在,所以并不是问题。 - Kevin Gosse
1
@Zbigniew 主要的区别在于你在任务内部使用锁。因此,你会启动与消费者数量相同的线程,并且它们都会等待轮到自己。从技术上讲,你满足了 OP 的要求,但效率非常低下。 - Kevin Gosse
显示剩余2条评论

0

正如 @MickyD 已经说过的,你需要了解异步编程最佳实践才能正确地解决这些问题。你的解决方案存在代码异味,因为它提供了一个使用 Task.Run 的异步包装器来处理同步代码。由于你被要求开发库,这将对你的库消费者产生相当大的影响。

你必须明白异步并不等同于多线程,因为它可以用一个线程完成。就像等待邮件一样——你不会雇佣一个工人在邮箱旁等待

这里的其他解决方案都不是真正的异步,因为它们违反了异步代码的另一个规则:不要阻塞异步操作,所以你应该避免使用lock结构。

所以,回到你的问题:如果你面临一个任务

一次只能运行一个任务

这与锁(Monitor)无关,而是与Semaphore(Slim)有关。如果将来您需要改进代码以便同时执行多个任务,则必须重写代码。在使用Semaphore的情况下,您只需要更改一个常量即可。此外,它还具有等待方法的async包装器。

因此,您的代码可以像这样(请注意,删除了Task.Run,因为客户端负责提供可等待对象):

public class AsynchronousWrapper
{
    private static SemaphoreSlim _mutex = new SemaphoreSlim(1);

    public async Task<T> ProcessAsync<T>(Task<T> arg)
    {
        await _mutex.WaitAsync().ConfigureAwait(false);

        try
        {
            return await arg;
        }
        finally
        {
            _mutex.Release();
        }
    }
}

1
谢谢。我以为自己醒来时进入了一个“到处都可以使用Task.Run()”的宇宙。;) +1 - user585968
1
@MickyD 我知道那种感觉 :) - VMAtm
1
另外(好吧,这次我有点讽刺),你知道 SemaphoreSlim.WaitAsync 使用锁来处理竞态条件,对吧?所以,关于你的评论,你的方法不是异步的。有趣的是,明确使用某些东西会让人们感到困扰,但他们没有意识到实际上内部是这样做的。https://referencesource.microsoft.com/#mscorlib/system/threading/SemaphoreSlim.cs,4772c1d9756a4a9a - Kevin Gosse
1
哦,我忘了。"_mutex不是静态变量,因此使用其他AsynchronousWrapper实例的其他线程将并行执行"。所以基本上你对我的答案进行了3个评论,然后在你的答案中没有应用任何一个。好吧。 - Kevin Gosse
2
@KevinGosse 对 Stephen Cleary 的文章印象深刻。好的,那么差异并不是很大。 - VMAtm
显示剩余7条评论

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