Асинхронное программирование, Часть 1

Поговорим о написании Concurrent кода. На мысль о том, что народу может быть интересно меня натолкнуло последнее собеседование, где кандидат, который в принципе не имел нормальной базы в данной области, был не в состоянии аргументировать принимаемые им при разработке приложений решения. Его знания строились на некоторых предположениях, домыслах и естественно на шишках которые ему пришлось набить, но все это не гарантирует понимания и фундаментальных знаний. Обычный путь, после нахождения некого обходного пути – это решение превозносится как единственно правильное и безоговорочно верное. Конечно никто из нас не идеален и каждый из нас, может легко признать нехватку знаний относительно различных областей в программировании, однако я посчитал, что если я могу поделиться своим небольшим опытом, то другие возможно захотят поделиться своим. Наверное многие из вас слышали об АРМ (Asynchronous Programming Model) это один из известных паттернов для реализации выполнения ассинхронного кода на платформе .NET. Конкретно о нем мы поговорим в следующий раз, а сегодня мы поговорим о возможной альтернативе, другой модели ассинхронного выполнения которая построена на использовании событийной модели. События тут используются для уведомления вызывающего кода. В самом простом случае это код который выполняется в создаваемом нами дополнительном потоке и по окончании выполнения уведомляющий родительский поток об окончании своей работы, в следствии чего родительский поток имеет возможность воспользоваться результатами труда потока дочернего. Особо любопытные могут поинтересоваться о том, какую именно модель стоит использовать при разработке их приложений ту о которой будем говорить или APM? Я считаю, что это не просто правильный вопрос он более, чем важен для понимания того, что мы используем и как мы пишем наш код. Однозначно верного ответа нет, каждая модель имеет свои преимущества и недостатки, но обобщенное правило все же существует. Если клиент твоего кода библиотека то обычно используется APM, если графический интерфейс то вероятно правильный выбор будет реализация ассинхронных вызовов основанных на событийной модели. Типичным представителем рассматриваемой нами модели есть общеизвестный BackgroundWorker класс. Сколько же времени он сэкономил нам!? Мне много, он выручал меня и в те времена когда многопоточное программирование было для меня тайной за семью замками, он же облегачал мою работу еще много и много раз и я уверен еще не раз выручит. Но суть разговора не будет сводиться к BackgroundWorker это было бы унизительно для нас с Вами мы умеем им пользоваться, Мы же поговорим о том, как правильно писать свой код реализуя классы подобные классу BackgroundWorker. Я не стану останавливаться на проблемах синхронизации cross-thread и GUI операций – сегодня коснемся их вскользь, мы же посвятим свое время написанию реально полезного кода, который продемонстрирует на практике использование описываемого мной паттерна, а также поможет лучше понять, что может предложить нам в помощь .NET Framework в частности и как нужно стараться писать свой код. Приступим!? Начнем с рассмотрения довольно типичной задачи, которая возникает перед программистом, - инициализация формы данными получение которых требует значительного времени. Довольно типична ситуация когда мы видим приложения или формы которые, как бы подвисают в момент выполнения некоторых, запрашиваемых пользователем действий. Что бы понять почему так происходит, достаточно просто понимать, что GUI в Windows всегда выполняется в единственном ассоциированном с ним потоке. Каждая презентационная технология в рамках .NET реализует специальный внутренний менеджер который неустанно проверяет и пресекает все наши попытки работать с GUI из других потоков. Мы можем иметь много потоков, но взаимодействовать с GUI можем только в одном единственном. Можно долго рассуждать почему это именно так и кто в этом виноват, но это данность с которой мы вынуждены мириться. Могу только заметить, что это не всегда плохо и такая модель предлагает некоторые бенефиты. Так вот как только приложение выполняет код который занимает какое либо значимое время из потока GUI, GUI перестает обрабатывать все поступающие ему сообщения, выполнение потока по сути приостанавливается до конца выполнения этого кода, что приводит к отсутствию перерисовки, «подвисанию» и появлению различных артефактов. Далеко за примером я ходить не буду. Думаю каждый из нас хоть раз да пользовался Data Connection – Add Connection мастером в студийном Server Explorer, после попытки выбрать сервер из комбобокса, окно уходит в «даун», а если попытаться подергать хидер появляется устрашающая надпись о том, что приложение не отвечает на запросы … Конечно мы знаем с вами, что это не так и в конце концов нужный код выполниться и мы сможем выбрать нужный нам сервер из списка. Причина происходящего думаю для Вас уже очевидно, в момент опроса сети на наличие экземпляров было «заморожено» выполнение потока GUI и приложение не было способно сделать, что ли бо полезное. Мне не нравятся такие приложения, а Вам? Гораздо лучше, стараться держать поток GUI не загруженным. С точки зрения идеалиста, можно было бы сказать, что идеальный GUI выполняет в GUI потоке исключительно обращения к GUI элементам (чтение, запись) и синхронизацию, весь же остальной код должен был бы выполняться в параллельных потоках. Мир не идеален, но мы можем постараться сделать так, что бы он хотя бы чуть чуть походил на такой. Что бы решить вышеприведенную проблему. Достаточно код который должен опросить сеть выполнить в независимом от GUI потоке, потом сообщить основному потоку, что код выполнился, передать результат выполнения кода в основной поток, произвести синхронизацию и обновить данные в контролах. Звучит устрашающе, но я помогу разобраться с первой частью проблемы, а известная статья PIL поможет разобраться с синхронизацией и обновлением. Все действительно просто.

