Гарантируется ли Task.Factory.StartNew () использовать другой поток, а не вызывающий поток?

Я запускаю новую задачу из функции, но не хочу, чтобы она выполнялась в том же потоке. Мне все равно, на каком потоке он работает, пока он другой (поэтому информация приведена в этот вопрос не помогает).

Я гарантирую, что приведенный ниже код всегда будет выходить из TestLock, прежде чем разрешить Task t чтобы ввести его снова? Если нет, каков рекомендуемый шаблон проектирования для предотвращения повторного входа?

object TestLock = new object();

public void Test(bool stop = false) {
    Task t;
    lock (this.TestLock) {
        if (stop) return;
        t = Task.Factory.StartNew(() => { this.Test(stop: true); });
    }
    t.Wait();
}

Редактировать . Исходя из приведенного ниже ответа Джона Скита и Стивена Тауба, простым способом детерминированного предотвращения повторного входа будет передача CancellationToken, как показано в этом методе расширения:

public static Task StartNewOnDifferentThread(this TaskFactory taskFactory, Action action) 
 {
    return taskFactory.StartNew(action: action, cancellationToken: new CancellationToken());
}
73 голоса | спросил Erwin Mayer 3 rdEurope/Moscowp30Europe/Moscow09bEurope/MoscowMon, 03 Sep 2012 14:19:37 +0400 2012, 14:19:37

4 ответа


0

Я отправил Стивену Таубу (Stephen Toub) - члену команды PFX - об этом вопросе. Он вернулся ко мне очень быстро, с большим количеством деталей - поэтому я просто скопирую и вставлю его текст сюда. Я не цитировал все это, поскольку чтение большого количества цитируемого текста в конечном итоге становится менее удобным, чем ванильное чёрно-белое, но на самом деле, это Стивен - я не знаю много такого :) Я сделал этот вики сообщества ответов, чтобы отразить, что все добрая черта ниже не является моим контентом:

  

Если вы позвоните Wait() на Задание выполнено, блокирования не будет (будет просто сгенерировано исключение, если задание выполнено с TaskStatus , кроме RanToCompletion или иным образом вернитесь как nop ). Если вы вызываете Wait() для Задачи, которая уже выполняется, она должна блокироваться, поскольку больше ничего не может разумно сделать (когда я говорю «блокировать», я Я включаю в себя как ожидание на ядре, так и вращение, так как это обычно делает смесь обоих) Аналогично, если вы вызываете Wait() для Задачи, в которой есть Created или WaitingForActivation, он будет блокироваться до завершения задачи. Ни один из них не является интересным обсуждаемым случаем.

     

Интересный случай, когда вы вызываете Wait() для задачи в WaitingToRun, означающее, что ранее оно было поставлено в очередь в TaskScheduler , но этот TaskScheduler еще не дошел до фактического запуска делегата Задачи. В этом случае вызов Wait спросит планировщика, можно ли сразу же выполнить задачу в текущем потоке, посредством вызова метода TryExecuteTaskInline планировщика. Это называется встраивание . Планировщик может выбрать встроенную задачу с помощью вызова base.TryExecuteTask или может вернуть «false», чтобы указать, что он не выполняется задача (часто это делается с помощью логики, как ...

return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);
     

Причина, по которой TryExecuteTask возвращает логическое значение, заключается в том, что он обрабатывает синхронизацию, чтобы гарантировать, что данная задача будет выполнена только один раз). Итак, если планировщик хочет полностью запретить встраивание Задачи во время Wait, он может быть просто реализован как return false; Если планировщик хочет всегда разрешать встраивание, когда это возможно, его можно реализовать следующим образом:

return TryExecuteTask(task);
     

В текущей реализации (и .NET 4, и .NET 4.5, и я лично не ожидаю, что это изменится), планировщик по умолчанию, предназначенный для ThreadPool, позволяет встраивать, если текущий поток является потоком ThreadPool, и если это поток был тем, кто ранее поставил задачу в очередь.

     

Обратите внимание, что здесь нет произвольного повторного входа, так как планировщик по умолчанию не будет перекачивать произвольные потоки при ожидании задачи ... он позволит только встроить эту задачу и, конечно, любой встраивать эту задачу в свою очередь решает сделать. Также обратите внимание, что Wait даже не запрашивает планировщик в определенных условиях, вместо этого предпочитая блокировать. Например, если вы передаете отменяемый CancellationToken или если вы пройдете вНеограниченный тайм-аут, он не будет пытаться встроить, потому что может потребоваться произвольно много времени, чтобы включить выполнение задачи, которое является полным или ничем, и это может в конечном итоге значительно задержать запрос отмены или тайм-аут. В целом, TPL пытается найти здесь достойный баланс между потерей потока, который выполняет Wait, и повторным использованием этого потока слишком много. Этот вид встраивания действительно важен для рекурсивных проблем типа «разделяй и властвуй» (например, QuickSort ), где Вы порождаете несколько задач, а затем ждете их завершения. Если бы это было сделано без встраивания, вы бы очень быстро зашли в тупик, исчерпав все потоки в пуле и любые будущие, которые он хотел вам дать.

     

