Выявление синхронных и асинхронных версий одного и того же метода

Вопрос или проблема

У меня есть метод со следующим сигнатурой метода (.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);
}

Здесь два вопроса:

  1. Правильно ли реализован третий метод (для вызова async версии при блокировке текущего потока), или мне следует остерегаться каких-либо подводных камней?
  2. В дополнение ко второму и третьему методам выше, есть ли лучший способ предоставить как синхронные, так и асинхронные версии таких методов без дублирования логики?

Ответ или решение

Ответ на вопрос о синхронных и асинхронных версиях одного метода

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, что минимизирует риски.

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

Оцените материал
Добавить комментарий

Капча загружается...