Давайте определим сначала наше синхронное API.

    public interface ISqlServerNetworkManager
    {
        IList<NetworkInstanceDesceriptor> EnumAvailableSqlServers();
    }

Думаю очень легко понять, что должна делать реализация данного интерфейса. Она должна вернуть нам список объектов, каждый из которых опишет доступный сервер в сети. Ниже для полноты картины привожу реализацию NetworkInstanceDescriptor

    public class NetworkInstanceDesceriptor
    {
        public NetworkInstanceDesceriptor(){}
 
        public NetworkInstanceDesceriptor(string name, string server, string instance)
        {
            Name = name;
            Server = server;
            Instance = instance;
        }
 
        public string Instance { get; set; }
        public string Server { get; set; }
        public string Name { get; set; }
 
        public override string ToString()
        {
            return String.Format("Name: {0} Server: {1} - Instance: {2}", Name, Server, Instance ?? "Default");
        }
    }

Сейчас же мы имплементируем данный интерфейс. .NET предлагает несколько путей того, как мы можем выполнить перечисление серверов. Я воспользуюсь возможностями SMO. Для того, что бы код компилировался вам, нужно подключить сборку Microsoft.SqlServer.Smo в проект и добавить соответствующее пространство имен. И так реализация:

    public class SqlServerNetworkManager : ISqlServerNetworkManager
    {
        public IList<NetworkInstanceDesceriptor> EnumAvailableSqlServers()
        {
            var descriptors = new List<NetworkInstanceDesceriptor>();
            using (DataTable table = SmoApplication.EnumAvailableSqlServers())
            {
                foreach (DataRow row in table.Rows)
                    descriptors.Add(BuildNetworkDescriptor(row));
            }
 
            return descriptors;
        }
 
        private static NetworkInstanceDesceriptor BuildNetworkDescriptor(DataRow row)
        {
            return new NetworkInstanceDesceriptor(row["Name"] as string,
                                                  row["Server"] as string,
                                                  row["Instance"] as string);
        }
     }

Синхронное API реализовано. В нем нет ничего из области Rocket Since, после выполнения запроса, мы обрабатываем полученный DataTable построчно, получая на выходе нужную нам коллекцию.

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

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

void EnumAvailableSqlServersAsync()

