it-roy-ru.com

Вызов асинхронных методов из неасинхронного кода

Я нахожусь в процессе обновления библиотеки, которая имеет поверхность API, которая была встроена в .NET 3.5. В результате все методы являются синхронными. Я не могу изменить API (то есть преобразовать возвращаемые значения в Task), потому что это потребует изменения всех вызывающих. Так что я остался с тем, как лучше всего вызывать асинхронные методы синхронно. Это в контексте консольных приложений ASP.NET 4, ASP.NET Core и .NET/.NET Core.

Возможно, я не был достаточно ясен - ситуация в том, что у меня есть существующий код, который не поддерживает асинхронность, и я хочу использовать новые библиотеки, такие как System.Net.Http и AWS SDK, которые поддерживают только асинхронные методы. Поэтому мне нужно преодолеть разрыв и иметь возможность иметь код, который можно вызывать синхронно, но затем можно вызывать асинхронные методы в другом месте.

Я много читал, и много раз это задавали и отвечали.

Вызов асинхронного метода из неасинхронного метода

синхронно ожидает асинхронную операцию и почему Wait () останавливает программу здесь

вызов асинхронного метода из синхронного метода

Как бы я синхронно запускал метод асинхронной задачи <T>?

синхронный вызов асинхронного метода

Как вызвать асинхронный метод из синхронного метода в C #?

Проблема в том, что большинство ответов разные! Наиболее распространенный подход, который я видел, это использование .Result, но это может привести к тупику. Я перепробовал все следующее, и они работают, но я не уверен, что это лучший способ избежать взаимоблокировок, иметь хорошую производительность и хорошо играть со временем выполнения (с точки зрения соблюдения планировщиков задач, параметров создания задач и т.д.). ). Есть ли окончательный ответ? Каков наилучший подход?

private static T taskSyncRunner<T>(Func<Task<T>> task)
    {
        T result;
        // approach 1
        result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 2
        result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 3
        result = task().ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 4
        result = Task.Run(task).Result;

        // approach 5
        result = Task.Run(task).GetAwaiter().GetResult();


        // approach 6
        var t = task();
        t.RunSynchronously();
        result = t.Result;

        // approach 7
        var t1 = task();
        Task.WaitAll(t1);
        result = t1.Result;

        // approach 8?

        return result;
    }
54
Erick T

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

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

Не обязательно быть полностью асинхронным для правильности. Блокировка чего-либо асинхронного для его синхронизации приводит к снижению производительности, которое может иметь значение или быть совершенно неактуальным Это зависит от конкретного случая.

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

Здесь все ваши вызовы .ConfigureAwait(false) бессмысленны, потому что вы не ожидаете.

RunSynchronously недопустимо использовать, потому что не все задачи могут быть обработаны таким образом.

.GetAwaiter().GetResult() отличается от Result/Wait() тем, что имитирует поведение распространения исключений await. Вам нужно решить, хотите вы этого или нет. (Так что исследуйте, что это за поведение; нет необходимости повторять это здесь.)

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

Мне лично нравится шаблон Task.Run(() => DoSomethingAsync()).Wait();, потому что он категорически избегает взаимоблокировок, прост и не скрывает некоторые исключения, которые может скрывать GetResult(). Но вы также можете использовать GetResult() с этим.

66
usr

Я нахожусь в процессе обновления библиотеки, которая имеет поверхность API, которая была встроена в .NET 3.5. В результате все методы являются синхронными. Я не могу изменить API (то есть преобразовать возвращаемые значения в Task), потому что это потребует изменения всех вызывающих. Так что я остался с тем, как лучше всего вызывать асинхронные методы синхронно.

Не существует универсального "лучшего" способа для выполнения анти-паттерна "синхронизация по асинхронности". Только множество хаков, у каждого из которых есть свои недостатки.

Я рекомендую вам сохранить старые синхронные API, а затем ввести асинхронные API вместе с ними. Вы можете сделать это, используя "взлом логических аргументов", как описано в моей статье MSDN о Brownfield Async .

Сначала короткое объяснение проблем с каждым подходом в вашем примере:

  1. ConfigureAwait имеет смысл только когда есть await; в противном случае это ничего не делает.
  2. Result обернет исключения в AggregateException; если вам нужно заблокировать, используйте вместо этого GetAwaiter().GetResult().
  3. Task.Run выполнит свой код в потоке пула потоков (очевидно). Это хорошо , только если код может выполняться в потоке пула потоков.
  4. RunSynchronously - это расширенный API, используемый в крайне редких ситуациях при выполнении динамического параллелизма на основе задач. Вы вообще не в этом сценарии.
  5. Task.WaitAll с одной задачей - это то же самое, что и Wait().
  6. async () => await x - просто менее эффективный способ сказать () => x.
  7. Блокировка задачи, запущенной из текущего потока может вызвать взаимные блокировки .

Вот разбивка:

// Problems (1), (3), (6)
result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (1), (3)
result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (1), (7)
result = task().ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (2), (3)
result = Task.Run(task).Result;

// Problems (3)
result = Task.Run(task).GetAwaiter().GetResult();

// Problems (2), (4)
var t = task();
t.RunSynchronously();
result = t.Result;

// Problems (2), (5)
var t1 = task();
Task.WaitAll(t1);
result = t1.Result;

Вместо любого из этих подходов, поскольку у вас есть существующий, работающий синхронный код , вы должны использовать его вместе с более новым естественно-асинхронным кодом. Например, если ваш существующий код использовал WebClient:

public string Get()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}

и вы хотите добавить асинхронный API, тогда я бы сделал это так:

private async Task<string> GetCoreAsync(bool sync)
{
  using (var client = new WebClient())
  {
    return sync ?
        client.DownloadString(...) :
        await client.DownloadStringTaskAsync(...);
  }
}

public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();

public Task<string> GetAsync() => GetCoreAsync(sync: false);

или, если вы должны использовать HttpClient по какой-то причине:

private string GetCoreSync()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}

private static HttpClient HttpClient { get; } = ...;

private async Task<string> GetCoreAsync(bool sync)
{
  return sync ?
      GetCoreSync() :
      await HttpClient.GetString(...);
}

public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();

public Task<string> GetAsync() => GetCoreAsync(sync: false);

При таком подходе ваша логика будет использовать методы Core, которые могут выполняться синхронно или асинхронно (как определено параметром sync). Если sync - true, то основные методы должны возвращать уже выполненную задачу. Для реализации используйте синхронные API для синхронной работы и асинхронные API для асинхронной работы.

В конце концов, я рекомендую отказаться от синхронных API.

26
Stephen Cleary