【问题标题】:Creating a Task with a heartbeat使用心跳创建任务
【发布时间】:2016-08-02 13:21:31
【问题描述】:

我想运行一个具有“heartbeat”的Task,该heartbeat会在特定的时间间隔内持续运行,直到任务完成。

我认为这样的扩展方法会很好用:

public static async Task WithHeartbeat(this Task primaryTask, TimeSpan heartbeatInterval, Action<CancellationToken> heartbeatAction, CancellationToken cancellationToken)

例如:

public class Program {
    public static void Main() {
        var cancelTokenSource = new CancellationTokenSource();
        var cancelToken = cancelTokenSource.Token;
        var longRunningTask = Task.Factory.StartNew(SomeLongRunningTask, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        var withHeartbeatTask = longRunningTask.WithHeartbeat(TimeSpan.FromSeconds(1), PerformHeartbeat, cancelToken);
        withHeartbeatTask.Wait();
        Console.WriteLine("Long running task completed!");
        Console.ReadLine()
    }

    private static void SomeLongRunningTask() {
        Console.WriteLine("Starting long task");
        Thread.Sleep(TimeSpan.FromSeconds(9.5));
    }
    private static int _heartbeatCount = 0;
    private static void PerformHeartbeat(CancellationToken cancellationToken) {
        Console.WriteLine("Heartbeat {0}", ++_heartbeatCount);
    }
}

这个程序应该输出:

Starting long task
Heartbeat 1
Heartbeat 2
Heartbeat 3
Heartbeat 4
Heartbeat 5
Heartbeat 6
Heartbeat 7
Heartbeat 8
Heartbeat 9
Long running task completed!

请注意,它不应(在正常情况下)输出“Heartbeat 10”,因为心跳在初始超时(即 1 秒)后开始。同样,如果任务花费的时间少于心跳间隔,则根本不应该发生心跳。

什么是实现这个的好方法?

背景信息:我有一个正在监听 Azure Service Bus 队列的服务。我不想Complete 消息(这会将它从队列中永久删除),直到我完成处理它,这可能需要比最长消息LockDuration 5 分钟更长的时间。因此,我需要使用这种心跳方法在锁定持续时间到期之前调用RenewLockAsync,以便在进行长时间处理时消息不会超时。

【问题讨论】:

  • 这听起来类似于在异步任务中报告进度(只是触发报告的事情是一个时间间隔,并且没有真正的进度要报告,除了可能是心跳计数)。这些链接中的任何一个都有帮助吗? blogs.msdn.com/b/dotnet/archive/2012/06/06/…stackoverflow.com/questions/15408148/…
  • @TimS。它们很相似,但不是我想要的,尤其是在任务快速完成时从不报告的情况。此外,心跳不知道每个说的进度。但是,我很高兴看到您是否可以实现进度方法以匹配我的扩展 API,并使用更简单的代码获得相同的净效果。

标签: c# .net task-parallel-library async-await azureservicebus


【解决方案1】:

这是我的尝试:

public static class TaskExtensions {
    /// <summary>
    /// Issues the <paramref name="heartbeatAction"/> once every <paramref name="heartbeatInterval"/> while <paramref name="primaryTask"/> is running.
    /// </summary>
    public static async Task WithHeartbeat(this Task primaryTask, TimeSpan heartbeatInterval, Action<CancellationToken> heartbeatAction, CancellationToken cancellationToken) {
        if (cancellationToken.IsCancellationRequested) {
            return;
        }

        var stopHeartbeatSource = new CancellationTokenSource();
        cancellationToken.Register(stopHeartbeatSource.Cancel);

        await Task.WhenAny(primaryTask, PerformHeartbeats(heartbeatInterval, heartbeatAction, stopHeartbeatSource.Token));
        stopHeartbeatSource.Cancel();
    }
        
    private static async Task PerformHeartbeats(TimeSpan interval, Action<CancellationToken> heartbeatAction, CancellationToken cancellationToken) {
        while (!cancellationToken.IsCancellationRequested) {
            try {
                await Task.Delay(interval, cancellationToken);
                if (!cancellationToken.IsCancellationRequested) {
                    heartbeatAction(cancellationToken);
                }
            }
            catch (TaskCanceledException tce) {
                if (tce.CancellationToken == cancellationToken) {
                    // Totally expected
                    break;
                }
                throw;
            }
        }
    }
}

或稍作调整,您甚至可以使心跳异步,如下所示:

    /// <summary>
    /// Awaits a fresh Task created by the <paramref name="heartbeatTaskFactory"/> once every <paramref name="heartbeatInterval"/> while <paramref name="primaryTask"/> is running.
    /// </summary>
    public static async Task WithHeartbeat(this Task primaryTask, TimeSpan heartbeatInterval, Func<CancellationToken, Task> heartbeatTaskFactory, CancellationToken cancellationToken) {
        if (cancellationToken.IsCancellationRequested) {
            return;
        }

        var stopHeartbeatSource = new CancellationTokenSource();
        cancellationToken.Register(stopHeartbeatSource.Cancel);

        await Task.WhenAll(primaryTask, PerformHeartbeats(heartbeatInterval, heartbeatTaskFactory, stopHeartbeatSource.Token));

        if (!stopHeartbeatSource.IsCancellationRequested) {
            stopHeartbeatSource.Cancel();
        }
    }

    public static Task WithHeartbeat(this Task primaryTask, TimeSpan heartbeatInterval, Func<CancellationToken, Task> heartbeatTaskFactory) {
        return WithHeartbeat(primaryTask, heartbeatInterval, heartbeatTaskFactory, CancellationToken.None);
    }

    private static async Task PerformHeartbeats(TimeSpan interval, Func<CancellationToken, Task> heartbeatTaskFactory, CancellationToken cancellationToken) {
        while (!cancellationToken.IsCancellationRequested) {
            try {
                await Task.Delay(interval, cancellationToken);
                if (!cancellationToken.IsCancellationRequested) {
                    await heartbeatTaskFactory(cancellationToken);
                }
            }
            catch (TaskCanceledException tce) {
                if (tce.CancellationToken == cancellationToken) {
                    // Totally expected
                    break;
                }
                throw;
            }
        }
    }

这将允许您将示例代码更改为以下内容:

private static async Task PerformHeartbeat(CancellationToken cancellationToken) {
    Console.WriteLine("Starting heartbeat {0}", ++_heartbeatCount);
    await Task.Delay(1000, cancellationToken);
    Console.WriteLine("Finishing heartbeat {0}", _heartbeatCount);
}

PerformHeartbeat 可以替换为 RenewLockAsync 之类的异步调用,这样您就不必使用 Action 方法所需的 RenewLock 之类的阻塞调用来浪费线程时间。

我是answering my own question per SO guidelines,但我也愿意接受更优雅的方法来解决这个问题。

【讨论】:

  • 嗨,我是从您对我的类似问题的评论中得到这篇文章的。在 SB 队列的情况下,您具体何时何地更新锁?我拥有的工作者角色只是一个线程,消息接收和处理在 Run() 方法的 while 循环中运行。
  • @Aravind 当我收到 SB 消息时,我创建了一个任务来处理它。只要任务正在运行,该任务就会使用此心跳助手进行心跳。
  • 哦,好的。由于我使用的这种消息处理可能不是那么频繁使用,因此我没有创建用于处理每条消息的任务。我在示例代码中没有发现 RenewLock 的使用,这就是为什么要这样问的原因。
  • 这是一个旧版本 - 但在“异步”版本中应该将 Task.WhenAny 更改为 Task.WhenAll 吗?我正在解决一个类似的问题,并且似乎在主要任务完成后一直心跳加速。但是改回WhenAny 看起来它现在对我来说可以正常工作了吗?
【解决方案2】:

这是我的方法

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication3
{
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Start Main");
        StartTest().Wait();
        Console.ReadLine();
        Console.WriteLine("Complete Main");
    }

    static async Task StartTest()
    {
        var cts = new CancellationTokenSource();

        // ***Use ToArray to execute the query and start the download tasks. 
        Task<bool>[] tasks = new Task<bool>[2];
        tasks[0] = LongRunningTask("", 20, cts.Token);
        tasks[1] = Heartbeat("", 1, cts.Token);

        // ***Call WhenAny and then await the result. The task that finishes 
        // first is assigned to firstFinishedTask.
        Task<bool> firstFinishedTask = await Task.WhenAny(tasks);

        Console.WriteLine("first task Finished.");
        // ***Cancel the rest of the downloads. You just want the first one.
        cts.Cancel();

        // ***Await the first completed task and display the results. 
        // Run the program several times to demonstrate that different
        // websites can finish first.
        var isCompleted = await firstFinishedTask;
        Console.WriteLine("isCompleted:  {0}", isCompleted);
    }

    private static async Task<bool> LongRunningTask(string id, int sleep, CancellationToken ct)
    {
        Console.WriteLine("Starting long task");


        await Task.Delay(TimeSpan.FromSeconds(sleep));

        Console.WriteLine("Completed long task");
        return true;
    }

    private static async Task<bool> Heartbeat(string id, int sleep, CancellationToken ct)
    {
        while(!ct.IsCancellationRequested)
        {
            await Task.Delay(TimeSpan.FromSeconds(sleep));
            Console.WriteLine("Heartbeat Task Sleep: {0} Second", sleep);
        }

        return true;
    }

}

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-11-07
    • 1970-01-01
    • 2014-10-17
    • 1970-01-01
    • 2015-09-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多