, мы ничего прямо не возвращаем клиенту потому тип возврата void, так же наш синхронный метод не требовал аргументов, потому и в нашем случае они не требуются. Обратите внимание на окончание в названии метода - Async, это правило хорошего тона, программист который будет пользоваться нашим API, сможет сразу определить, что вызов этого метода приводит к ассинхронной операции, а следовательно, вполне возможно ему потребуется подписаться на событие, что бы получить результат работы данного метода. Предварительно отметим, что в этом методе мы и будем создавать дополнительный поток, в котором и будет вызываться наша синхронная реализация. Добрались наши руки и события. .NET Framework, предоставляет стандартный базовый класс для аргумента, который должен использоваться в данном случае это – AsyncCompletedEventArgs. Я просто настоятельно рекомендую пользоваться им. Самое любопытное, что большинство из встреченных мной программистов, не знает о существовании подобного класса. Сейчас я остановлюсь на двух важных свойствах определенных в данном типе это Canceled и Error. Первое позволяет обработчику узнать, что выполнение потока было прервано, по желанию пользователя (мы могли бы реализовать метод, который позволил бы нам отменять выполнение ассинхронной операции), второе предоставляет возможность коду, что выполняется в дочернем потоке сообщить родительскому об исключении произошедшем при его выполнении. Это важно. Это очень, очень важно…

Тут мы сделаем короткий экскурс в модель обработки исключений при работе с потоками. Проведем один небольшой эксперимент.

        static void Main(string[] args)
        {
            try
            {
                ThrowExceptionMethod(null);
            }
            catch
            {
 
            }
 
            Console.ReadLine();
        }
 
        private static void ThrowExceptionMethod(object o)
        {
            throw new Exception("Child thread exception");
        }

Мы «скушали» исключение, и программа продолжает работать. Суть в другом, мы могли не «есть» исключение, а корректно его обработать и далее восстановить нормальную работу приложения, либо же если ошибка была фатальной завершить приложение в мягкой для него форме.

Давайте вызовем, ThrowExceptionMethod из другого потока:

        static void Main(string[] args)
        {
            try
            {
                ThreadPool.QueueUserWorkItem(ThrowExceptionMethod);
            }
            catch(Exception ex)
            {
 
            }
 
            Console.ReadLine();
        }
 
        private static void ThrowExceptionMethod(object o)
        {
            throw new Exception("Child thread exception");
        }

Catch не возымел никакого действия. Теперь давайте вспомним о том, что необработанное исключение в порожденных потоках, приводят к фатальному сбою в приложении. В .NET как мы только, что убедились существуют механизмы которые мешают вам из главного потока подавить необработанные исключения в дочернем потоке. Единственное, что вам остается подписаться на AppDomain.UnhandledException, но это может оказаться полезным разве, что для логирования. Восстановить работу приложения все равно не удастся. Следовательно обработка исключений в дочернем потоке является обязательной если вы не желаете, что бы ваше приложение вываливалось с ошибкой! Что бы посмотреть на эффект обработки исключения в дочернем потоке давайте «скушаем» исключение в уже в нем.

class Program
    {
        static void Main(string[] args)
        {
            try
            {
                ThreadPool.QueueUserWorkItem(ThrowExceptionMethod);
            }
            catch(Exception ex)
            {
 
            }
 
            Console.ReadLine();
        }
 
        private static void ThrowExceptionMethod(object o)
        {
            try
            {
                throw new Exception("Child thread exception");
            }
            catch(Exception ex)
            {
 
            }
        }
    }

Приложение продолжило работать, единственное, что надо запомнить раз и на всю жизнь, что «глотать» исключения не хорошо их всегда нужно обрабатывать или пробрасывать выше, если ваш код библиотека.

Возвращаясь к AsyncCompletedEventArgs и к свойству его Error, то оно предоставляет удобный путь дочернему потоку обработать исключение и выслать его «посылкой» главному потоку. Я буду наследовать этот класс т.к. я хочу добавить возможность передавать еще дополнительно результат выполнения метода в нашем случае это IList<NetworkInstanceDesceriptor>. Параллельно хотелось бы сделать так, что бы этот наследник был более универсальным и мог использоваться с различными типами возврата при написании любого ассинхронного API, хотя решение и без Generic имеет смысл во многих кейсах, когда разрабатывается специализированные библиотеки.

    public class EnumAvailableSqlServersEventArgs<T> : AsyncCompletedEventArgs 
    {
        public EnumAvailableSqlServersEventArgs(T result, Exception error, bool cancelled, object userState)
            : this(error, cancelled, userState)
        {
            Result = result;
        }
 
        public EnumAvailableSqlServersEventArgs(Exception error, bool cancelled, object userState) : base(error, cancelled, userState)
        {
        }
 
        public T Result { get; set; }
    }

