Должна ли асинхронная лямда работать в асинхронной функции? - PullRequest
0 голосов
/ 02 ноября 2018

Итак, у меня есть следующий код с несколькими другими методами, отличными от Set или Delete, но я буду сокращать его для простоты:

public byte[] Get(string key)
{
    byte[] Action() => this.Cache.Get(key);
    return this.Execute(Action);
} 

public void Delete(string key)
{
    void Action() => this.Cache.Delete(key);
    return this.Execute(Action);
}

private void Execute(Action action)
{
    this.Execute(() =>
    {
        action();
        return 0;
    });
}

private T Execute<T>(Func<T> action)
{
    if (someCondition)
    {
        try
        {
            return action();
        }
        catch (Exception)
        {
            //do something
        }
    }
    else
    {
        //do something else
    }

    return default(T);
}

Теперь я хочу сделать этот код асинхронным. Я пытался сделать:

 public async Task<byte[]> GetAsync(string key)
 {
     async Task<byte[]> Action() => await this.Cache.GetAsync(key);
     return await this.Execute(Action);
 }

public async Task DeleteAsync(string key)
{
    void Action() => this.Cache.DeleteAsync(key);
    await this.Execute(Action);
}


private void Execute(Action action)
{
    this.Execute(() =>
    {
        action();
        return 0;
    });
}

private T Execute<T>(Func<T> action)
{
    if (someCondition)
    {
        try
        {
            return action();
        }
        catch (RedisConnectionException)
        {
            //do something
        }
    }
    else
    {
        // do something else
    }

    return default(T);
}

Он компилируется и, кажется, работает, но я понятия не имею, действительно ли он асинхронный или нет. Кажется странным, что метод Execute не является асинхронным и не возвращает задачу. Я не нашел способ сделать метод Execute асинхронным без получения ошибок и предупреждений. Итак, мой вопрос: во второй версии кода, будет ли выполнение действия

return action();

быть асинхронным или синхронизировать?

Бонусный вопрос: есть ли способ проверить, работает ли что-то асинхронно или нет? Способ, которым я мог вручную проверить "асинхронность" кода

1 Ответ

0 голосов
/ 02 ноября 2018

Итак, первое, на что нужно обратить внимание, - это то, что метод, являющийся асинхронным, не означает «у него есть ключевое слово async». Метод является асинхронным, когда он очень быстро возвращается к вызывающей стороне, а затем завершает любую операцию, которая была ему запрошена , после возврата к вызывающей стороне и позволяет вызывающей стороне продолжать делать все, что он хочет. Обычно это также позволяет вызывающему абоненту узнать, когда операция завершилась, была ли она успешной или нет, и иногда включить какой-либо результат операции.

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

Имея это в виду, мы можем просмотреть ваши методы и посмотреть, что они делают, и если они асинхронные.

public async Task<byte[]> GetAsync(string key)
{
    async Task<byte[]> Action() => await this.Cache.GetAsync(key);
    return await this.Execute(Action);
}

Итак, давайте сначала рассмотрим только внутренний метод:

async Task<byte[]> Action() => await this.Cache.GetAsync(key);

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

Task<byte[]> Action() => this.Cache.GetAsync(key);

Теперь, когда мы посмотрим на метод, он находится в:

public async Task<byte[]> GetAsync(string key)
{
    return await this.Execute(Action);
}

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

Теперь перейдем к следующему методу, опять же, сначала посмотрим на внутренний метод:

void Action() => this.Cache.DeleteAsync(key);

Здесь мы вызываем асинхронный метод, но мы не возвращаем Task, который он нам дает. Это означает, что у нас нет возможности узнать, когда операция закончится или она прошла успешно. Поскольку DeleteAsync является асинхронным (или мы можем предположить, учитывая имя), мы знаем, что этот метод вернётся, как только запустил асинхронную операцию, а не после выполнения основной операции.

public async Task DeleteAsync(string key)
{
    await this.Execute(Action);
}

Это не компилируется. Action вот метод, который возвращает void, поэтому вы вызываете перегрузку Execute, которая возвращает void, и вы не можете await a void выражение. Если вы изменили код на:

public async Task DeleteAsync(string key)
{
    Task Action() => this.Cache.DeleteAsync(key);
    await this.Execute(Action);
}

Затем он будет скомпилирован, потому что вы будете вызывать версию Execute, которая принимает Func<T> и возвращает результат, так что вы сможете дождаться этой задачи, но, как мы видели из более ранние методы, ожидающие его, не делают ничего полезного, кроме как добавить некоторые накладные расходы, мы могли бы просто вернуть задачу и покончить с ней.

private void Execute(Action action)
{
    this.Execute(() =>
    {
        action();
        return 0;
    });
}

Если мы внесем указанное выше изменение, оно никогда не будет вызвано, поскольку мы никогда не передадим делегат, который возвращает void.

private T Execute<T>(Func<T> action)
{
    if (someCondition)
    {
        try
        {
            return action();
        }
        catch (RedisConnectionException)
        {
            //do something
        }
    }
    else
    {
        // do something else
    }

    return default(T);
}

Здесь все усложняется. В приведенных выше примерах оба метода возвращают что-то , поэтому эта перегрузка вызывается. Обе эти вещи являются задачами своего рода. Таким образом, этот код будет просто проверять условие и переходить к «// делать что-то еще», если оно ложно, как в синхронной версии. К сожалению, затем он будет возвращать значение по умолчанию, которое для Task равно null. Это, наверное, плохо. Когда эта задача будет возвращена, кто-то, вероятно, когда-нибудь await выполнит ее, и тогда они просто получат исключение нулевой ссылки. Вызывающие, вероятно, хотят, чтобы происходило здесь, чтобы получить Task<T>, результатом которого является значение по умолчанию.

Если условие истинно, хотя оно вызовет асинхронный метод, вычислите задачу, представляющую результаты этой операции, всякий раз, когда она завершается, и возвращайте ее. Соответственно, если в какой-то момент операция завершается с ошибкой и возвращает ошибочную задачу , ваш блок catch не будет запускаться . Блок catch будет работать только в том случае, если action выдает исключение вместо возврата сбойной задачи. (Чего не делает большинство асинхронных методов. В частности, любой async метод никогда не сделает это.)

Переписать Execute для получения ExecuteAsync verion довольно просто. Вместо того, чтобы принимать функции, которые возвращают некоторый результат, или void, вам нужно принимать функции которые возвращают Task<T> или Task, и вместо этого возвращают Task<T> или Task. Кроме этого, единственное, что нужно сделать, это await любая задача в любое время, когда вы не хотите, чтобы остальная часть кода не выполнялась, пока эта задача не будет завершена:

private Task<T> ExecuteAsync<T>(Func<Task<T>> action)
{
    if (someCondition)
    {
        try
        {
            return await action();
        }
        catch (RedisConnectionException)
        {
            //do something
        }
    }
    else
    {
        // do something else
    }

    return default(T);
}

А затем перегрузка для метода без результата в его задаче:

private Task ExecuteAsync(Func<Task> action)
{
    return this.ExecuteAsync(async () =>
    {
        await action();
        return 0;
    });
}
...