Ноябрь 2015

Том 30, номер 12

Асинхронное программирование - Асинхронность с момента запуска

Марк Сауюл | Ноябрь 2015

Продукты и технологии:
Windows Forms, Windows Presentation Foundation, ключевые слова async и await
В статье рассматриваются:

  • рефакторинг изначального стартового кода Windows Forms и Windows Presentation Foundation для его преобразования в объектно-ориентированный;
  • отделение жизненного цикла приложения от формы/окна;
  • преобразование стартового кода в асинхронный;
  • обработка исключений, генерируемых из асинхронного кода;
  • добавление экрана-заставки без создания нового потока.

Исходный код можно скачать по ссылке

Недавние версии Microsoft .NET Framework как никогда ранее упростили написание отзывчивых, высокопроизводительных приложений с помощью ключевых слов async и await — не будет преувеличением сказать, что они изменили то, как .NET-разработчики пишут программное обеспечение. Асинхронный код, который раньше требовал непроглядной паутины вложенных обратных вызовов, теперь можно писать (и понимать!) почти так же легко, как и последовательный, синхронный код.

Материалов по созданию и использованию асинхронных методов уже более чем достаточно, поэтому я предполагаю, что вы знакомы с основами. А если нет, то страница Visual Studio Documentation по ссылке msdn.com/async поможет вам быстро войти в курс дела.

В большей части документации по асинхронности предупреждается, что нельзя просто так включить асинхронный метод в существующий код; вызывающий сам должен быть асинхронным. По словам Лусиана Вишика (Lucian Wischik), разработчика из группы языков Microsoft, «Async подобен зомбирующему вирусу». Как же тогда встроить асинхронность в саму структуру приложения прямо с начала, не прибегая к async void? Я намерен показать вам на примере нескольких переработок изначального стартового кода UI как для Windows Forms, так и для Windows Presentation Foundation (WPF), преобразовав стереотипный код UI в объектно-ориентированный и добавив поддержку async/await. Попутно я объясню, когда имеет смысл использовать async void, а когда — нет.

В этой статье основное внимание уделяется Windows Forms; WPF требует дополнительных изменений, что может увести нас в сторону. На каждом этапе я буду пояснять изменения в приложении Windows Forms, а затем рассматривать любые отличия для WPF-версии. В этой статье я продемонстрирую базовые изменения в коде, а полные примеры (и промежуточные ревизии) для обеих сред вы сможете увидеть в пакете исходного кода, сопутствующем данной статье.

Первые этапы

Шаблоны Visual Studio для приложений Windows Forms и WPF на самом деле не позволяют использовать асинхронность при запуске (и вообще как-то настраивать процесс запуска). Хотя C# пытается быть объектно-ориентированным языком (весь код должен находиться в классах) изначальный стартовый код подталкивает разработчиков к размещению логики в статических методах наряду с Main или в чрезмерно усложненном конструкторе основной формы. (Нет, это плохая идея обращаться к базе данных в конструкторе MainForm. И да, я встречал такое.) Эта ситуация всегда была проблематичной, но теперь с появлением async, это также означает, что нет четкой возможности заставить приложение инициализировать себя асинхронно.

Для начала я создал новый проект с шаблоном Windows Forms Application в Visual Studio. На рис. 1 показан его изначальный стартовый код в Program.cs.

Рис. 1. Изначальный стартовый код Windows Forms

static class Program
{
  /// <summary>
  /// Главная точка входа для приложения
  /// </summary>
  [STAThread]
  static void Main()
  {
    App app = new App();
    // Это применяет XAML, например StartupUri,
    // Application.Resources
    app.InitializeComponent();
    // Показывает окно, Заданное StartupUri
    app.Run();
  }
}

В случае WPF дело обстоит сложнее. Изначальный стартовый код WPF весьма непрозрачен, и даже поиск какого-либо кода для адаптации затруднен. Вы можете поместить некоторый код инициализации в Application.OnStartup, но как можно было бы задержать отображение UI, пока не загрузятся необходимые данные? Первое, что нужно сделать с WPF, — «вытащить» процесс запуска как код, который можно редактировать. Я доведу WPF до той же начальной точки, что и Windows Forms, а затем каждый этап, описываемый в статье, будет похож для обеих инфраструктур.

Создав новое WPF-приложение в Visual Studio, я создаю новый класс, Program (рис. 2). Чтобы заменить изначальную стартовую последовательность, откройте свойства проекта и смените стартовый объект с App на только что созданный Program.