Ну, что пришло время расширить наш интерфейс: Теперь он выглядит так:

        protected void InvokeEnumAvailableSqlServersComplited(EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>> e)
        {
            EventHandler<EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>> complited = EnumAvailableSqlServersComplited;
            if (complited != null)
                complited(this, e);
        }

А метод который запускает код в отдельном потоке, выглядит следующим образом:

       public void EnumAvailableSqlServersAsync()
        {
            ThreadPool.QueueUserWorkItem(o =>
            {
                try
                {
                    IList<NetworkInstanceDesceriptor> result = EnumAvailableSqlServers();
                    InvokeEnumAvailableSqlServersComplited(new EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>(result,
                        null,
                        false,
                        null));
                }
                catch (Exception ex)
                {
                    InvokeEnumAvailableSqlServersComplited(new EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>(ex, false, null));
                }
            });

Как Вы можете видеть я использовал ThreadPool, вместо создания потока в вручную, я почти всегда предпочитаю делать именно так. Вместо же использования явной реализации метода удовлетворяющего WaitCallback, я использую лямбда выражение, на мой взгляд код не только короче, но и понятнее. Реализация через ThreadPool, прямолинейна, но имеет ряд ограничений, тем не менее обычно этого достаточно для приложения главное это просто и безопасно, а при надобности всегда можно изменить реализацию на более сложную. Отдельно стоит отметить, что реализация возможности множественных вызовов ассинхронного метода, в этой нашей версии никак не обрабатывается, вообще-то было бы не плохо это сделать. Я обозначу то, как это делается. Первый вариант через управляющий флаг, что-то вроде IsBusy или InProgress это даст клиенту класса возможность проверить возможность повторного обращения, в свою очередь код обнаруживая множественные вызовы, может бросать исключение. Другой более не тривиальный способ состоит в том, что бы передавать, в Async метод дополнительный параметр, уникально идентифицирующий вызов, что то похожее на object taskId. В свою очередь при генерации события для клиента класса, этот taskId, может быть передан обработчику. Дополнительный плюс состоит в том, что в этом сценарии, можно использовать данный идентификатор при реализации возможностей по отмене конкретных «таск».

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

        static void Main(string[] args)
        {
            var manager = new SqlServerNetworkManager();
            manager.EnumAvailableSqlServersComplited += ManagerEnumAvailableSqlServersComplited;
            manager.EnumAvailableSqlServersAsync();
            Console.ReadLine();
        }
 
        static void ManagerEnumAvailableSqlServersComplited(object sender, EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>> e)
        {
            if (e.Error == null)
            {
                foreach (var instance in e.Result)
                    Console.WriteLine(instance);
            }
            else
            {
                Console.WriteLine("Показуем пользователю красивое окошко сообщающие, что случилось, что-то не доброе :)");
            }
        }

Подписываемся на событие, вызываем ассинхронный метод. Обработчик получает результат и выводит его на консоль… Желаю Вам всем успеха. Напоследок хочу напомнить, что когда будете использовать эту модель при работе с GUI, не забудьте синхронизировать доступ об этом очень хорошо и подробно написал PIL в своей давешней, но от того не менее актуальной статье.

Следующим нашим шагом будет введение автоматического маршалинга кода который выполняется ассинхронно, в поток GUI. Это безусловно несколько усложнит наш код, но сделает работу с нашим классом гораздо более удобной. Конечно я знаю .NET Framework облегчит нам жизнь и мы разберемся с механизмами позволяющими маршалить код с потока в поток.

Перед тем как мы добавим поддержку маршализации в наш код, нам прийдется остановится на нескольких теоретических моментах, уверен без этого приведенный мной код не будет очевидным. Прежде всего нужно поговорить о классе SynchronizationContext и о его предназначении. Строго говоря этот класс предоставляет базовую функциональность по маршалингу. Классы наследники обычно реализуют возможность синхронизации вызовов для конкретной технологии. Такие классы имеются в WPF, WindowsForms, APS.NET. Возможно вы уже догадались для чего использовалось наследование. Конечно, что бы обеспечить нам с Вами, возможность работать с таким контекстом через базовый класс при этом не задумываться при написании компонентов с какой именно технологией мы имеем дело. Какой же базовый функционал несет в себе контекст синхронизации? Самыми важными являются метод Post и Send. По сути эти методы будут производить маршализацию. Базовый класс, имеет наивные реализации, например Post использует ThreadPool, а Send обычный синхронный вызов делегата, зато методы объявлены как виртуальные, что и позволяет классам наследникам определять свои собственные механизмы маршалинга.

Вот то, как реализованы методы в классе SynchronizationContext:

    public virtual void Post(SendOrPostCallback d, object state)
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state);
    }
 
    //Тут для вызова метода через делегат используется пул потоков.
    public virtual void Send(SendOrPostCallback d, object state)
    {
        d(state);
    }