Отдельно от Wait также возможно (удаленно), что Task.Factory.StartNew вызов может закончиться выполнением задачи тогда и там, если планировщик является используется для запуска задачи синхронно, как часть вызова QueueTask. Ни один из планировщиков, встроенных в .NET, никогда не сделает этого, и я лично думаю, что это будет плохой дизайн для планировщика, но это теоретически возможно, например:

protected override void QueueTask(Task task, bool wasPreviouslyQueued)
{
    return TryExecuteTask(task);
}
     

Перегрузка Task.Factory.StartNew, которая не принимает TaskScheduler использует планировщик из TaskFactory, который в случае Task.Factory цели TaskScheduler.Current. Это означает, что если вы вызываете Task.Factory.StartNew из Задачи, поставленной в очередь в этот мифический RunSynchronouslyTaskScheduler, он также будет поставлен в очередь в RunSynchronouslyTaskScheduler, что приведет к StartNew вызов, выполняющий задание синхронно. Если вас это вообще беспокоит (например, вы реализуете библиотеку и не знаете, откуда будете вызываться), вы можете явно передать TaskScheduler.Default для вызова StartNew, используйте Task.Run (который всегда идет в TaskScheduler.Default), или используйте TaskFactory создан для цели TaskScheduler.Default.


РЕДАКТИРОВАТЬ: Хорошо, похоже, я был совершенно неправ, и поток, который в данный момент ожидает выполнения задачи , можно перехватить. Вот простой пример этого:

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

namespace ConsoleApplication1 {
    class Program {
        static void Main() {
            for (int i = 0; i < 10; i++)
            {
                Task.Factory.StartNew(Launch).Wait();
            }
        }

        static void Launch()
        {
            Console.WriteLine("Launch thread: {0}", 
                              Thread.CurrentThread.ManagedThreadId);
            Task.Factory.StartNew(Nested).Wait();
        }

        static void Nested()
        {
            Console.WriteLine("Nested thread: {0}", 
                              Thread.CurrentThread.ManagedThreadId);
        }
    }
}

Пример вывода:

Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4

Как видите, во многих случаях ожидающий поток повторно используется для выполнения новой задачи. Это может произойти, даже если поток получил блокировку. Противный повторный вход. Я соответственно шокирован и обеспокоен: (

ответил Shepmaster 7 Jpm1000000pmWed, 07 Jan 2015 21:30:25 +030015 2015, 21:30:25
0

Почему бы просто не придумать его, а не наклониться назад, чтобы этого не произошло?

TPL - это красная сельдь, повторное включение может произойти в любом коде, если вы можете создать цикл, и вы точно не знаете, что произойдет «к югу» от вашего стекового фрейма. Синхронный возврат - лучший результат здесь - по крайней мере, вы не можете самостоятельно заблокировать себя (так легко).

Замки управляют синхронизацией между потоками. Они ортогональны управлению повторным входом. Если вы не защищаете подлинный одноразовый ресурс (возможно, физическое устройство, и в этом случае вам, вероятно, следует использовать очередь), почему бы просто не убедиться в том, что состояние вашего экземпляра соответствует, чтобы повторный вход мог «просто работать».

(Сторонняя мысль: Семафоры возвращаются без уменьшения?)

ответил piers7 4 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowTue, 04 Sep 2012 17:22:33 +0400 2012, 17:22:33
0

Вы можете легко проверить это, написав быстрое приложение, которое разделяет сокетное соединение между потоками /задачами.

Задача получит блокировку перед отправкой сообщения в сокет и ожиданием ответа. Как только это блокируется и становится бездействующим (IOBlock), поставьте другую задачу в том же блоке, чтобы сделать то же самое. Он должен блокироваться при получении блокировки, если это не так, и вторая задача может пройти блокировку, потому что она запускается тем же потоком, тогда у вас есть проблема.

ответил JonPen 4 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowTue, 04 Sep 2012 11:54:19 +0400 2012, 11:54:19
0

Решение с new CancellationToken(), предложенное Эрвином, не сработало для меня, в любом случае произошло встраивание.

Поэтому я использовал другое условие, рекомендованное Джоном и Стивеном (... or if you pass in a non-infinite timeout ...):

  Task<TResult> task = Task.Run(func);
  task.Wait(TimeSpan.FromHours(1)); // Whatever is enough for task to start
  return task.Result;

Примечание: здесь для простоты пропущена обработка исключений и т. д. Вы должны учитывать это в рабочем коде.

ответил Vitaliy Ulantikov 1 stEurope/Moscowp30Europe/Moscow09bEurope/MoscowThu, 01 Sep 2016 10:09:58 +0300 2016, 10:09:58

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132