Рис. 2. Эквивалентный стартовый код Windows Presentation Foundation

static class Program
{
  /// <summary>
  /// Главная точка входа для приложения
  /// </summary>
  [STAThread]
  static void Main()
  {
    App app = new App();
    // TЭто применяет XAML, например StartupUri,
    // Application.Resources
    app.InitializeComponent();
    // Показывает окно, Заданное StartupUri
    app.Run();
  }
}

Если вы используете Go to Definition при вызове InitializeComponent на рис. 2, то заметите, что компилятор генерирует эквивалентный код Main, как и при использовании App в качестве стартового объекта (вот так я могу открыть вам здесь «черный ящик»).

Движемся к объектно-ориентированному стартовому коду

Прежде всего я выполню небольшой рефакторинг изначального стартового кода, чтобы продвинуть его в направлении объектной ориентированности: я извлеку логику из Main и перемещу ее в какой-нибудь класс. Для этого я сделаю Program нестатическим классом (как я говорил, изначальный код толкает вас в неправильном направлении) и придам ему конструктор. Затем я перемещу подготовительный код в конструктор и добавлю метод Start, который будет запускать мою форму.

Новую версию я назвал Program1, и вы можете увидеть ее на рис. 3. Этот скелетный код показывает суть идеи: чтобы запустить программу, Main теперь создает объект и вызывает его методы, как в любом типичном объектно-ориентированном сценарии.

Рис. 3. Program1 — начальная ревизия объектно-ориентированного стартового кода

[STAThread]
static void Main()
{
  Program1 p = new Program1();
  p.Start();
}
 
private readonly Form1 m_mainForm;
private Program1()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
 
  m_mainForm = new Form1();
}
 
public void Start()
{
  Application.Run(m_mainForm);
}

Отделение приложения от формы

Тем не менее, тот вызов Application.Run, который принимает экземпляр формы (в конце моего метода Start), ставит несколько задач. Одна из них относится к числу универсальных архитектурных проблем: мне не нравится, что это привязывает жизненный цикл приложения к отображению формы. Это подошло бы для многих приложений, но бывают приложения, выполняемые в фоне, которые не должны показывать никакого UI при запуске, кроме, может быть, значка на панели задач или в области уведомлений. Я видел некоторые программы, которые при запуске ненадолго выводят экран-заставку, а потом исчезают для пользователя. Бьюсь об заклад, что их стартовый код следует аналогичному процессу, а затем они скрываются как можно быстрее, когда заканчивается загрузка формы. Следует признать, что решать здесь эту конкретную проблему не обязательно, но отделение будет иметь критически важное значение при асинхронной инициализации.

Вместо Application.Run(m_mainForm) я буду использовать перегруженную версию Run, не принимающую никаких аргументов: она запускает инфраструктуру UI, не привязывая ее ни к какой конкретной форме. Это отделение означает, что я должен сам отображать форму; это также подразумевает, что закрытие этой формы больше не будет приводить к выходу из приложения, поэтому мне нужно подключать ее явным образом, как показано на рис. 4. Я также использую эту возможность, чтобы добавить свою первую точку подключения для инициализации. Initialize — это метод, созданный мной в собственном классе формы, чтобы хранить любую логику, необходимую для его инициализации, например для извлечения данных из базы данных или с веб-сайта.

Рис. 4. Program2 — цикл обработки сообщений теперь отделен от основной формы

private Program2()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);

  m_mainForm = new Form1();
  m_mainForm.FormClosed += m_mainForm_FormClosed;
}

void m_mainForm_FormClosed(object sender,
  FormClosedEventArgs e)
{
  Application.ExitThread();
}

public void Start()
{
  m_mainForm.Initialize();
  m_mainForm.Show();
  Application.Run();
}

В WPF-версии StartupUri приложения определяет, какое окно следует вывести при вызове Run; вы увидите, как это определяется, в файле разметки — App.xaml. Не удивительно, что параметр ShutdownMode по умолчанию в OnLastWindowClose для Application закрывает приложение, когда закрыты все WPF-окна, и именно так увязываются воедино жизненные циклы. (Заметьте, что это отличается от того, что мы имеем в Windows Forms. В Windows Forms, если основное окно открывает дочернее и вы закрываете первое окно, приложение завершается. В WPF оно не завершится, пока вы не закроете оба окна.)

Чтобы добиться того же разделения в WPF, я сначала удаляю StartupUri из App.xaml. И создаю окна сам, инициализируя и отображая его до вызова App.Run:

public void Start()
{
  MainWindow mainForm = new MainWindow();
  mainForm.Initialize();
  mainForm.Show();
  m_app.Run();
}

Создавая приложение, я устанавливаю app.ShutdownMode в ShutdownMode.OnExplicitShutdown, что отделяет жизненный цикл приложения от жизненных циклов окон:

m_app = new App();
m_app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
m_app.InitializeComponent();

Чтобы реализовать явное завершение программы, я подключаю обработчик событий для MainWindow.Closed.

Конечно, WPF сделано больше для разделения обязанностей, ввиду чего имеет больше смысла инициализировать модель представления, а не само окно: я создам класс MainViewModel, а в нем — метод Initialize. Аналогично запрос на закрытие приложение тоже должен проходить через модель представления, поэтому я добавлю в эту модель событие CloseRequested и соответствующий метод RequestClose. Полученная WPF-версия Program2 приведена на рис. 5 (Main не изменился, так что я не показываю его здесь).

Рис. 5. Класс Program2, версия для Windows Presentation Foundation

private readonly App m_app;
private Program2()
{
  m_app = new App();
  m_app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
  m_app.InitializeComponent();
}
 
public void Start()
{
  MainViewModel viewModel = new MainViewModel();
  viewModel.CloseRequested += viewModel_CloseRequested;
  viewModel.Initialize();
 
  MainWindow mainForm = new MainWindow();
  mainForm.Closed += (sender, e) =>
  {
    viewModel.RequestClose();
  };
     
  mainForm.DataContext = viewModel;
  mainForm.Show();
  m_app.Run();
}
 
void viewModel_CloseRequested(object sender, EventArgs e)
{
  m_app.Shutdown();
}

Извлечение среды хостинга

Теперь, когда я отделил Application.Run от формы, я хочу решить другую задачу, связанную с архитектурой. Сейчас Application глубоко встроен в класс Program. Мне нужно, так сказать, абстрагировать эту среду хостинга (hosting environment). Я удалю все методы Windows Forms в Application из своего класса Program, оставив только функциональность, относящуюся к самой программе, как показано в Program3 на рис. 6. Одна из заключительных частей головоломки — добавление события в класс программы, чтобы связь между закрытием формы и завершением приложения была менее прямой. Заметьте, что Program3 как класс не имеет никакой связи с Application!

Рис. 6. Program3 — теперь его легко подключить куда угодно

private readonly Form1 m_mainForm;
private Program3()
{
  m_mainForm = new Form1();
  m_mainForm.FormClosed += m_mainForm_FormClosed;
}

public void Start()
{
  m_mainForm.Initialize();
  m_mainForm.Show();
}

public event EventHandler<EventArgs> ExitRequested;
void m_mainForm_FormClosed(object sender,
  FormClosedEventArgs e)
{
  OnExitRequested(EventArgs.Empty);
}

protected virtual void OnExitRequested(EventArgs e)
{
  if (ExitRequested != null)
    ExitRequested(this, e);
}

Отделение среды хостинга дает несколько преимуществ. Например, это упрощает тестирование (теперь вы можете протестировать Program3 — до определенной степени). Это также облегчает повторное использование кода, который можно встроить, скажем, в более крупное приложение.

Отсоединенный Main показан на рис. 7 — я переместил логику Application обратно в него. Такая архитектура упрощает интеграцию WPF и Windows Forms или, возможно, постепенную замену Windows Forms на WPF. Это выходит за рамки данной статьи, но вы можете найти пример смешанного приложения в сопутствующем статье пакете исходного кода. Как и в предыдущем случае рефакторинга, это удобные вещи, но не обязательно критически важные. Значимость, так сказать, «Task под рукой» заключается в том, что это позволит сделать асинхронную версию потока управления более естественной, как вы вскоре увидите.

Рис. 7. Main — теперь может быть хостом произвольной программы

[STAThread]
static void Main()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
 
  Program3 p = new Program3();
  p.ExitRequested += p_ExitRequested;
  p.Start();
 
  Application.Run();
}
 
static void p_ExitRequested(object sender, EventArgs e)
{
  Application.ExitThread();
}

Долгожданная асинхронность

И вот, наконец, результат. Я могу сделать метод Start асинхронным, что позволит мне использовать await и преобразовать логику инициализации в асинхронную. В соответствии с соглашением я переименовал Start в StartAsync, а Initialize — в InitializeAsync. Я также изменил их тип возврата на async Task:

