Подтвердить что ты не робот

Является ли 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());
}
4b9b3361

Ответ 1

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

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

Интересный случай заключается в том, что вы вызываете Wait() в Задаче в состоянии WaitingToRun, что означает, что его ранее поставили в очередь на TaskScheduler, но TaskScheduler еще не добрался до фактического выполнения делегата Task еще. В этом случае вызов Wait попросит планировщика, нормально ли запускать Task, а затем в текущем потоке, путем вызова метода планировщика TryExecuteTaskInline. Планировщик может выбрать либо запустить задачу тогда и там, используя вызов base.TryExecuteTask, либо он может вернуть false, чтобы указать, что он не выполняет задачу (часто это делается с помощью логики типа return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);.. причина TryExecuteTask возвращает Boolean, так это то, что он обрабатывает синхронизацию, чтобы гарантировать, что задание выполняется только один раз). Таким образом, если планировщик хочет полностью запретить вложение задачи во время ожидания, ее можно просто реализовать как return false; Если планировщик хочет всегда разрешать встраивание, когда это возможно, его можно просто реализовать как return TryExecuteTask(task); В текущей реализации ( как .NET 4, так и .NET 4.5, и я лично не ожидаю, что это изменится), планировщик по умолчанию, который нацелен на ThreadPool, позволяет встраивать, если текущий поток является потоком ThreadPool, и если этот поток был тем, кто ранее поставил задачу в очередь,

Обратите внимание, что здесь нет произвольного повторного включения, поскольку планировщик по умолчанию не будет накачать произвольные потоки при ожидании задачи... он позволит только этой задаче быть встроенным и, конечно же, любой вставкой этой задачи в свою очередь решает сделать. Также обратите внимание, что Wait даже не спросит планировщика в определенных условиях, вместо этого предпочитает блокировать. Например, если вы передадите отмененный CancellationToken или если вы пройдете бесконечный тайм-аут, он не попытается встроить его, потому что может потребоваться сколько угодно времени для выполнения задачи, которая все или ничего, и что может привести к значительному отсрочке запроса на отмену или таймаута. В целом, TPL пытается нанести приличный баланс между тем, чтобы тратить поток, который выполняет ожидание, и повторно использовать этот поток слишком много. Такая вставка действительно важна для рекурсивных проблем с разделением и победой, например. 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.


EDIT: Хорошо, похоже, что я был совершенно не прав, и поток, который в настоящее время ждет задания, может быть захвачен. Вот более простой пример этого:

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

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

Ответ 2

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

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

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

(Сид мысли: семафоры реентерабельны без декремента?)

Ответ 3

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

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

Ответ 4

Решение с 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;

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