Вопрос или проблема
У меня есть метод со следующим сигнатурой метода (.NET 8, C# 12):
public static async Task<Model> GetModelFromSchemaAsync (SqlConnection connection, CancellationToken cancellationToken = default)
{
var model = new Model();
using (var dataTable = await connection.GetSchemaAsync("MetaDataCollections", cancellationToken))
{ ... /* Логика */ ...}
return (model);
}
Я хочу предоставить синхронную версию этого метода следующим образом:
public static Model GetModelFromSchema (SqlConnection connection)
{
var model = new Model();
using (var dataTable = connection.GetSchema("MetaDataCollections", cancellationToken))
{ ... /* Логика */ ...}
return (model);
}
Часто я сталкиваюсь с необходимостью делать это при написании библиотек (хоть и для собственного использования). Я пишу свои собственные генераторы кода, поэтому предоставление как синхронных
, так и асинхронных
версий таких методов важно для моей ситуации. С увеличением количества методов и строк кода также увеличивается дублирование логики.
Я видел примеры людей, использующих следующий подход:
public static Model GetModelFromSchema (SqlConnection connection)
{
// Асинхронный метод вызывается синхронно (без `await`).
var task = MsSqlServerAdoSchema.GetSchemaAsync (connection, CancellationToken.None);
var model = task.GetAwaiter().GetResult();
return (model);
}
Здесь два вопроса:
- Правильно ли реализован третий метод (для вызова
async
версии при блокировке текущего потока), или мне следует остерегаться каких-либо подводных камней? - В дополнение ко второму и третьему методам выше, есть ли лучший способ предоставить как
синхронные
, так иасинхронные
версии таких методов без дублирования логики?
Ответ или решение
Ответ на вопрос о синхронных и асинхронных версиях одного метода
1. О реализации третьего метода
В представленной реализации третьего метода, который вызывает асинхронный метод синхронно с использованием GetAwaiter().GetResult()
, есть несколько нюансов, на которые стоит обратить внимание:
-
Синхронное блокирование: Использование
GetAwaiter().GetResult()
блокирует текущий поток до завершения задачи. Это может привести к блокировке потоков, особенно если асинхронный код вызываетTask.Run
или другой асинхронный метод, который зависит от контекста синхронизации, оставленного в рабочем потоке. В результате может возникнуть deadlock (висячая блокировка). -
Исключения: Исключения, возникающие в асинхронном методе, будут обернуты в
AggregateException
, когда используешьGetResult()
. Это может усложнить обработку ошибок. -
Производительность: Синхронный вызов асинхронного метода приведет к потере преимуществ асинхронного программирования, таких как более эффективное использование ресурсов.
Таким образом, хотя ваш метод и работает, его использование потенциально небезопасно и может привести к проблемам в многопоточной среде.
2. Альтернативное решение без дублирования логики
Для того чтобы избежать дублирования логики между синхронными и асинхронными версиями методов, лучше использовать общий вспомогательный метод с реализацией общего поведения. Приведем пример:
private static Model ConstructModelFromDataTable(DataTable dataTable)
{
var model = new Model();
// Ваша логика обработки dataTable для модели.
return model;
}
public static async Task<Model> GetModelFromSchemaAsync(SqlConnection connection, CancellationToken cancellationToken = default)
{
using (var dataTable = await connection.GetSchemaAsync("MetaDataCollections", cancellationToken))
{
return ConstructModelFromDataTable(dataTable);
}
}
public static Model GetModelFromSchema(SqlConnection connection)
{
var task = GetModelFromSchemaAsync(connection, CancellationToken.None);
return task.GetAwaiter().GetResult();
}
В этом примере:
- Весь код, который обрабатывает
DataTable
и создает объектModel
, вынесен в отдельный методConstructModelFromDataTable()
. Это позволяет избежать дублирования логики между синхронным и асинхронным методами. - При синхронной версии вызывается асинхронный метод, и, хотя это может быть рискованно (как упоминалось ранее), это делается только с использованием
CancellationToken.None
, что минимизирует риски.
Таким образом, вы можете создавать и поддерживать обе версии метода, избегая дублирования кода и обеспечивая единообразие в логике обработки данных. Однако, для большего контроля и избегания блокировок, рекомендуется переосмыслить необходимость предоставления синхронной версии, если это возможно в контексте вашего приложения.