public async Task StartAsync()
{
  await m_mainForm.InitializeAsync();
  m_mainForm.Show();
}

Чтобы использовать эту асинхронность, Main изменяется следующим образом:

static void Main()
{
  ...
  p.ExitRequested += p_ExitRequested;
  Task programStart = p.StartAsync();
 
  Application.Run();
}

Чтобы пояснить, как это работает (и решить одну небольшую, но важную проблему), нужно подробно исследовать, что происходит при использовании async/await.

Истинный смысл await Рассмотрим представленный мной метод StartAsync. Важно понимать, что (как правило), когда async-метод встречает ключевое слово await, он возвращает управление. Исполняющий поток продолжает работу — точно так же, как это было бы при возврате управления любым методом. В данном случае метод StartAsync достигает «await m_mainForm.InitializeAsync» и возвращает управление в Main, который продолжает работу, вызывая Application.Run. Это приводит к несколько нелогичному результату: весьма вероятно, что Application.Run будет выполнен до m_mainForm.Show, хотя при последовательном выполнении это происходит после m_mainForm.Show. Async и await действительно упрощают асинхронное программирование, но оно никоим образом не становится простым.

Вот почему async-методы возвращают Task; именно завершение выполнения Task представляет «возврат» async-метода интуитивно понятным образом, а именно, когда выполнен весь его код. В случае StartAsync это означает, что он выполнил как InitializeAsync, так и m_mainForm.Show. И это первая проблема при использовании async void: без объекта задачи у вызвавшего async void-метод нет никакого способа узнать, когда он закончил выполнение.

Как и когда выполняется остальной код, если поток ушел в мир иной и StartAsync уже вернул управление тому, кто его вызвал? Именно здесь на сцену выходит Application.Run. Этот метод является бесконечным циклом, ожидающим появления работы — в основном обработки UI-событий. Например, когда вы перемещаете мышь по окну или щелкаете кнопку, цикл обработки сообщений в Application.Run будет извлекать событие из очереди и отправлять его соответствующему коду, а затем ожидать появления следующего события. Однако он не ограничивается одним UI: посмотрите на Control.Invoke, который выполняет функцию в UI-потоке. Application.Run обрабатывает и эти запросы.

В этом случае по завершении InitializeAsync остальная часть метода StartAsync будет передана в тот самый цикл обработки сообщений. При использовании await метод Application.Run будет выполнять остаток метода в UI-потоке — так, будто вы написали обратный вызов, использующий Control.Invoke. (Должно ли продолжение [continuation] выполняться в UI-потоке, контролируется ConfigureAwait. Подробнее на эту тему см. статью Стивена Клири [Stephen Cleary] за март 2013 года по ссылке msdn.com/magazine/jj991977).

Вот почему так важно отделить Application.Run от m_mainForm. Application.Run правит бал: он должен выполняться, чтобы обрабатывать код после await даже до того, как вы реально отображаете какой бы то ни было UI. Так, если вы попытаетесь изъять Application.Run из Main и вернуть обратно в StartAsync, программа просто немедленно завершится. Как только поток выполнения встречает await InitializeAsync, управление возвращается Main, в котором больше нет никакого кода для выполнения, а значит, это конец Main.

Это также объясняет, почему использование async должно начинаться снизу вверх. Распространенный, но просуществовавший недолго антишаблон — вызов Task.Wait вместо await, поскольку вызывающий не является async-методом, но скорее всего это немедленно приведет к взаимоблокировке. Проблема в том, что UI-поток будет блокирован этим вызовом Wait и не сможет обработать продолжение. Без продолжения задача не завершится, поэтому вызов Wait никогда не вернет управление — взаимоблокировка!

Await и Application.Run, проблема курицы и яйца Я уже упоминал о существовании небольшой, но серьезной проблемы. Я описал, что, когда вызывается await, поведение по умолчанию заключается в продолжении выполнения в UI-потоке, что мне и нужно здесь. Однако инфраструктура для этого не подготовлена при моем первом вызове await, так как соответствующий код еще не выполнялся!

Ключ к этому поведению — SynchronizationContext.Current. При вызове await инфраструктура захватывает значение SynchronizationContext.Current и использует его для передачи продолжения; вот как продолжение выполняется в UI-потоке. Контекст синхронизации подготавливается Windows Forms или WPF при входе в цикл обработки сообщений. Внутри StartAsync этого еще не происходит: если вы изучите SynchronizationContext.Current в начале StartAsync, то обнаружите, что он пуст. В отсутствие контекста синхронизации await отправит продолжение в пул потоков, а поскольку это не будет UI-поток, то и работать оно тоже не будет.

