Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[52.14.22.250] |
|
Сообщ.
#1
,
|
|
|
Давайте проанализируем результат того, что мы имеем по окончанию первой моей статьи. Мы легко вызываем метод асинхронно и имеем возможность проинформировать вызывающий код о результатах его работы, также мы позаботились о том, что бы иметь возможность обработать исключение, которое могло бы возникнуть в дочерном потоке. В конце прошлой статьи я обратил внимание, что код нужно синхронизировать и отправил читателя к статье PIL о cross-thread в GUI. Зачастую такой подход обоснован, но справедливости ради нужно заметить, что способ синхронизации описаный там не единственный, более того мной был упомянут компонет BackgroundWorker, который умеет работать с GUI и синхронизировать операции его обновления. Следующим нашим шагом будет введение автоматического маршалинга кода который выполняется ассинхронно, в поток 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; 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(); } } Написав не так много кода, мы обеспечили клиенту очень удобную ассинхронную модель взаимодействия с нашим кодом, не требующую синхронизации со стороны клиента. До новых встреч. |