Выполнять определенные фоновые задачи в отдельном ThreadPool, чтобы избежать голодания до критических задач, выполняемых в основном потоке - PullRequest
3 голосов
/ 28 марта 2020

Выполнение определенных фоновых задач (не потоков) в отдельном ThreadPool, чтобы избежать голодания для критических задач (не потоков), выполняемых в главном потоке

Наш сценарий

Мы размещаем веб-сайт большого объема WCF служба, которая логически имеет следующий код:

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

Теперь проблема в некоторых сценариях ios, фоновая задача с низким приоритетом может занять больше времени (~ 30 se c), например, , для обнаружения SQL проблем с соединением, проблем с производительностью БД, проблем с Redis-кешем и т. д. c. что приведет к задержке фоновых потоков, что означает, что TOTAL PENDING TASK COUNT увеличится из-за большого объема.

Это создает сценарий, в котором более новые исполнения API не могут планировать высокоприоритетную задачу, потому что много фоновых задач находятся в очереди.

Решения, которые мы попробовали

  1. Добавление TaskCreationOptions.LongRunning к задаче высокого уровня немедленно выполнит ее. Тем не менее, это не может быть решением для нас, так как везде в системе вызывается много задач, мы не можем сделать их долгосрочными везде. Кроме того, обработка WCF входящих API-интерфейсов будет зависеть от. NET пула потоков, который сейчас находится в состоянии истощения.

  2. Создание короткого замыкания с помощью фоновой задачи с низким уровнем примитивов через семафор. Создавать потоки можно только в том случае, если система способна их обработать (проверьте, не завершены ли ранее созданные потоки). Если нет, просто не создавайте темы. Например, из-за проблемы (скажем, из-за проблем с базой данных) ~ 10 000 фоновых потоков (не асинхронно c) находятся в состоянии ожидания ввода-вывода, что может вызвать истощение потоков в пуле основных. net потоков. В этом конкретном случае c мы могли бы добавить Семафор, чтобы ограничить создание до 100, поэтому, если 100 задач застряли, 101-я задача не будет создана в первую очередь.

Спросите об альтернативном решении

Есть ли способ специально порождать "задачи" в "пользовательских потоках / пуле потоков" вместо значения по умолчанию. NET thread бассейн. Это для фоновых задач, которые я упомянул, поэтому в случае задержки они не разрушают всю систему вместе с ними. Можно переопределить и создать пользовательский TaskScheduler для передачи в Task.Factory.StartNew (), поэтому созданные задачи НЕ будут по умолчанию. NET Пул потоков, а не какой-то другой пользовательский пул.

Ответы [ 2 ]

1 голос
/ 29 марта 2020

Вот метод stati c RunLowPriority, который вы можете использовать вместо Task.Run. Он имеет перегрузки для простых и общих задач c, а также для обычных и асинхронных делегатов.

const int LOW_PRIORITY_CONCURRENCY_LEVEL = 100;
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();
}

Имейте в виду, что событие Elapsed для System.Timers.Timer, которое имеет его свойство SynchronizingObject, установленное на null, работает и в потоках ThreadPool. Поэтому, если вы выполняете работу с низким приоритетом внутри этого обработчика, вам, вероятно, следует запланировать ее через тот же планировщик ограниченного параллелизма:

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
    });
};
1 голос
/ 29 марта 2020

На основе https://codereview.stackexchange.com/questions/203213/custom-taskscheduler-limited-concurrency-level?newreg=acb8e97fe4c94844a660bcd7473c4876 существует встроенное решение для ограничения порождения потоков через ограниченный параллелизм TaskScheduler.

Встроенный ConcurrentExclusiveSchedulerPair.ConcurrentScheduler может быть использован для достижения этой цели.

Для приведенного выше сценария следующий код ограничивает фоновые потоки от разрушения приложения / предотвращает голодание потока.

        {
            // fire and forget background task
            var task = Task.Factory.StartNew(
                () =>
                {
                    // background threads
                }
                , CancellationToken.None
                , TaskCreationOptions.None
                , concurrentSchedulerPair.ConcurrentScheduler);
        }

        private static ConcurrentExclusiveSchedulerPair concurrentSchedulerPair = new ConcurrentExclusiveSchedulerPair(
            TaskScheduler.Default,
            maxConcurrencyLevel: 100);

Предупреждение об использовании параметров TaskScheduler.Default и maxConcurrencyLevel: 100, скажем, вы создаете 10000 задач с помощью этого ограниченного контура c -scheduler и пытаетесь сразу же создать другой поток, используя ' default-scheduler ', этот новый spawn будет заблокирован, если не будут созданы все 100 потоков. Если вы попробуете maxConcurrencyLevel: 10, новые потоки создаются сразу, а не блокируются после создания всех 10 потоков.

Спасибо @Theodor Zoulias за указатель.

...