C#等待条件为真

63

我正在尝试编写一段在满足条件时执行的代码。目前,我正在使用while...循环,但我知道这不是非常高效的方法。我还在看AutoResetEvent(),但我不知道如何实现它以使其在条件为真之前不断检查。

这段代码也恰好位于一个async方法中,因此可能需要一些等待的操作?

private async void btnOk_Click(object sender, EventArgs e)
{
        // Do some work
        Task<string> task = Task.Run(() => GreatBigMethod());
        string GreatBigMethod = await task;

        // Wait until condition is false
        while (!isExcelInteractive())
        {
            Console.WriteLine("Excel is busy");
        }

        // Do work
        Console.WriteLine("YAY");
 }


    private bool isExcelInteractive()
    {
        try
        {
            Globals.ThisWorkbook.Application.Interactive = Globals.ThisWorkbook.Application.Interactive;
            return true; // Excel is free
        }
        catch
        {
            return false; // Excel will throw an exception, meaning its busy
        }
    }

我需要找到一种方法来持续检查isExcelInteractive()而不使CPU陷入循环。

注意:Excel中没有在非编辑模式下引发事件处理程序的机制。


1
谁改变了isExcelInteractive的值? - Peyman
isExcelInteractive() 是一个检查 Excel 是否繁忙的方法。如果 Excel 不处于编辑模式,则返回 true。这是我需要不断检查直到为真的东西,而且没有事件处理程序。 - Keylee
我猜你这里不需要异步方法,你看了我的答案吗? - Peyman
这是一个Windows窗体应用程序吗?如果是,那么您可以在单击事件处理程序的开头禁用OK按钮,并在处理程序结束时再次启用它。这样,您的应用程序将保持响应。 - Tarik
8个回答

72

至少你可以将循环从忙等待改为缓慢轮询。 例如:

    while (!isExcelInteractive())
    {
        Console.WriteLine("Excel is busy");
        await Task.Delay(25);
    }

15
我考虑过这个。这是一种常见的做法吗?我在编程方面还很新。 - Keylee

49

今天写了这个,看起来还不错。你可以这样使用:

await TaskEx.WaitUntil(isExcelInteractive);

代码(包括反向操作)

public static class TaskEx
{
    /// <summary>
    /// Blocks while condition is true or timeout occurs.
    /// </summary>
    /// <param name="condition">The condition that will perpetuate the block.</param>
    /// <param name="frequency">The frequency at which the condition will be check, in milliseconds.</param>
    /// <param name="timeout">Timeout in milliseconds.</param>
    /// <exception cref="TimeoutException"></exception>
    /// <returns></returns>
    public static async Task WaitWhile(Func<bool> condition, int frequency = 25, int timeout = -1)
    {
        var waitTask = Task.Run(async () =>
        {
            while (condition()) await Task.Delay(frequency);
        });

        if(waitTask != await Task.WhenAny(waitTask, Task.Delay(timeout)))
            throw new TimeoutException();
    }

    /// <summary>
    /// Blocks until condition is true or timeout occurs.
    /// </summary>
    /// <param name="condition">The break condition.</param>
    /// <param name="frequency">The frequency at which the condition will be checked.</param>
    /// <param name="timeout">The timeout in milliseconds.</param>
    /// <returns></returns>
    public static async Task WaitUntil(Func<bool> condition, int frequency = 25, int timeout = -1)
    {
        var waitTask = Task.Run(async () =>
        {
            while (!condition()) await Task.Delay(frequency);
        });

        if (waitTask != await Task.WhenAny(waitTask, 
                Task.Delay(timeout))) 
            throw new TimeoutException();
    }
}

使用示例: https://dotnetfiddle.net/Vy8GbV


1
@Ustin我添加了一个指向fiddle的链接,其中包含WaitUntil(...)作为内联Func<bool>的示例用法以及相同签名的函数指针的另一个示例。对于WaitWhile(...)也是一样的。基本上使用任何不带参数并返回bool的函数。该函数将在指定的时间间隔内运行多次,直到它评估为true或超时为止。 - Sinaesthetic
1
以下是几个注意点:
  1. 应该使用 while( !condition() ) 进行否定判断吧?
  2. 为什么你在线程轮询时使用异步,而不是 Task.Run( () => { while (!condition()) { Thread.Sleep(pollDelay); } });
  3. 我建议添加一个取消令牌 while (!condition() && !ct.IsCancellationRequested)Task.Delay(timeout, ct);
  4. 我也会重命名这个方法为 WaitWhileAsyncWaitUntilAsync
  5. 最后,我认为你应该将是否使用新线程的选择委托给 Task.Run,并且也许可以使用非线程轮询的异步方法,例如 await PollConditionAsync()