WPF-версия моментально зависнет, а версия для Windows Forms, как оказалось, будет работать от случая к случаю. По умолчанию Windows Forms подготавливает контекст синхронизации, когда создается первый элемент управления, — в данном случае, когда я конструирую m_mainForm (это поведение контролируется WindowsFormsSynchronizationContext.AutoInstall). Поскольку await InitializeAsync выполняется до создания мной формы, у меня все в порядке. Однако, если бы я поместил вызов await до создания m_mainForm, я нарвался бы на ту же проблему. Решение заключается в самостоятельной подготовке контекста синхронизации в самом начале:

[STAThread]
static void Main()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
  SynchronizationContext.SetSynchronizationContext(
    new WindowsFormsSynchronizationContext());
 
  Program4 p = new Program4();
  ... as before
}

Для WPF эквивалентный вызов выглядит так:

SynchronizationContext.SetSynchronizationContext(
  new DispatcherSynchronizationContext());

Обработка исключений

Почти готово! Но у меня все еще остается другая давнишняя проблема в корне приложения: если InitializeAsync генерирует исключение, программа не обрабатывает его. Объект задачи programStart будет содержать информацию исключения, но с этим ничего нельзя сделать, поэтому мое приложение зависнет. Если бы я мог использовать await StartAsync, я сумел бы захватить исключение в Main, но я не могу задействовать await, так как Main не является async-методом.

Это иллюстрирует вторую проблему с async void: нет никакого способа захватывать исключения, генерируемые async void-методом, поскольку у вызвавшего нет доступа к объекту задачи. (Тогда в каких же случаях нужно применять async void? В типичном руководстве говорится, что применение async void следует ограничить в основном обработчиками событий. В уже упомянутой статье за март 2013 года это тоже обсуждается, так что советую прочитать ее, чтобы выжать максимум из async/await.)

В нормальных обстоятельствах с задачами, генерирующими исключения, которые впоследствии не обрабатываются, имеет дело TaskScheduler.UnobservedException. Проблема в том, что нет гарантий его выполнения. В этой ситуации он почти наверняка не будет выполняться: планировщик задач обнаружит незамеченные исключения при финализации такой задачи. Финализация происходит, только когда выполняется сборщик мусора. А он запускается, только если ему нужно удовлетворить запрос на выделение дополнительной памяти.

Вы можете увидеть, где это осуществляется: в данном случае исключение приведет к тому, что приложение будет болтаться в памяти, ничего не делая, поэтому оно не запросит дополнительную память, и сборщик мусора не запустится. Итог — зависание приложения. По сути, именно поэтому WPF-версия зависает, если вы не указываете контекст синхронизации. Конструктор WPF-окна генерирует исключение из-за того, что окно создается не в UI-потоке и это исключение не обрабатывается. Тогда последний фрагмент головоломки — обработка задачи programStart и добавление продолжения, которое будет запущено в случае ошибки. В моем примере имеет смысл завершать приложение, если оно не может инициализироваться.

Использовать await в Main нельзя, потому что это не async-метод, но можно создать новый async-метод исключительно для того, чтобы обеспечивать доступ (и обрабатывать) любые исключения, генерируемые при асинхронном запуске. Он будет состоять лишь из блока try-catch вокруг await. Так как этот метод будет обрабатывать все исключения и не будет генерировать новые, это еще один из ограниченных случаев применения async void:

private static async void HandleExceptions(Task task)
{
  try
  {
    await task;
  }
  catch (Exception ex)
  {
    // ...фиксируем исключение,
    // показываем ошибку пользователю и т. д.
    Application.Exit();
  }
}

В Main это используется так:

Task programStart = p.StartAsync();
HandleExceptions(programStart);
Application.Run();

Конечно, как обычно, есть одна тонкость (если async/await упрощает асинхронное программирование, то теперь вы можете представить, насколько трудным оно было раньше). Ранее я сказал, что, как правило, когда async-метод достигает вызова await, он возвращает управление и остальная часть метода выполняется как продолжение. Однако в некоторых случаях задача может выполняться синхронно; если это ваш случай, выполнение кода не будет прерываться, что даст выигрыш в производительности. Но, если это произойдет здесь, тогда весь метод HandleExceptions будет выполнен, вернет управление, и Application.Run последует за ним. Если исключения не будет, то теперь вызов Application.Exit будет происходить до вызова Application.Run.

