Страницы

Поиск по вопросам

пятница, 13 декабря 2019 г.

task и нативный thread

#c_sharp #многопоточность


здравствуйте, не могу понять что такое task с точки зрения операционной системы...
везде пишут, что task-based параллелилизм берет "задачу" из пула... а что значит
"задача"?
вот есть нативный thread со своим стеком... правильно ли я понимаю что task это "thread
без своего стека" т.е. легковесный поток? если да, то как это реализовано с точки зрения
операционной системы?
и правильно ли я полагаю, что async/await использует как-то эту самую концепцию легковесных
потоков?
    


Ответы

Ответ 1



Нет, вы понимаете не вполне верно. Для начала, как правильно замечает @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(); var timer = new System.Threading.Timer( _ => { tcs.SetCompleted(null); timer.Dispose(); }, null, milliseconds, System.Threading.Timeout.Infinite); return tcs.Task; } Этот является аналогом Task.Delay. Ещё по теме: Нет никакого потока (Оригинал: There is no thread). Как работают await async Нужен async/await или не нужен?

Ответ 2



Влад в своём ответе напирает на высокоуровневые концепции тасков, в то время как топикстартера больше интересует устройство изнутри, как я понял. Опишу, как это вижу я. Все задачи в программировании условно можно разделить на два вида: CPU-bound - нагрузка на процессор, вычислительные задачи; IO-bound - операции ввода-вывода, посылка/получение данных в/из файла/сети и т. п. Для вторых задач отдельный поток не нужен. Они выполняются на специальных портах завершения ввода-вывода - IOCP (IO completion port). Грубо говоря, процессор даёт контролёру команду: скопируй участок памяти с такого-то адреса по такой в поток (stream), после чего может заниматься своими делами. Когда контролёр завершит работу, он посылает аппаратное прерывание (IRQ) центральному процессору (равносильно как обухом по голове), что задание выполнено. ЦП на это реагирует тем или иным образом. Для тяжёлых вычислений поток необходим. Создание потока довольно дорогая операция, поэтому в системе поддерживается пул запущенных потоков (его размер можно менять). Можно создать новый поток, можно взять из пула. Когда создаётся вычислительная задача вызовом Task.Factory.StartNew или Task.Run, то поток берётся из пула. В методе StartNew можно указать параметр TaskCreationOptions.LongRunning - при этом будет создан новый поток, а не взят из пула. Дело в том, что потоки из пула нежелательно занимать на долгое время, ведь они могут понадобиться в любой момент другим приложениям. Поэтому на потоках из пула принято делать относительно короткие операции. Нужно отметить, что ОС умеет отслеживать случаи, когда в отдельном потоке выполняются длительные IO-операции: она автоматически перекидывает их на IOCP. Когда поток из пула (который надолго занимать крайне нежелательно!) висит на вводе-выводе, планировщик пула автоматически добавит новый поток в пул (он умеет это детектировать), чтобы предотвратить так называемое голодание (starvation) системы. В данный момент вычислительный Task напрямую соответствует управляемому потоку Thread. Однако, это в любой момент может быть изменено и полагаться на это нельзя. В свою очередь, управляемый Thread напрямую соответствует нативному потоку ОС. Аналогично, это в любой момент может быть изменено и закладываться на это нельзя.

Комментариев нет:

Отправить комментарий