Сегодня мы рассмотрим довольно распространенный вопрос по поводу использования Task’а как обертки некого синхронного кода.
Представьте, что у вас есть интерфейс:
public interface IAsyncCommand { Task ExecuteAsync(); }
Теперь представьте, что вы хотите реализовать этот интерфейс, но код сработает на самом деле не асинхронно. Это задача не ресурсоемкая и не требует своего собственного потока. Это просто обычный кусок синхронного кода. Есть три способа сделать это:
public class AsyncCommand1 : IAsyncCommand { public Task ExecuteAsync() { int x = 2 + 2; return Task.FromResult(true); } } public class AsyncCommand2 : IAsyncCommand { public async Task ExecuteAsync() { int x = 2 + 2; } } public class AsyncCommand3 : IAsyncCommand { public Task ExecuteAsync() { return Task.Run(() => { int x = 2 + 2; }) } }
Так в чем же разница между этими тремя реализациями? С точки зрения генерации IL кода ответов довольно много. С точки зрения относительной производительности эти реализации попадают строго в категорию микро-оптимизации.
Код
Первая реализация является наиболее оптимальным подходом. Метод возвращает Task со внутренним конструктором, который принимает результат. Статический метод FromResult() это просто публичная обертка конструктора, который возвращает выполненную задачу с вашим значением.
Второй подход я нахожу попросту более простым для чтения, потому что вам просто надо добавить async и все. НО он генерирует довольно много IL кода. Вы получаете полный созданный конечный автомат, который инициализируется, а затем выполняется.
[AsyncStateMachine(typeof (Class1.<ExecuteAsync>d__0))] [DebuggerStepThrough] public Task ExecuteAsync() { Class1.<ExecuteAsync>d__0 stateMachine; stateMachine.<>4__this = this; stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create(); stateMachine.<>1__state = -1; stateMachine.<>t__builder.Start<Class1.<ExecuteAsync>d__0>(ref stateMachine); return stateMachine.<>t__builder.Task; } [CompilerGenerated] [StructLayout(LayoutKind.Auto)] private struct <ExecuteAsync>d__0 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Class1 <>4__this; void IAsyncStateMachine.MoveNext() { try { if (this.<>1__state != -3) ; } catch (Exception ex) { this.<>1__state = -2; this.<>t__builder.SetException(ex); return; } this.<>1__state = -2; this.<>t__builder.SetResult(); } [DebuggerHidden] void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0) { this.<>t__builder.SetStateMachine(param0); } }
Т.е. очень много сгенерированного кода только для того что бы защитить меня от явно возвращаемого Task’a!
Третий вариант генерирует код лямбда-выражения, а затем передает его в Task. Затем Task передается планировщику и то, что происходит дальше, зависит от вашего планировщика. Хотя этот вариант легче по кодогенерации, процесс “hand-off” делает его самым медленным.
Производительность
Я запустил несколько контрольных измерений по этими трем реализациям. В каждом случае метод срабатывал 100000 раз. Первая реализация отработала где-то за 2 мс, вторая — за 15 мс и третья — за 170 мс. Как я уже и говорил — все это всего лишь микро-оптимизация. Что интересно отметить, если вы повторите тест с присоединенным отладчиком — третий вариант срабатывает более чем за 30000 мс! Я предполагаю, что еще несколько переключений происходит с присоединенным отладчиком, которые влияют на производительность
Заключение
В идеале вы захотите не делать ничего из этого. Ваш лучший вариант заключается в поддержке как синхронных и асинхронных реализаций (в случае необходимости). На практике я часто сталкиваюсь, что часто нужно обрабатывать процесс обертывания (использования Task) и так удобней, но необходимо понимать, что именно Вы просите сделать компилятор в таких случаях. Это может быть микро-оптимизацией, но это не мешает Вам сделать это наилучшим образом!
Ссылка на источник: Wrapping synchronous code in a Task returning method
Добавить комментарий