А тут как говорилось, просто идет прямой вызов посредством делегата. Не очень полезно как по мне. С наследниками же дело обстоит веселее, вот реализация методов из WindowsFormsSynchronizationContext, я чуть ниже поясню смысл.

    public override void Post(SendOrPostCallback d, object state)
    {
        if (this.controlToSendTo != null)
        {
            this.controlToSendTo.BeginInvoke(d, new object[] { state });
        }
    }
 
    public override void Send(SendOrPostCallback d, object state)
    {
        Thread destinationThread = this.DestinationThread;
        if ((destinationThread == null) || !destinationThread.IsAlive)
        {
            throw new InvalidAsynchronousStateException(SR.GetString("ThreadNoLongerValid"));
        }
        if (this.controlToSendTo != null)
        {
            this.controlToSendTo.Invoke(d, new object[] { state });
        }
    }

Метод пост пользуется тем, что контекст имеет ссылку на Control из Windows Forms, каждый контрол как вы возможно знаете, предоставляет метод BeginInvoke, позволяющий запустить код в потоке GUI. В общем-то не хитро. Несмотря на много «букаф» в методе Send тут ситуация не сложнее, когда контекст создается, он получает ссылку на поток

 this.DestinationThread = Thread.CurrentThread;

это код из конструктора. Проверив, что с потоком все хорошо, следует тот же прием что и для метода Post, но в синхронном исполнении. Осталось узнать как контекст получает ссылку на контрол который будут «беспокоить» на самом деле из другого контекста, на этот раз контекста потока. Вот полный код конструктора, думаю должно быть понятно без дополнительных объяснений:

public WindowsFormsSynchronizationContext()
{
    this.DestinationThread = Thread.CurrentThread;
    Application.ThreadContext context = Application.ThreadContext.FromCurrent();
    if (context != null)
    {
        this.controlToSendTo = context.MarshalingControl;
    }
}

