Страницы

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

четверг, 28 ноября 2019 г.

Почему создавать асинхронные “обертки” для синхронных методов считается неправильным?

#c_sharp #.net #async_await


Не могу понять, чем плох подобный код:

public ICollection GetAllProducts() { //getting data from DB }

public async Task> GetAllProductsAsync()
{
    return await Task>.Factory.StartNew(GetAllProducts)
}

    


Ответы

Ответ 1



Я не буду отвечать от себя, а просто приведу слова разработчиков .NET. В концов концов кто как не они являются истиной в последней инстанции о том, как что-то должно работать. Ниже очень краткое содержимое поста "Should I expose asynchronous wrappers for synchronous methods?" от разработчиков async/await и создателей TAP. Знаете язык -- читайте, не знаете -- переводите Гуглом. Но обязательно читайте. Асинхронность Два главных преимущества асинхронности -- это масштабируемость и разгрузка основного потока (отзывчивость UI, параллелизм). Разным типам приложений, как правило, нужны разные преимущества: серверным приложениям нужна масштабируемость, UI приложениям -- разгрузка. Масштабируемость Возможность запускать синхронные методы как асинхронные не увеличивает масштабируемость, поскольку количество затрачиваемых ресурсов остается тем же самым -- вместо текущего потока выполняется в другом потоке. Масштабируемость приложений достигается за счет того, что при правильной асинхронности те же самые ресурсы выполняют бОльшее количество работы. Разгрузка В случае с повышением отзывчивости UI и запуском кода в параллельных потоках оборачивание синхронного кода в асинхронный -- это то, что нужно. Дискуссия Команда разработчиков .NET в Task-based Asynchronous Pattern не рекомендует держать в API асинхронные методы, которые просто оборачивают синхронные. Как уже было сказано, для того, чтобы улучшить масштабируемость, код должен быть действительно асинхронным -- т.е. исходный синхронный код нужно переделывать. В случае разгрузки -- исходный синхронный код не нуждается в переделках. При этом если вы предоставляете в API только синхронную версию, вы получаете некоторые бонусы: Меньше методов -- меньше головной боли. Разработчикам нужно меньше сопровождать и тестировать. Пользователям API -- меньше мучаться выбором, какой же метод использовать в каждом конкретном случае. По гайдлайну все содержащиеся в API методы должны быть по-настоящему асинхронными. Т.е. видя асинхронный метод в API, пользователь API должен быть уверен, что его использование улучшит масштабируемость. Выбор того, запускать ли синхронный метод асинхронным образом, остается за пользователем. Если он может "потерпеть"/позволяют обстоятельства -- он запустит метод синхронно. Если ему нужна будет отзывчивость -- он запустит метод асинхронно. Главный вывод: API не должен врать и вводить в заблуждение. От себя лишь скажу, что пока пишешь программы для себя, или пока пишешь код, который больше никто не использует, эти доводы могут показаться пустыми. Но поверьте, при серьезной разработке в команде, а тем более при создании библиотек, которыми пользуются множество разработчиков, эти правила оказываются очень полезными. Также стоит добавить, что речь в посте выше идет о публичном API. Степень публичность может быть различна -- это может быть библиотека, которую устанавливают сотнями с нугета, или это могут быть ваши коллеги, которые пользуются разработанным вами компонентом. Важно лишь, чтобы публичный API не вводил в заблуждение. Утверждение "async over sync -- это плохо" ни в коей мере не относится ко всему коду. Потому что, например, если в полностью синхронном коде начинают использовать асинхронные методы, то понятно, что пока приложение полностью не будет "пронизано" async/await, в коде будут встречаться "стыки": асинхронные обертки синхронных методов. Это нормально, это рабочий процесс. Важно понимать, что async over sync должен являться либо вынужденной мерой (в случае разгрузки), либо временной мерой (в случае смешения), но не нормой. Дело в том, что любая вещь (хоть фича языка, хоть бытовой прибор) должна быть сделана так, что бы ей было легко пользоваться правильно и сложно пользоваться неправильно. К сожалению, основное достоинство async/await -- легкое написание асинхронного кода -- является одновременно и его недостатком. Например потому, что можно легко предоставить асинхронную обертку для синхронного кода. Это может привести к тому, что разработчик может в пару к любому синхронному методу предоставлять асинхронную обертку. Если же вспомнить предыдущие шаблоны асинхронного программирования в .NET -- Asynchronous Programming Model (методы BeginXXX/EndXXX) и Event-based Asynchronous Pattern (метод XXXAsync и событие XXXCompleted) -- то в них значительно сложнее сделать асинхронную обертку для синхронного метода. И следующей после мысли "А давай-ка я сделаю еще и асинхронный вариант" будет мысль "Да ну его, кому надо, сами сделают". Т.е. эти модели заставляли хорошенько подумать, а действительно ли нужна асинхронная обертка. При использование async/await этот барьер отсутствует, поэтому разработчики фичи вынуждены об этом активно говорить и напоминать.

Ответ 2



Как минимум тем, что тратятся лишние ресурсы. Например, Windows поддерживает операции асинхронного ввода-вывода, поэтому для ожидания куска данных не надо создавать новый поток и отправлять его в спячку - система сама уведомит о завершении чтения. Аналогично может быть и с другими операциями. Если ты предоставляешь async-версию, то от неё ожидается более оптимальное использование ресурсов, чем от обычной. В конце концов, вызывающий код может сам запустить обычную операцию через StartNew. И, если ему надо 10 таких операций, он их запусти всей пачкой, а не за 10 раз. И не потратит на побочную работу в 10 раз больше, чем при том, что каждый async-метод делает StartNew.

Ответ 3



async/await, в зависимости от типа приложения, даёт два разных бонуса: В Desktop (WPF/Winforms) этот механизм позволяет перенести часть работы в фоновый поток. UI поток в этом случае - ценный ресурс, и асинхронность путем запуска фонового потока вполне имеет смысл. Но в таком случае проще написать весь вызываемый код синхронно, а обёртки сделать на самом верху. в ASP.NET UI потока нет. Код обработки запроса выполняется на потоке из пула. Ваш код освобождает этот поток (при условии что на самом верху у вас используется асинхронный контроллер). Но при этом выедает еще один поток из пула для выполнения операции. Т.е. ваш код как использовал поток из пула, так и использует. Но теперь вы платите дополнительные расходы на "асинхронность". В случае ASP.NET может возникнуть соблазн запустить таким образом два обращения к базе в рамках одного потока (ака "распараллелить"). Вот только проблема в том, что в ASP.NET потоки и ядра - это ценный ресурс. Если серверу для выполнения запроса придётся загрузить два ядра - то он сможет обработать в два раза меньше одновременных запросов. Т.е. на девелоперский машине вы получите ускорение, а на сервере под нагрузкой - нет. При правильном использовании - протягивании асинков от сетевых или дисковых операций - async/await позволит вам сэкономить потоки. При неправильном - наоборот, наоборот, потратит их. Т.е. могут быть случаи, когда от обертывания будет значительный выигрыш, но делать это надо очень аккуратно - иначе вы получите совершенно противоположный ожидаемому эффект.

Ответ 4



//getting data from DB за этим комментарием вероятнее всего стоит работа с сетью или файлом, данные операции следует выполнять асинхронно. Автор кода делает асинхронную обертку над синхронным методом, который по природе должен быть асинхронным. Допускаю единственный сценарий, почему так иногда случается: интерфейс для запроса к БД синхронный, а интерфейс, который реализует автор, требует асинхронной сигнатуры. Даже если так, то логичнее выглядит public Task> GetAllProductsAsync() { return Task.FromResult(GetAllProducts()); }

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

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