在单独的线程池中执行某些后台任务,以避免主线程中执行的关键任务出现饥饿现象

AAA*_*Guy 4 c# multithreading task task-parallel-library async-await

在单独的线程池中执行某些后台任务(不是线程),以避免主线程中执行的关键任务(不是线程)饥饿

我们的场景

我们托管一个大容量的 WCF Web 服务,其逻辑上具有以下代码:

void WcfApiMethod()
{
   // logic

   // invoke other tasks which are critical
   var mainTask = Task.Factory.StartNew(() => { /* important task */ });
   mainTask.Wait();

   // invoke background task which is not critical
   var backgroundTask = Task.Factory.StartNew(() => { /* some low-priority background action (not entirely async) */ });
   // no need to wait, as this task is best effort. Fire and forget

   // other logic
}

// other APIs
Run Code Online (Sandbox Code Playgroud)

现在的问题是,在某些情况下,低优先级后台任务可能需要更长的时间(约 30 秒),例如,检测 SQL 连接问题、DB 性能问题、redis 缓存问题等,这将使这些后台线程延迟,这意味着由于数量较大,TOTAL PENDING TASK COUNT 将增加。

这会导致 API 的较新执行无法安排高优先级任务,因为大量后台任务在队列中。

我们尝试过的解决方案

  1. 将 TaskCreationOptions.LongRunning 添加到高优先级任务将立即执行它。然而,这对我们来说不是一个解决方案,因为系统中到处都有很多任务被调用,我们不能让它们在任何地方都长时间运行。此外,WCF 对传入 API 的处理将依赖于 .NET 线程池,而该线程池现在正处于饥饿状态。

  2. 通过信号量短路低优先级后台任务创建。仅当系统有能力处理线程时才生成线程(检查早期创建的线程是否已退出)。如果没有,就不要产生线程。例如,由于某个问题(例如数据库性能问题),大约 10,000 个后台线程(非异步)处于 IO 等待状态,这可能会导致主 .net 线程池中的线程匮乏。在这种特定情况下,我们可以添加一个信号量来将创建限制为 100 个,因此如果 100 个任务被卡住,则首先不会创建第 101 个任务。

询问替代解决方案

有没有办法专门在“自定义线程/线程池”上生成“任务”,而不是默认的 .NET 线程池。这是针对我提到的后台任务的,因此,如果它们被延迟,它们不会导致整个系统崩溃。可以覆盖并创建一个自定义的 TaskScheduler 以传递到 Task.Factory.StartNew() 中,因此创建的任务不会位于默认的 .NET 线程池中,而是位于其他一些自定义池中。

The*_*ias 5

这是一个静态RunLowPriority方法,您可以使用它来代替Task.Run. 它具有针对简单和通用任务以及普通和异步委托的重载。

const int LOW_PRIORITY_CONCURRENCY_LEVEL = 2;
static TaskScheduler LowPriorityScheduler = new ConcurrentExclusiveSchedulerPair(
    TaskScheduler.Default, LOW_PRIORITY_CONCURRENCY_LEVEL).ConcurrentScheduler;

public static Task RunLowPriority(Action action,
    CancellationToken cancellationToken = default)
{
    return Task.Factory.StartNew(action, cancellationToken,
        TaskCreationOptions.DenyChildAttach, LowPriorityScheduler);
}

public static Task RunLowPriority(Func<Task> function,
    CancellationToken cancellationToken = default)
{
    return Task.Factory.StartNew(function, cancellationToken,
        TaskCreationOptions.DenyChildAttach, LowPriorityScheduler).Unwrap();
}

public static Task<TResult> RunLowPriority<TResult>(Func<TResult> function,
    CancellationToken cancellationToken = default)
{
    return Task.Factory.StartNew(function, cancellationToken,
        TaskCreationOptions.DenyChildAttach, LowPriorityScheduler);
}

public static Task<TResult> RunLowPriority<TResult>(Func<Task<TResult>> function,
    CancellationToken cancellationToken = default)
{
    return Task.Factory.StartNew(function, cancellationToken,
        TaskCreationOptions.DenyChildAttach, LowPriorityScheduler).Unwrap();
}
Run Code Online (Sandbox Code Playgroud)

通过该方法调度的操作RunLowPriority将在ThreadPool线程上运行,但最多ThreadPool可以同时分配所有可用线程中的 2 个RunLowPriority任务。

请记住,属性设置为aElapsed的事件也在线程中运行。因此,如果您在此处理程序中执行低优先级工作,您可能应该通过相同的有限并发调度程序来调度它:System.Timers.TimerSynchronizingObjectnullThreadPool

var timer = new System.Timers.Timer();
timer.Elapsed += (object sender, System.Timers.ElapsedEventArgs e) =>
{
    Thread.Sleep(10); // High priority code
    var fireAndForget = RunLowPriority(() =>
    {
        if (!timer.Enabled) return;
        Thread.Sleep(1000); // Simulate long running code that has low priority
    });
};
Run Code Online (Sandbox Code Playgroud)