Собственно можно было бы уже начать пользоваться этим чудом, посредством волшебного статического свойства Current возвращающего ссылку на контекст, но на самом деле нам нужно познакомится с еще одним семейством классов, которые облегчат нам работу с контекстами Первый класс это вспомогательный AsyncOperatonManager, он имеет свойство SynchronizatoinContext, возвращающий ссылку на текущий контекст синхронизации, а также фабричный метод CreateCommand, который возвращает объект AsyncOperation. Попробуем разобраться с тем как это поможет нам и с тем как это работает. Первое свойство AsyncOperatonManager SynchronizatoinContext, метод простой он проверяет существует ли установленный контекст синхронизации и если да, то возвращает его, если нет, то возвращается дефолтная реализация SynchronizatoinContext. Это свойство просто экономит нам написание пары лишних строчек кода при обращению к контексту, например не нужно на null проверять. Метод CreateCommand создает команду. Что за команду давайте взглянем подробней. Команда имеет ссылку на текущий контекст синхронизации, команда позволяет сохранить некий пользовательский стейт плюс к этому команда имеет собственное состояние (выполнена или нет). Когда команда выполнена, она может к примеру сообщить об этом контексту, что бы он коректно осводил необходимые ресурсы. В общем алгоритм следующий вы создаете команду вызывая метод CreateOpperation у AsyncOperatonManager и передаете (опционально, можно передать Null) пользовательский объект служащий параметром для ассинхронного метода, затем AsyncOperatonManager устанавливает команде текущий контекст, устанавливает команде поле _complited в false и дергает метод контекста OperationStarted. Дефолтный контекст имеет пустую реализацию, зато наследники такие как WindowsFormsSynchronizationContext могут определять собственную логику по отслеживанию операции маршалинга (скажу по секрету, что они этого не делают ) по сути вызов этот пустой, только контекст для ASP.NET определяет собственную логику в этом месте. И так команда получена, но как с ее помощью маршалить вызовы посредством ассоциированного контекста? Все предельно просто каждая команда имеет пару уже знакомых нам методов Post и Send которые делегируют всю нужную работу ассоциированному контексту. Единственные дополнениями к делегации, есть проверки, что делегат не null и собственное состояние команды установлено в не выполнено. Команда в состоянии – выполнена, более не пригодна к использованию. Это сделано для того, что бы корректно освобождать ресурсы. Еще один полезный метод который нужно описать это PostOperationComplited которые также делегирует вызов через Post, но дополнительно устанавливает команду в состояние выполнена. Теперь вооружившись общими знаниями, применим это все на практике по отношению к нашему собственному компоненту. Объявим переменную

private AsyncOperation _currentOperation;

Я буду реализовывать модель по которой клиентский код не допускает несколько параллельных ассинхронных вызовов. В EnumAvailableSqlServersAsync инициализируем команду Вот тело метода:

           _currentOperation = AsyncOperationManager.CreateOperation(null);
 
 
            ThreadPool.QueueUserWorkItem(obj =>
             {
                 Exception execption = null;
                 IList<NetworkInstanceDesceriptor> result = null;
 
                 try
                 {
                     result = EnumAvailableSqlServers();
                 }
                 catch (Exception ex)
                 {
                     execption = ex;
                 }
                 finally
                 {
                     CombineResults(result, execption);
                 }
             });

Несложно заметить, что в методе произошли изменения, вместо непосредственной генерации события об окончании выполнения, я вызываю метод CombineResult назначение которого собрать EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor» и посредством команды выполнить маршализацию в поток GUI посредством PostOperationCompleted который вызывается у нашей комманды, ниже реализация этого метода:

        private void CombineResults(IList<NetworkInstanceDesceriptor> result, Exception exception)
        {
            var args = new EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>(result, exception, false, null);
            _currentOperation.PostOperationCompleted(_onCompletedDelegate, args);
        }

Первым параметром используется делегат SendOrPostCallback делегат объявлен как принимающий методы возвращающие void и принимающий параметром object. В этом методе мы еще находимся в коде который вызывается через пул, а вот после маршализации, то есть когда вызовется метод назначенный делегату мы окажемся в потоке GUI (если конечно наш код выполняется в WinForms или WPF)

Нам нужно объявить такой делегат в программе:

private readonly SendOrPostCallback _onCompletedDelegate;

и в конструкторе класса проинициализировать его:

        public SqlServerNetworkManager()
        {
            _onCompletedDelegate = new SendOrPostCallback(AsyncCallCompleted); 
        }

Метод который используется для инициализации:

        private void AsyncCallCompleted(object operationState)
        {
            var e = operationState as EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>;
            InvokeEnumAvailableSqlServersComplited(e);
        }

Внутри этого метода мы уже в потоке GUI!!! (если конечно наш код выполняется в WinForms или WPF )

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

Добавим управляющий флаг и свойство.

        private bool _isBusy;
 
        public bool IsBusy
        {
            get { return _isBusy; }
            protected set { _isBusy = value; }
        }

Когда вызывается EnumAvailableSqlServersAsync мы проверяем, что код не занят:

            if (IsBusy)
                throw new InvalidOperationException();
 
            IsBusy = true;