- fibriZo raZiel
将创建新线程的选择委托给调用者,使用Task.Run。关于第一点不要担心,你是完全正确的,不需要在WaitWhile中否定条件。(抱歉,5分钟后我无法编辑我的评论...) - fibriZo raZiel
2
有没有一个NuGet包已经包含了这个功能? - minus one
1
如果“条件”从未为真,但已达到“超时”,那么任务是否会无限期地继续运行?该任务是否应该有一个取消令牌,在“超时”时被取消?如果是这样,它可能看起来像什么? - TheGrovesy
显示剩余2条评论

16
你可以使用线程等待处理程序
private readonly System.Threading.EventWaitHandle waitHandle = new System.Threading.AutoResetEvent(false);
private void btnOk_Click(object sender, EventArgs e)
{
    // Do some work
    Task<string> task = Task.Run(() => GreatBigMethod());
    string GreatBigMethod = await task;

    // Wait until condition is false
    waitHandle.WaitOne();
    Console.WriteLine("Excel is busy");
    waitHandle.Reset();

    // Do work
    Console.WriteLine("YAY");
 }

然后其他一些工作需要设置您的处理程序

void isExcelInteractive()
{
   /// Do your check
   waitHandle.Set()
}

更新: 如果您想使用此解决方案,您需要以特定间隔连续调用isExcelInteractive():

var actions = new []{isExcelInteractive, () => Thread.Sleep(25)};
foreach (var action in actions)
{                                      
    action();
}

这是你的答案吗?如果isExcelInteractive无法访问waitHandle,那么你需要在两个方法之间共享此对象。 - Peyman
我不确定这个答案是否可行。调用isExcelInteractive()的是什么?好像没有持续检查isExcelInteractive() - Keylee
所以你必须使用@Ben的解决方案,或者按特定间隔调用isExcelInteractive()方法。 - Peyman
是的没错。如果你只需要一个地方,可以使用他的解决方案,否则你可以使用我的(复杂)解决方案 :) - Peyman
感谢您的输入。我会将它放在我的工具口袋里 :) - Keylee
显示剩余3条评论

7

这个实现完全基于Sinaesthetic的,但增加了CancellationToken并保持相同的执行线程和上下文;也就是说,根据condition是否需要在同一线程中计算,将使用Task.Run()委托给调用者。

此外,注意,如果你不需要抛出TimeoutException而仅需中断循环,你可能想要使用cts.CancelAfter()new CancellationTokenSource(millisecondsDelay)来代替使用timeoutTaskTask.DelayTask.WhenAny

public static class AsyncUtils
{
    /// <summary>
    ///     Blocks while condition is true or task is canceled.
    /// </summary>
    /// <param name="ct">
    ///     Cancellation token.
    /// </param>
    /// <param name="condition">
    ///     The condition that will perpetuate the block.
    /// </param>
    /// <param name="pollDelay">
    ///     The delay at which the condition will be polled, in milliseconds.
    /// </param>
    /// <returns>
    ///     <see cref="Task" />.
    /// </returns>
    public static async Task WaitWhileAsync(CancellationToken ct, Func<bool> condition, int pollDelay = 25)
    {
        try
        {
            while (condition())
            {
                await Task.Delay(pollDelay, ct).ConfigureAwait(true);
            }
        }
        catch (TaskCanceledException)
        {
            // ignore: Task.Delay throws this exception when ct.IsCancellationRequested = true
            // In this case, we only want to stop polling and finish this async Task.
        }
    }

    /// <summary>
    ///     Blocks until condition is true or task is canceled.
    /// </summary>
    /// <param name="ct">
    ///     Cancellation token.
    /// </param>
    /// <param name="condition">
    ///     The condition that will perpetuate the block.
    /// </param>
    /// <param name="pollDelay">
    ///     The delay at which the condition will be polled, in milliseconds.
    /// </param>
    /// <returns>
    ///     <see cref="Task" />.
    /// </returns>
    public static async Task WaitUntilAsync(CancellationToken ct, Func<bool> condition, int pollDelay = 25)
    {
        try
        {
            while (!condition())
            {
                await Task.Delay(pollDelay, ct).ConfigureAwait(true);
            }
        }
        catch (TaskCanceledException)
        {
            // ignore: Task.Delay throws this exception when ct.IsCancellationRequested = true
            // In this case, we only want to stop polling and finish this async Task.
        }
    }

