Затраты на создание задач - PullRequest
0 голосов
/ 09 января 2019

Я читаю книгу "Террелл Р. - Параллелизм в .NET".

Вот хороший пример кода:

Lazy<Task<Person>> person = new Lazy<Task<Person>>(
     async () =>
     {
         using (var cmd = new SqlCommand(cmdText, conn))
         using (var reader = await cmd.ExecuteReaderAsync())
         {
             // some code...
         }
     });

async Task<Person> FetchPerson()
{
    return await person.Value;
}

Автор сказал:

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

Насколько я понимаю, поток приходит к FetchPerson и застревает в исполнении Lamda. Это действительно плохо? Какие последствия?

В качестве решения автор предлагает создать задание:

Lazy<Task<Person>> person = new Lazy<Task<Person>>(
      () => Task.Run(
        async () =>
        {
            using (var cmd = new SqlCommand(cmdText, conn))
            using (var reader = await cmd.ExecuteReaderAsync())
            {
                // some code...
            }
        }));

Это действительно правильно? Это операция ввода-вывода, но мы крадем поток ЦП из Threadpool.

Ответы [ 3 ]

0 голосов
/ 09 января 2019

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

Лямбда может быть запущена из любого потока (если вы не заботитесь о том, какие типы потоков вы позволяете получить доступ к значению Lazy), и, как таковой, он будет запускаться в контексте из этой темы. Это не , потому что это асинхронный , было бы верно, даже если бы он был синхронным, если бы он выполнялся в контексте любого потока, вызывающего его.

Насколько я понимаю, поток приходит в FetchPerson и застревает в исполнении Lamda.

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

Это действительно плохо? Какие последствия?

Если вы неправильно внедрили свой асинхронный метод и долго выполняете синхронную работу, то да, вы блокируете этот поток / контекст. Если нет, то нет.

Кроме того, по умолчанию все продолжения в ваших асинхронных методах будут выполняться в исходном контексте (если он вообще имеет SynchonrizationContext). В вашем случае ваш код почти наверняка не полагается на повторное использование этого контекста (поскольку вы не знаете, какие контексты может иметь ваш вызывающий объект, я не могу представить, что вы написали остальную часть кода для его использования). Учитывая это, вы можете вызывать .ConfigureAwait(false) для всего, что вы await, чтобы не использовать текущий контекст для этих продолжений. Это просто незначительное улучшение производительности, чтобы не тратить время на планирование работы в исходном контексте, ожидание чего-либо еще, что ему нужно, или заставлять что-либо еще ждать этого кода, когда это не нужно.

В качестве решения автор предлагает создать задание: [...] Это действительно правильно?

Это ничего не сломает. Это запланирует работу для выполнения в потоке пула потоков, а не в исходном контексте. Это будет иметь дополнительные издержки для начала. Вы можете сделать примерно то же самое с меньшими накладными расходами, просто добавив ConfigureAwait(false) ко всему, что вы await.

Это операция ввода-вывода, но мы крадем поток ЦП из Threadpool.

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

0 голосов
/ 19 января 2019

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

Делегат в этом примере будет работать в вызывающем потоке, пока не будет нажата await. Затем он вернет Task, что Task перейдет в ленивый, и ленивый полностью завершен на этом этапе.

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

Task.Run - это способ избежать этого. Он перемещает код в пул потоков, придавая ему определенный контекст. Накладные расходы состоят в организации очереди в пул. Задача пула закончится в кулак await. Так что это , а не асинхронная синхронизация. Блокировка не вводится. Единственное изменение состоит в том, что происходит в процессах на основе потоков (теперь детерминировано в пуле потоков).

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

Если вы уверены, что все абоненты Value работают в подходящем контексте, тогда вам это не нужно. Но если вы ошиблись, это серьезная ошибка. Таким образом, вы можете утверждать, что лучше вставлять в обороне Task.Run. Будьте прагматичны и делайте то, что работает.

Также обратите внимание, что Task.Run работает асинхронно (так сказать). Задание, которое он возвращает, по сути, разворачивает внутреннюю задачу (в отличие от Task.Factory.StartNew). Так что безопасно вкладывать задачи, как здесь.

0 голосов
/ 09 января 2019

Я совершенно не понимаю, почему Террелл Р. предлагает использовать Task.Run. Это не имеет добавленной стоимости вообще. В обоих случаях лямбда будет назначена в пул потоков. Поскольку он содержит операции ввода-вывода, рабочий поток из пула потоков будет освобожден после вызова ввода-вывода; когда вызов IO завершится, следующий оператор продолжится в произвольном потоке из пула потоков.

Кажется, что автор пишет:

выражение будет выполняться в контексте

Да, выполнение вызовов IO начнется в контексте вызывающего, но завершится в произвольном контексте, если вы не вызовете .ConfigureAwait.

...