这是编写异步方法的正确方法吗

本文关键字:方法 异步方法 | 更新日期: 2023-09-27 18:27:28

我目前正在尝试编写异步代码,我觉得我的代码根本不太正确。

我有以下方法:

public void Commit()
{
    _context.SaveChangesToDatabase();
}

不要在这里判断代码,因为这只是示例。此外,不要说如果我使用的是实体框架,那么它们已经与异步方法打包在一起了。我只是想理解这里的异步概念。

假设SaveChangesToDatabase的方法需要几秒钟才能完成。现在,我不想等待它,所以我创建了一个异步方法:

public async Task CommitAsync()
{
    await Task.Run(() => Commit());
}

这是否意味着,如果我有一个方法:

public void Method()
{
    // Operation One:
    CommitAsync();
    // Operation Two.
}

这是否意味着我在操作二上的代码将在CommitAsync()完成之前执行?

如果没有,请指引我正确的方向。

更新

根据这里的注释,我忽略了异步方法的结果,这个实现更好吗?

public Task<TaskResult> CommitAsync()
{
    var task = new Task<TaskResult>(() =>
    {
        try { Commit(); }
        catch (Exception ex)
        {
            return new TaskResult
            {
                Result = TaskExceutionResult.Failed,
                Message = ex.Message
            };
        }
        return new TaskResult { Result = TaskExceutionResult.Succeeded };
    });
    task.Start();
    return task;
}

这意味着我需要在调用此代码的方法上加上async修饰符,这样我就可以等待它,这意味着继续当前执行,并在该方法完成后返回。

这是编写异步方法的正确方法吗

开火,但别忘了

CommitAsync()返回一个Task,但Method完全忽略了CommitAsync的返回值——所以是的,代码不会等待,而是继续执行之后的操作。这很糟糕,因为如果Commit()抛出异常,你永远不会看到它。理想情况下,每个任务都应该有人在某个地方等待,这样你至少可以看到它是否失败。

假设您没有SaveChangesToDatabase的异步替代方案,但无论如何您都希望在异步上下文中使用它。可以使用Task.Run创建"伪异步"方法,但不建议这样做(见下文):

public Task CommitAsync() {
    return Task.Run(() => Commit());
}

然后,假设Method正在用async做一些有趣的事情(下面的代码做而不是,因为它是其中唯一的异步操作):

public async Task MethodAsync() {
    // Operation One:
    await CommitAsync();
    // Operation Two.
}

假设你不想等待,但如果任务失败,你确实想做点什么,你可以使用一个单独的方法:

public void Method() {
    // Operation One:
    var _ = TryCommitAsync();
    // Operation Two.
}
private async Task TryCommitAsync()
{
    try
    {
        await CommitAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine(
            "Committing failed in the background: {0}", 
            ex.Message
        );
    }
}

正在获取结果

假设.Commit()确实返回了一些内容(比如受影响的记录数);类似的"伪异步"包装器(同样,不推荐使用-见下文)看起来是这样的:

public Task<int> CommitAsync() {
    return Task.Run(() => Commit());
}

如果你想要这个结果,你可以立即等待任务:

public async Task MethodAsync() {
    // Operation One:
    int recordsAffected = await CommitAsync();
    // Operation Two.
}

或者,如果您不需要立即使用,请在需要时使用await

public async Task MethodAsync() {
    // Operation One:
    Task<int> commit = CommitAsync();
    // Operation Two.
    // At this point I'd really like to know how many records were committed.
    int recordsAffected = await commit;
}

异步:不要伪造

一般来说,您不想编写像CommitAsync()这样的包装器,因为它们会误导调用方,使其认为代码将是异步的,而事实并非如此,这除了不阻塞之外几乎没有什么好处(这在UI代码中仍然很有用,但不如真正的异步代码好,因为它不需要对所有事情都使用工作线程)。换言之,您应该在方法的调用中使用Task.Run,而不是将其用作方法的实现。

因此,作为一种习惯,不要为您拥有的每个同步方法编写像CommitAsync这样的包装器——相反,您希望制作一个真正的CommitAsync,它使用底层库/框架(SqlCommand.ExecuteReaderAsync()等)的异步支持

如果您别无选择,必须使用Task.Run,那么适当的用法看起来更像:

// This method is in the UI layer.
public async Task MethodAsync() {
    // Operation One:
    // Commit() is a method in the DA layer.
    await Task.Run(() => Commit());
    // Operation Two.
}

此处为

http://channel9.msdn.com/events/TechEd/NorthAmerica/2013/DEV-B318#fbid=

很好地解释了如何使用异步,以及为什么应该避免"异步过度同步",这就是您现在使用所做的

public Task CommitAsync() {
    return Task.Run(() => Commit());
}

在某些情况下,你可以从中受益,但如果你要将其作为库的一部分提供,那么做这件事不是一个好主意。如果这个代码是ONLY并且ONLY将由你的应用程序使用,并且你确定你正在做什么,并且在你的异步方法中没有调用异步方法的方法,那么就这样做吧