    /// <summary>
    ///     Blocks while condition is true or timeout occurs.
    /// </summary>
    /// <param name="ct">
    ///     The cancellation token.
    /// </param>
    /// <param name="condition">
    ///     The condition that will perpetuate the block.
    /// </param>
    /// <param name="pollDelay">
    ///     The delay at which the condition will be polled, in milliseconds.
    /// </param>
    /// <param name="timeout">
    ///     Timeout in milliseconds.
    /// </param>
    /// <exception cref="TimeoutException">
    ///     Thrown after timeout milliseconds
    /// </exception>
    /// <returns>
    ///     <see cref="Task" />.
    /// </returns>
    public static async Task WaitWhileAsync(CancellationToken ct, Func<bool> condition, int pollDelay, int timeout)
    {
        if (ct.IsCancellationRequested)
        {
            return;
        }

        using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct))
        {
            Task waitTask     = WaitWhileAsync(cts.Token, condition, pollDelay);
            Task timeoutTask  = Task.Delay(timeout, cts.Token);
            Task finishedTask = await Task.WhenAny(waitTask, timeoutTask).ConfigureAwait(true);

            if (!ct.IsCancellationRequested)
            {
                cts.Cancel();                            // Cancel unfinished task
                await finishedTask.ConfigureAwait(true); // Propagate exceptions
                if (finishedTask == timeoutTask)
                {
                    throw new TimeoutException();
                }
            }
        }
    }

    /// <summary>
    ///     Blocks until condition is true or timeout occurs.
    /// </summary>
    /// <param name="ct">
    ///     Cancellation token
    /// </param>
    /// <param name="condition">
    ///     The condition that will perpetuate the block.
    /// </param>
    /// <param name="pollDelay">
    ///     The delay at which the condition will be polled, in milliseconds.
    /// </param>
    /// <param name="timeout">
    ///     Timeout in milliseconds.
    /// </param>
    /// <exception cref="TimeoutException">
    ///     Thrown after timeout milliseconds
    /// </exception>
    /// <returns>
    ///     <see cref="Task" />.
    /// </returns>
    public static async Task WaitUntilAsync(CancellationToken ct, Func<bool> condition, int pollDelay, int timeout)
    {
        if (ct.IsCancellationRequested)
        {
            return;
        }

        using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct))
        {
            Task waitTask     = WaitUntilAsync(cts.Token, condition, pollDelay);
            Task timeoutTask  = Task.Delay(timeout, cts.Token);
            Task finishedTask = await Task.WhenAny(waitTask, timeoutTask).ConfigureAwait(true);

            if (!ct.IsCancellationRequested)
            {
                cts.Cancel();                            // Cancel unfinished task
                await finishedTask.ConfigureAwait(true); // Propagate exceptions
                if (finishedTask == timeoutTask)
                {
                    throw new TimeoutException();
                }
            }
        }
    }
}

4

试试这个

async void Function()
{
    while (condition) 
    {
        await Task.Delay(1);
    }
}

这将使程序等待条件不为真时才执行。 你可以在条件前加上 "!" 来取反它,这样它就会等待条件为真时才执行。

3

您可以使用内置于 .net-framework 的 SpinUntil 方法。请注意:此方法会导致高 CPU 负载。


虽然SpinUntil确实是while循环的直接替代品,但它仅仅是while循环的直接替代品。在等待期间,它会占用线程并使CPU保持繁忙状态。异步编程旨在避免这两个问题。稍微更为优雅的解决方案仍会阻塞线程,但在满足条件之前不会使用任何CPU周期。更加优雅的解决方案将释放线程直到满足条件。请参见本页其他答案中的示例。 - NSFW

0

经过大量的研究,我终于找到了一个不会挂掉CI的好解决方案 :) 根据您的需求进行调整!

public static Task WaitUntil<T>(T elem, Func<T, bool> predicate, int seconds = 10)
{
    var tcs = new TaskCompletionSource<int>();
    using(var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(seconds)))
    {
        cancellationTokenSource.Token.Register(() =>
        {
            tcs.SetException(
                new TimeoutException($"Waiting predicate {predicate} for {elem.GetType()} timed out!"));
            tcs.TrySetCanceled();
        });

        while(!cancellationTokenSource.IsCancellationRequested)
        {
            try
            {
                if (!predicate(elem))
                {
                    continue;
                }
            }
            catch(Exception e)
            {
                tcs.TrySetException(e);
            }

            tcs.SetResult(0);
            break;
        }

        return tcs.Task;
    }
}

似乎它从不抛出异常。 - ArieDov
@ArieDov,这取决于你想要什么。如果可以预见到“predicate”会抛出异常,那么是的,你需要捕获并将其设置为“tcs”结果。 - Oğuzhan Soykan
这将仍然使CPU保持繁忙,只是在原始线程空闲时在不同的线程中执行。这里获得了什么? - ygoe

-3
你可以使用异步结果和委托来实现这个功能。如果你阅读文档,应该很清楚该怎么做。如果需要,我可以编写一些示例代码并将其附加到此答案中。
Action isExcelInteractive = IsExcelInteractive;

private async void btnOk_Click(object sender, EventArgs e)
{
    IAsyncResult result = isExcelInteractive.BeginInvoke(ItIsDone, null);
    result.AsyncWaitHandle.WaitOne();
    Console.WriteLine("YAY");
} 

static void IsExcelInteractive(){
   while (something_is_false) // do your check here
   {
       if(something_is_true)
          return true;
   }
   Thread.Sleep(1);
}

void ItIsDone(IAsyncResult result)
{
   this.isExcelInteractive.EndInvoke(result);
}

如果这段代码不完整,可能是因为我没有在这台电脑上安装 Visual Studio。但希望它能够帮助你达到你想要的目标。


1
请这样做,这将帮助我更快地理解 :) - Keylee

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