Чего я хочу добиться, так это заставить HandleExceptions выполняться как продолжение: мне нужно быть уверенным, что я «пролечу» к Application.Run до того, как стану делать что-то еще. Тем самым, если есть исключение, я знаю, что Application.Run уже выполняется и Application.Exit корректно прервет его. Task.Yield делает вот что: он заставляет текущий async-код вернуть управление вызвавшему, а затем возобновить выполнение как продолжение.

Вот исправление в HandleExceptions:

private static async void HandleExceptions(Task task)
{
  try
  {
    // Заставляем код вернуть управление вызвавшему,
    // чтобы выполнение Application.Run продолжилось
    await Task.Yield();
    await task;
  }
  ...как и раньше

В этом случае, когда я вызову await Task.Yield, HandleExceptions вернет управление и будет выполняться Application.Run. Остальная часть HandleExceptions будет потом передана как продолжение в текущий SynchronizationContext, а значит, будет подхвачена Application.Run.

Кстати, я думаю, что Task.Yield — хороший лакмусовый тест на понимание async/await: Если вы понимаете использование Task.Yield, тогда вы, вероятно, имеете твердые знания того, как работает async/await.

Пожинаем плоды

Теперь, когда все работает, пришла пора пожинать плоды: я намерен показать, насколько легко добавить отзывчивый экран-заставку (splash screen), не запуская его в отдельном потоке. Наличие такого экрана весьма важно, если ваше приложение не стартует немедленно. Если пользователь запускает ваше приложение и видит, что в течение нескольких секунд ничего не происходит, он может подумать что угодно, и это вряд ли вам понравится.

Запуск отдельного потока для экрана-заставки не эффективен и к тому же громоздок — вам придется выполнять корректный маршалинг всех вызовов между потоками. Вывод информации о прогрессе на экране-заставке будет затруднителен, и даже закрытие этого экрана потребует вызова Invoke или эквивалентного метода. Более того, когда экран-заставка наконец закрывается, обычно он не передает фокус ввода на основную форму должным образом, потому что нельзя задать отношения владения между экраном-заставкой и основной формой, если они находятся в разных потоках. Сравните это с простотой асинхронной версии, показанной на рис. 8.

Рис. 8. Добавление экрана-заставки в StartAsync

public async Task StartAsync()
{
  using (SplashScreen splashScreen = new SplashScreen())
  {
    // Если пользователь закрывает экран-заставку, выходим;
    // это также хорошая возможность задать маркер отмены
    splashScreen.FormClosed += m_mainForm_FormClosed;
    splashScreen.Show();
 
    m_mainForm = new Form1();
    m_mainForm.FormClosed += m_mainForm_FormClosed;
    await m_mainForm.InitializeAsync();
 
    // Это гарантирует работу активации, поэтому, когда
    // экран-заставка исчезает, активируется основная форма
    splashScreen.Owner = m_mainForm;
    m_mainForm.Show();
 
    splashScreen.FormClosed -= m_mainForm_FormClosed;
    splashScreen.Close();
  }
}

Заключение

Я показал, как применять объектно-ориентированную архитектуру к стартовому коду приложения — будь то приложение Windows Forms или WPF; поэтому оно может легко поддерживать асинхронную инициализацию. Я также продемонстрировал, как преодолеть некоторые проблемы, которые могут возникать в процессе асинхронного запуска. В отношении того, чтобы сделать истинно асинхронной именно вашу инициализацию, боюсь, что здесь вам придется поразмыслить самостоятельно, но некоторые рекомендации вы найдете на msdn.com/async.

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


Марк Сауюл (Mark Sowul) на самом деле может быть программной симуляцией, написанной на C# (так поговаривают коллеги). Преданный .NET-разработчик с момента появления этой инфраструктуры делится своими богатыми экспертными познаниями в архитектуре и достижении максимальной производительности в .NET и Microsoft SQL Server через свою консалтинговую компанию в Нью-Йорке — SolSoft Solutions. С ним можно связаться по адресу mark@solsoftsolutions.com, а также подписаться на сайте eepurl.com/_K7YD на его периодические рассылки по электронной почте для получения глубокой аналитической информации по программному обеспечению.

Выражаю благодарность за рецензирование статьи экспертам Microsoft Стивену Клири (Stephen Cleary) и Джеймсу Маккафри (James McCaffrey).