и устанавливаем IsBusy в true, после того же как маршализация выполнена, мы можем снова разрешить пользоваться методом EnumAvailableSqlServersAsync

        private void CombineResults(IList<NetworkInstanceDesceriptor> result, Exception exception)
        {
            var args = new EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>(result, exception, false, null);
            _currentOperation.PostOperationCompleted(_onCompletedDelegate, args);
            IsBusy = false;
        }

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

    public class SqlServerNetworkManager : ISqlServerNetworkManager
    {
        private bool _isBusy;
        private readonly SendOrPostCallback _onCompletedDelegate;
 
        // используется для маршализации обращений между потоками
        private AsyncOperation _currentOperation;
 
        // подписавшись на событие клиент получит результат обработать резульатт или узнать об ошибке
        public event EventHandler<EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>> EnumAvailableSqlServersComplited;
 
        public SqlServerNetworkManager()
        {
            _onCompletedDelegate = new SendOrPostCallback(AsyncCallCompleted); 
        }
 
        public bool IsBusy
        {
            get { return _isBusy; }
            protected set { _isBusy = value; }
        }
 
        private void AsyncCallCompleted(object operationState)
        {
            // Находимся здесь после синхронизации, можем без проблем обращаться к GUI.
            var e = operationState as EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>;
            InvokeEnumAvailableSqlServersComplited(e);
        }
 
        // Синхронная реализация
        public IList<NetworkInstanceDesceriptor> EnumAvailableSqlServers()
        {
            var desceriptors = new List<NetworkInstanceDesceriptor>();
            using (DataTable table = SmoApplication.EnumAvailableSqlServers(false))
            {
                foreach (DataRow row in table.Rows)
                    desceriptors.Add(BuildNetworkDescriptor(row));
            }
 
            return desceriptors;
        }
 
        // Ассинхронная реализация
        public void EnumAvailableSqlServersAsync()
        {
            // Не даем обращаться к коду до конца выполнения маршализации, 
            // клиенту предоставляется свойство IsBusy, для того, 
            // что бы тот мог проверить возможность вызова
            if (IsBusy)
                throw new InvalidOperationException();
 
            IsBusy = true;
 
            _currentOperation = AsyncOperationManager.CreateOperation(null);
 
            ThreadPool.QueueUserWorkItem(obj =>
             {
                 Exception execption = null;
                 IList<NetworkInstanceDesceriptor> result = null;
 
                 try
                 {
                     result = EnumAvailableSqlServers();
                 }
                 catch (Exception ex)
                 {
                     execption = ex;
                 }
                 finally
                 {
                     CombineResults(result, execption);
                 }
             });
 
        }
 
        // Собираем результат работы кода в аргумент события и маршализируем его между потоками
        private void CombineResults(IList<NetworkInstanceDesceriptor> result, Exception exception)
        {
            var args = new EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>(result, exception, false, null);
            _currentOperation.PostOperationCompleted(_onCompletedDelegate, args);
            IsBusy = false;
        }
 
        private static NetworkInstanceDesceriptor BuildNetworkDescriptor(DataRow row)
        {
            return new NetworkInstanceDesceriptor(row["Name"] as string,
                                                  row["Server"] as string,
                                                  row["Instance"] as string);
        }
 
        protected void InvokeEnumAvailableSqlServersComplited(EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>> e)
        {
            EventHandler<EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>>> complited = EnumAvailableSqlServersComplited;
            if (complited != null)
                complited(this, e);
        }
    }

А теперь код клиента:

   public partial class MainForm : Form
    {
        private readonly ISqlServerNetworkManager _manager = new SqlServerNetworkManager();
        public MainForm()
        {
            InitializeComponent();
            _manager.EnumAvailableSqlServersComplited += ManagerEnumAvailableSqlServersComplited;
        }
 
        protected void ManagerEnumAvailableSqlServersComplited(object sender, EnumAvailableSqlServersEventArgs<IList<NetworkInstanceDesceriptor>> e)
        {
            comboBoxServers.DataSource = e.Result;
            comboBoxServers.DisplayMember = "Name";
        }
 
        private void buttonRefresh_Click(object sender, EventArgs e)
        {
            if (_manager.IsBusy)
                return;
 
            _manager.EnumAvailableSqlServersAsync();
        }
    }

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

До новых встреч.