здравствуйте, не могу понять что такое task с точки зрения операционной системы... везде пишут, что task-based параллелилизм берет "задачу" из пула... а что значит "задача"? вот есть нативный thread со своим стеком... правильно ли я понимаю что task это "thread без своего стека" т.е. легковесный поток? если да, то как это реализовано с точки зрения операционной системы? и правильно ли я полагаю, что async/await использует как-то эту самую концепцию легковесных потоков?
2 Answers
Нет, вы понимаете не вполне верно.
Для начала, как правильно замечает @PetSerAl, Task — внутреннее понятие .NET и никак не отображается на объекты операционной системы.
Затем, информация о том, что «task-based параллелилизм берет "задачу" из пула», просто ошибочна. Проще всего представлять себе Task как promise (которым он реально и является).
Вы должны видеть Task лишь как объект, который в какой-то момент времени завершается. Слово «завершается» означает лишь то, что он переходит в состояние Completed, и что вызов var result = await task; при этом также завершается. При этом await не является блокирующим вызовом, оно прекращает выполнение текущего кода, и возобновляет его при завершении promise.
На деле, то, как именно реализован Task, и в какой ситуации он представляет собой реально бегущий в каком-либо потоке (или в каких-либо потоках) код, а в какой вовсе ни в каком (например, код подписался на какое-либо событие, и завершит таск при приходе этого события). Вас должна интересовать лишь семантика обещания завершиться и произвести результат (или исключение).
Это похоже на легковесный поток, но не является им: поток не производит результат, а просто бежит, в отличие от таска, который каким-то своим образом получает результат в какой-то отдалённый момент времени. Но и в том, и в другом случае к него нету выделенного стека: ведь потоки и стек — более низкоуровневое понятие.
Пример Task'а, который не занимает никакого потока, легко сконструировать:
Task Delay(int milliseconds)
{
var tcs = new TaskCompletionSource<object>();
var timer = new System.Threading.Timer(
_ => {
tcs.SetCompleted(null); timer.Dispose();
}, null, milliseconds, System.Threading.Timeout.Infinite);
return tcs.Task;
}
Этот является аналогом Task.Delay.
Ещё по теме:
- 206,799
-
хм, вы говорите что await не является блокирующим вызовом... но чтобы обеспечить "неблокируемость", то, что следует за await, должно же выполняться каким-то образом параллельно.... каждый раз создавать нативный поток слишком дорого... скорее всего берется из какого-то пула... правильно ли я понимаю, и если да, то откуда берется этот пул? ну, например, std::async с флагом std::launch::async в с++ на windows берет и выполняют функцию в отдельном треде взятом из пула(в linux такого пула нету)...вот что это за пул то? думаю, это нечто системное, который так же используется и для async/await – xperious May 07 '17 at 12:04
-
@xperious: Давайте-ка я ещё раз повторюсь, что поток в Task вовсе не обязателен. Пример: https://pastebin.com/f47CUNLA — Task без потока, завершается через 600 миллисекунд. – VladD May 07 '17 at 12:15
-
@xperious: То, что следует за await, не «выполняется». Оно просто дожидается promise и возобновляет выполнение в прерванной точке. Task в C#, наверное, не то же самое, что
std::asyncв С++. – VladD May 07 '17 at 12:16 -
ну так чтобы дождаться promise, результат то для этого promise где-то должен быть вычислен?) разве нет, а это делается "параллельно" каким-то образом – xperious May 07 '17 at 12:19
-
1@xperious: ну да, но для этого не обязательно занимать поток. Вы смотрели пример кода? – VladD May 07 '17 at 12:19
-
я в c# конечно слабоват... но что то я не могу понять: разве var tcs = new TaskCompletionSource – xperious May 07 '17 at 12:23
-
@xperious: Ну, создаётся объект типа
Task, да. Но это ж объект? Никакой поток в этот момент не запускается. – VladD May 07 '17 at 12:26 -
хм, хорошо, вот, допустим, делегаты можно запускать асинхронно... если без запуска отдельного треда или таска, то как это может быть реализовано? – xperious May 07 '17 at 12:31
-
-
-
@xperious: .NET заводит свой пул потоков. Но это не имеет прямого отношения к
Task'ам (хотя имеет косвенное). – VladD May 07 '17 at 12:40 -
вот не могу понять: у нас уже есть халявный пул, созданный за нас... почему для получения результата promise (из таска) это может делаться не в отдельном потоке? какой смысл тогда в тасках, если ничего нельзя знать точно – xperious May 07 '17 at 12:47
-
-
@xperious: Потому что пул не халявный. У вас может быть в системе десятки тысяч тасков, если каждому из них раздать thread, это сильно нагрузит систему. – VladD May 07 '17 at 13:26
-
-
@xperious: По идее, да. Пул создаётся ленивым образом наверное, но это по идее не гарантировано. – VladD May 07 '17 at 13:57
-
Влад в своём ответе напирает на высокоуровневые концепции тасков, в то время как топикстартера больше интересует устройство изнутри, как я понял. Опишу, как это вижу я.
Все задачи в программировании условно можно разделить на два вида:
- CPU-bound - нагрузка на процессор, вычислительные задачи;
- IO-bound - операции ввода-вывода, посылка/получение данных в/из файла/сети и т. п.
Для вторых задач отдельный поток не нужен. Они выполняются на специальных портах завершения ввода-вывода - IOCP (IO completion port). Грубо говоря, процессор даёт контроллеру команду: скопируй участок памяти с такого-то адреса по такой в поток (stream), после чего может заниматься своими делами. Когда контроллер завершит работу, он посылает аппаратное прерывание (IRQ) центральному процессору (равносильно как обухом по голове), что задание выполнено. ЦП на это реагирует тем или иным образом.
Для тяжёлых вычислений поток необходим. Создание потока довольно дорогая операция, поэтому в системе поддерживается пул запущенных потоков (его размер можно менять). Можно создать новый поток, можно взять из пула.
Когда создаётся вычислительная задача вызовом Task.Factory.StartNew или Task.Run, то поток берётся из пула. В методе StartNew можно указать параметр TaskCreationOptions.LongRunning - при этом будет создан новый поток, а не взят из пула. Дело в том, что потоки из пула нежелательно занимать на долгое время, ведь они могут понадобиться в любой момент другим приложениям. Поэтому на потоках из пула принято делать относительно короткие операции.
Нужно отметить, что ОС умеет отслеживать случаи, когда в отдельном потоке выполняются длительные IO-операции: она автоматически перекидывает их на IOCP. Когда поток из пула (который надолго занимать крайне нежелательно!) висит на вводе-выводе, планировщик пула автоматически добавит новый поток в пул (он умеет это детектировать), чтобы предотвратить так называемое голодание (starvation) системы.
В данный момент вычислительный Task напрямую соответствует управляемому потоку Thread. Однако, это в любой момент может быть изменено и полагаться на это нельзя. В свою очередь, управляемый Thread напрямую соответствует нативному потоку ОС. Аналогично, это в любой момент может быть изменено и закладываться на это нельзя.
- 29,233
-
«управляемый Thread напрямую соответствует нативному потоку ОС» — за исключением, если мне не изменяет память, ASP.NET 2.0. – VladD May 07 '17 at 13:27
-
@Alexander Petrov, спасибо за ответ... если вы в курсе про std::async, там что за пул берется? – xperious May 07 '17 at 13:54
-
@xperious - если поток вообще используется (см. ответ VladD), то берётся из ThreadPool. – Alexander Petrov May 07 '17 at 14:04
-
-
@AlexanderPetrov, и вот что не понятно, что iocp, что epoll в линуксе используют одинаковую технику, только я не понимаю смысла: вот есть загруженный проц на 100% вычислительными задачами, и тут пришло в сокет что-то и мы асинхронно считываем и генерируем сигнал(в линуксе) по приходу... ресурсы же на i/o тоже тратятся, но их распределяет проц сам чтоли, не операционная система? – xperious May 07 '17 at 14:14
-
@xperious: Ну уровне процессора понятия thread вовсе нет. Thread — это абстракция операционной системы. – VladD May 07 '17 at 14:21
-
1А,
std. Ну, берётся пул потоков ОС. Операции IO выполняет контролёр (DMA и т. п.); ЦП в этом не участвует. Он лишь даёт команду на начало операции, потом по шине получит сигнал о её завершении. – Alexander Petrov May 07 '17 at 14:21 -
@VladD, а я и не про тред... я так понял iocp и epoll это аппаратное что-то – xperious May 07 '17 at 14:24
-
@xperious: Ну, это более сервис ОС. Посмотрите в статью There is no thread, там как раз про IOCP. – VladD May 07 '17 at 14:26
-
@xperious - да, аппаратное. Можно почитать Windows Internals, Russinovich - "Внутреннее устройство Windows", Руссинович. – Alexander Petrov May 07 '17 at 14:26
-
-
-
@AlexanderPetrov, о, годно... в общем спасибо всем за ответы, стало хоть чуток понятней... а то green threads, легковесные потоки, fiber, async/await - каша полнейшая – xperious May 07 '17 at 14:35
-
1
Taskэто примитив .NET и в операционной системе никак не представлен. – user181245 May 07 '17 at 03:07Taskнапрямую соответствует управляемому потокуThread. Однако, это в любой момент может быть изменено и полагаться на это нельзя. В свою очередь, управляемыйThreadнапрямую соответствует нативному потоку ОС. Аналогично, это в любой момент может быть изменено и закладываться на это нельзя. – Alexander Petrov May 07 '17 at 09:52