Стратегии асинхронного вызова синхронных сервисов в C # - PullRequest
7 голосов
/ 28 сентября 2010

С бизнес-логикой, инкапсулированной в синхронных сервисных вызовах, например ::10000 *

interface IFooService
{
    Foo GetFooById(int id);
    int SaveFoo(Foo foo);
}

Каков наилучший способ расширения / использования этих сервисных вызовов в асинхронном режиме ?

В настоящее время я создал простой класс AsyncUtils:

public static class AsyncUtils
{
    public static void Execute<T>(Func<T> asyncFunc)
    {
        Execute(asyncFunc, null, null);
    }

    public static void Execute<T>(Func<T> asyncFunc, Action<T> successCallback)
    {
        Execute(asyncFunc, successCallback, null);
    }

    public static void Execute<T>(Func<T> asyncFunc, Action<T> successCallback, Action<Exception> failureCallback)
    {
        ThreadPool.UnsafeQueueUserWorkItem(state => ExecuteAndHandleError(asyncFunc, successCallback, failureCallback), null);
    }

    private static void ExecuteAndHandleError<T>(Func<T> asyncFunc, Action<T> successCallback, Action<Exception> failureCallback)
    {
        try
        {
            T result = asyncFunc();
            if (successCallback != null)
            {
                successCallback(result);
            }
        }
        catch (Exception e)
        {
            if (failureCallback != null)
            {
                failureCallback(e);
            }
        }
    }
}

Что позволяет мне вызывать что-либо асинхронно:

AsyncUtils(
     () => _fooService.SaveFoo(foo),
     id => HandleFooSavedSuccessfully(id),
     ex => HandleFooSaveError(ex));

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

Опции, о которых я до сих пор думал, включают:

  • с AsyncUtils возвращает WaitHandle
  • если AsyncUtils использует AsyncMethodCaller и возвращает IAsyncResult
  • переписывает API для включения Begin, End async call

например. что-то похожее:

interface IFooService
{
    Foo GetFooById(int id);
    IAsyncResult BeginGetFooById(int id);
    Foo EndGetFooById(IAsyncResult result);
    int SaveFoo(Foo foo);
    IAsyncResult BeginSaveFoo(Foo foo);
    int EndSaveFoo(IAsyncResult result);
}

Есть ли другие подходы, которые я должен рассмотреть? Каковы преимущества и потенциальные подводные камни каждого?

В идеале мне бы хотелось, чтобы уровень обслуживания был простым / синхронным, и чтобы было несколько простых в использовании утилит для вызова их асинхронно. Мне было бы интересно услышать о решениях и идеях, применимых к C # 3.5 и C # 4 (мы еще не обновились, но сделаем это в относительно ближайшем будущем).

Жду ваших идей.

Ответы [ 3 ]

3 голосов
/ 28 сентября 2010

Учитывая ваше требование оставаться только .NET 2.0, а не работать на 3.5 или 4.0, это, вероятно, лучший вариант.

У меня есть три замечания по поводу вашей текущей реализации.

  1. Есть ли конкретная причина, по которой вы используете ThreadPool.UnsafeQueueUserWorkItem ?Если нет особой причины, по которой это требуется, я бы рекомендовал использовать ThreadPool.QueueUserWorkItem , особенно если вы работаете в большой группе разработчиков.Небезопасная версия может потенциально допускать появление уязвимостей безопасности при потере вызывающего стека и, как следствие, возможность максимально точно контролировать разрешения.

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

  3. В настоящее время у вас нет возможности определить, является ли асинхронный вызоввыполнен.Это было бы другим преимуществом использования IAsyncResult в вашем проекте (хотя это добавляет сложности к реализации).


После обновления до.NET 4, однако, я бы порекомендовал просто поместить это в Task или Task<T>, так как он был разработан, чтобы обрабатывать это очень аккуратно.Вместо:

AsyncUtils(
     () => _fooService.SaveFoo(foo),
     id => HandleFooSavedSuccessfully(id),
     ex => HandleFooSaveError(ex));

Вы можете использовать встроенные инструменты и просто написать:

var task = Task.Factory.StartNew( 
                () => return _fooService.SaveFoo(foo) );
task.ContinueWith( 
                t => HandleFooSavedSuccessfully(t.Result),
                    TaskContinuationOptions.NotOnFaulted);
task.ContinueWith( 
                t => try { t.Wait(); } catch( Exception e) { HandleFooSaveError(e); },
                    TaskContinuationOptions.OnlyOnFaulted );

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

2 голосов
/ 28 сентября 2010

Асинхронный интерфейс (на основе IAsyncResult) полезен только тогда, когда у вас есть какой-то неблокирующий вызов под прикрытием.Основная задача интерфейса - сделать возможным выполнение вызова без блокировки потока вызывающей стороны.

  • Это полезно в тех случаях, когда вы можете сделать какой-то системный вызов, и система сообщит вам, когда что-то произойдет (например, когда получен ответ HTTP или когда произойдет событие).

  • Цена за использование интерфейса на основе IAsyncResult заключается в том, что вы должны писать код несколько неловко (делая каждый вызов с использованием обратного вызова).Хуже того, асинхронный API делает невозможным использование стандартных языковых конструкций, таких как while, for или try .. catch.

Я не вижуточка включения синхронного API в асинхронный интерфейс, потому что вы не получите преимущества (всегда будет какой-то поток заблокирован), и вы просто получите более неловкий способ вызоваit.

Конечно, имеет смысл как-то запустить синхронный код в фоновом потоке (чтобы не блокировать основной поток приложения).Или Task<T> в .NET 4.0 или QueueUserWorkItem в .NET 2.0.Тем не менее, я не уверен, следует ли делать это автоматически в службе - такое ощущение, что делать это на стороне вызывающего абонента будет проще, поскольку вам может потребоваться выполнить несколько вызовов службы.Используя асинхронный API, вы должны написать что-то вроде:

svc.BeginGetFooId(ar1 => {
  var foo = ar1.Result; 
  foo.Prop = 123;
  svc.BeginSaveFoo(foo, ar2 => { 
    // etc...
  }
});

При использовании синхронного API вы будете писать что-то вроде:

ThreadPool.QueueUserWorkItem(() => {
  var foo = svc.GetFooId();
  foo.Prop = 123;
  svc.SaveFoo(foo);
});
1 голос
/ 28 сентября 2010

Ниже приводится ответ на дополнительный вопрос Рида. Я не предполагаю, что это правильный путь.

    public static int PerformSlowly(int id)
    {
        // Addition isn't so hard, but let's pretend.
        Thread.Sleep(10000);
        return 42 + id;
    }

    public static Task<int> PerformTask(int id)
    {
        // Here's the straightforward approach.
        return Task.Factory.StartNew(() => PerformSlowly(id));
    }

    public static Lazy<int> PerformLazily(int id)
    {
        // Start performing it now, but don't block.
        var task = PerformTask(id);

        // JIT for the value being checked, block and retrieve.
        return new Lazy<int>(() => task.Result);
    }

    static void Main(string[] args)
    {
        int i;

        // Start calculating the result, using a Lazy<int> as the future value.
        var result = PerformLazily(7);

        // Do assorted work, then get result.
        i = result.Value;

        // The alternative is to use the Task as the future value.
        var task = PerformTask(7);

        // Do assorted work, then get result.
        i = task.Result;
    }
...