最佳异步 while 方法

24

我需要编写一些异步代码,它会尝试多次连接并初始化数据库。由于第一次尝试经常会失败,因此需要重试机制。

以前,我会使用类似于以下模式的方式:

void WaitForItToWork()
{
    bool succeeded = false;
    while (!succeeded)
    {
        // do work
        succeeded = outcome; // if it worked, mark as succeeded, else retry
        Threading.Thread.Sleep(1000); // arbitrary sleep
    }
}

我意识到近期在.NET方面进行了许多关于异步模式的更改,所以我的问题是:这是最好的方法吗?还是值得探索async的东西?如果是的话,我该如何在async中实现这种模式?

更新

只是为了澄清,我想要异步地生成这个工作,这样生成它的方法就不必等待它完成,因为它将在一个服务的构造函数中生成,所以构造函数必须立即返回。

4个回答

37
你可以像这样重构该代码片段:
async Task<bool> WaitForItToWork()
{
    bool succeeded = false;
    while (!succeeded)
    {
        // do work
        succeeded = outcome; // if it worked, make as succeeded, else retry
        await Task.Delay(1000); // arbitrary delay
    }
    return succeeded;
}

显然,它唯一的好处是更有效地利用线程池,因为并不总是需要一个完整的线程来完成延迟。

根据您获取outcome的方式,可能有更有效的方法使用async/await完成此任务。通常,您可能会像这样拥有GetOutcomeAsync(),它会以自然的方式异步地调用Web服务、数据库或套接字,因此您只需执行var outcome = await GetOutcomeAsync()

重要的是要考虑到编译器将通过await行将WaitForItToWork分成几部分,并继续异步进行的事实。 这里也许是关于如何在内部完成的最好的解释。问题在于,通常在代码的某个点上,您需要对异步任务的结果进行同步。例如:

private void Form1_Load(object sender, EventArgs e)
{
    Task<bool> task = WaitForItToWork();
    task.ContinueWith(_ => {
        MessageBox.Show("WaitForItToWork done:" + task.Result.toString()); // true or false
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

你本可以简单地这样做:
private async void Form1_Load(object sender, EventArgs e)
{
    bool result = await WaitForItToWork();
    MessageBox.Show("WaitForItToWork done:" + result.toString()); // true or false
}

然而,这将使Form1_Load也成为异步方法。

[更新]

以下是我尝试说明在这种情况下async/await实际上做了什么的内容。我创建了两个相同逻辑的版本,WaitForItToWorkAsync(使用async/await)和WaitForItToWorkAsyncTap(使用TAP模式但不使用async/await)。第一个版本相当简单,而第二个版本则不同。因此,虽然async/await在很大程度上是编译器的语法糖,但它使异步代码更容易编写和理解。

// fake outcome() method for testing
bool outcome() { return new Random().Next(0, 99) > 50; }

// with async/await
async Task<bool> WaitForItToWorkAsync()
{
    var succeeded = false;
    while (!succeeded)
    {
        succeeded = outcome(); // if it worked, make as succeeded, else retry
        await Task.Delay(1000);
    }
    return succeeded;
}

// without async/await
Task<bool> WaitForItToWorkAsyncTap()
{
    var context = TaskScheduler.FromCurrentSynchronizationContext();
    var tcs = new TaskCompletionSource<bool>();
    var succeeded = false;
    Action closure = null;

    closure = delegate
    {
        succeeded = outcome(); // if it worked, make as succeeded, else retry
        Task.Delay(1000).ContinueWith(delegate
        {
            if (succeeded)
                tcs.SetResult(succeeded);
            else
                closure();
        }, context);
    };

    // start the task logic synchronously
    // it could end synchronously too! (e.g, if we used 'Task.Delay(0)')
    closure();

    return tcs.Task;
}

// start both tasks and handle the completion of each asynchronously
private void StartWaitForItToWork()
{
    WaitForItToWorkAsync().ContinueWith((t) =>
    {
        MessageBox.Show("WaitForItToWorkAsync complete: " + t.Result.ToString());
    }, TaskScheduler.FromCurrentSynchronizationContext());

    WaitForItToWorkAsyncTap().ContinueWith((t) =>
    {
        MessageBox.Show("WaitForItToWorkAsyncTap complete: " + t.Result.ToString());
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

// await for each tasks (StartWaitForItToWorkAsync itself is async)
private async Task StartWaitForItToWorkAsync()
{
    bool result = await WaitForItToWorkAsync();
    MessageBox.Show("WaitForItToWorkAsync complete: " + result.ToString());

    result = await WaitForItToWorkAsyncTap();
    MessageBox.Show("WaitForItToWorkAsyncTap complete: " + result.ToString());
}

关于线程的几句话。这里没有显式地创建任何额外的线程。在内部,Task.Delay() 的实现可能会使用线程池(我怀疑它们使用 计时器队列),但在这个特定的示例中(一个 WinForms 应用程序),await 之后的继续操作将在同一 UI 线程上发生。在其他执行环境中(例如控制台应用程序),它可能会继续在不同的线程上。在我看来,Stephen Cleary 的 这篇文章 是了解 async/await 线程概念的必读之作。


我只需要通过 this.WaitForItToWork(); 来调用它 - 异步库会为我处理线程吗? - Chris
1
你可以这样调用它:await this.WaitForItToWork(),整个调用链必须重构以支持此操作... 我会详细说明我的答案并提供更多信息。 - noseratio - open to work
@Chris:你必须记得使用 "await" 关键字。经验法则是:始终将 "await" 与 "async" 函数配对使用。因此,你应该这样做 await WaitForItToWork(); - now he who must not be named.
我不想等待它,我想要启动这个任务并继续我正在做的事情。 - Chris
我刚刚更新了我的答案,希望现在更有意义了。实际上你并不需要等待(没有像阻塞Sleep这样的东西)。对于await Task.Delay()最好的类比可能是一个计时器事件。async/await只是编译器提供的一种语法糖。从某种意义上说,它类似于node.js,如果你熟悉它的话(尽管Node的javascript缺乏语法方便性 - 一切都通过闭包 - 在C#中使用委托)。 - noseratio - open to work

2

请提供另一种解决方案。

public static void WaitForCondition(Func<bool> predict)
    {
        Task.Delay(TimeSpan.FromMilliseconds(1000)).ContinueWith(_ =>
        {
            var result = predict();
            // the condition result is false, and we need to wait again.
            if (result == false)
            {
                WaitForCondition(predict);
            }
        });
    }

2

如果任务是异步的,你可以尝试以下方法:

    async Task WaitForItToWork()
    {
        await Task.Run(() =>
        {
            bool succeeded = false;
            while (!succeeded)
            {
                // do work
                succeeded = outcome; // if it worked, make as succeeded, else retry
                System.Threading.Thread.Sleep(1000); // arbitrary sleep
            }
        });
    }

请参阅http://msdn.microsoft.com/en-us/library/hh195051.aspx

1
这正是我在回答中想要避免的。也许,我自己漏掉了什么:] - noseratio - open to work
我说错了。放松点。即使事后进行更正或澄清也没有任何伤害。我进行了编辑以避免其他读者的困惑。我不是为了让你看起来愚蠢而进行编辑。那将违反DRY原则。/sickburn - Gusdor
@Gusdor,明白了。我已经删除了我的评论,因为在这种情况下没有用处。 - noseratio - open to work
1
也许在这种情况下,最好的解决方案是使用一个计时器,在每个间隔尝试连接,当连接成功后禁用它。 - Alessandro D'Andria
3
这是一个已知的反模式(http://blogs.msdn.com/b/pfxteam/archive/2012/04/13/10293638.aspx)。我建议接受Noseratio的答案,因为我不希望未来的访问者将其视为最佳解决方案。 - usr
显示剩余3条评论

0

你其实不需要使用WaitItForWork方法,只需等待数据库初始化任务完成:

async Task Run()
{
    await InitializeDatabase();
    // Do what you need after database is initialized
}

async Task InitializeDatabase()
{
    // Perform database initialization here
}

如果您有多个调用WaitForItToWork的代码片段,则需要将数据库初始化包装到一个Task中,并在所有工作程序中等待它,例如:
readonly Task _initializeDatabaseTask = InitializeDatabase();

async Task Worker1()
{
    await _initializeDatabaseTask;
    // Do what you need after database is initialized
}

async Task Worker2()
{
    await _initializeDatabaseTask;
    // Do what you need after database is initialized
}

static async Task InitializeDatabase()
{
    // Initialize your database here
}

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