0% нашли этот документ полезным (0 голосов)
7 просмотров14 страниц

Synchronization Context

Контекст синхронизации в C#. Кратко

Загружено:

tokarevyuriinikolaevich
Авторское право
© All Rights Reserved
Мы серьезно относимся к защите прав на контент. Если вы подозреваете, что это ваш контент, заявите об этом здесь.
Доступные форматы
Скачать в формате DOC, PDF, TXT или читать онлайн в Scribd
0% нашли этот документ полезным (0 голосов)
7 просмотров14 страниц

Synchronization Context

Контекст синхронизации в C#. Кратко

Загружено:

tokarevyuriinikolaevich
Авторское право
© All Rights Reserved
Мы серьезно относимся к защите прав на контент. Если вы подозреваете, что это ваш контент, заявите об этом здесь.
Доступные форматы
Скачать в формате DOC, PDF, TXT или читать онлайн в Scribd

SynchronizationContext.

Current является ключом к такому поведению: при вызове await инфраструктура


фиксирует значение [Link] и использует его для публикации продолжения; вот
как это продолжается в потоке пользовательского интерфейса. Контекст синхронизации настраивается
Windows Forms или WPF при запуске цикла сообщений. Внутри StartAsync этого еще не произошло: если
вы проверите [Link] в начале StartAsync, вы увидите, что он равен нулю. Если
контекст синхронизации отсутствует, await вместо этого отправит продолжение в пул потоков, и
поскольку это не будет поток пользовательского интерфейса, он не будет работать.

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

С#Копировать
[STAThread]
static void Main()
{
[Link]();
[Link](false);
[Link](
new WindowsFormsSynchronizationContext());

Program4 p = new Program4();


... as before
}
Практические советы по повышению производительности приложений Windows Forms

Содержание

Быстрый запуск
Ускоренный запуск пользовательского интерфейса
Управление загрузкой пользовательского интерфейса
Оптимизация привязки данных
Облегченный макет
Производительность рисования
текста и изображений
Применение методов рисования
Управление ресурсами
Заключение

Windows Forms позволяют создавать богатый и отзывчивый пользовательский интерфейс для ваших
приложений. В этой статье я расскажу о ряде методов, которые можно использовать, чтобы обеспечить
оптимальную производительность приложений на базе Windows® Forms. Я обсужу распространенные
сценарии, критичные к производительности, такие как запуск, контроль заполнения и контроль
рисования. Кроме того, я расскажу, как проектировать и писать код для повышения производительности
вашего приложения. Вместе эти методы должны дать вам хорошую базовую основу для получения
максимальной отдачи от ваших пользовательских интерфейсов.

Быстрый запуск

Время запуска является важным показателем производительности для большинства


приложений. Быстрый запуск оставляет у пользователя благоприятное впечатление о
производительности и удобстве использования вашего приложения. Есть два термина, описывающих
запуск приложения: «теплый» запуск и «холодный запуск». Если вы запустите какое-либо управляемое
приложение сразу после перезагрузки компьютера, закройте приложение и запустите его снова. Обычно
вы заметите довольно значительную разницу во времени запуска. При первом (холодном) запуске
потребуется выполнить операции дискового ввода-вывода, чтобы перенести все необходимые страницы
в память. Второй (теплый) запуск происходит значительно быстрее, поскольку он повторно использует
страницы, которые уже присутствуют в памяти.

Чтобы получить эффект теплого запуска, вам не обязательно ранее запускать то же приложение. Простой
запуск любого управляемого приложения приведет к сохранению в памяти большого количества
страниц из двоичных файлов Microsoft® .NET Framework, что сократит время запуска любых
управляемых приложений, которые запускаются позже.

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

Я начну с некоторых общих советов по производительности, применимых к стартовому приложению,


написанному на любом управляемом коде. Затем я дам вам несколько советов по повышению
производительности приложений на базе Windows Forms. Подробные рекомендации по повышению
производительности запуска управляемых приложений см. в колонке CLR Inside Out Клаудио Калдато в
февральском выпуске журнала MSDN ® Magazine за 2006 г. на странице CLR Inside Out: Улучшение
времени запуска приложений .

Один из способов улучшить холодный запуск — уменьшить количество библиотек DLL, загружаемых в
приложение. Это напрямую влияет на количество страниц, которые приходится трогать при запуске
приложения. Кроме того, уменьшение количества загружаемых модулей снижает нагрузку на ЦП,
связанную с загрузкой модуля, а также сокращает время горячего запуска.

Вы можете диагностировать, как модули загружаются в ваше приложение, с помощью фильтра событий
загрузки модуля в отладчике WinDbg в Platform SDK. Альтернативно вы можете использовать следующую
команду WinDbg, чтобы узнать, почему был загружен конкретный модуль:

Копировать
sxe ld:[Link]

Просмотрите стеки вызовов и посмотрите, можно ли избежать загрузки некоторых модулей. Вы также
можете использовать опцию «ca ml» управляемого отладчика MDbg, доступного как часть .NET
Framework SDK. В некоторых случаях вы можете избежать загрузки дополнительных модулей, просто
проведя рефакторинг кода. Если ваше приложение загружает множество принадлежащих вам DLL,
рассмотрите возможность объединения их в меньшее количество более крупных DLL, где это имеет
смысл.

JIT-компиляция методов может потреблять много циклов ЦП и легко стать узким местом во время
запуска приложения. Чтобы избежать этих накладных расходов, вы можете предварительно
скомпилировать сборки с помощью встроенного генератора изображений [Link] (см. Скорость: NGen
повышает производительность с помощью новых мощных функций ). NGen выполняет всю работу JIT-
компилятора, но выполняет эту работу заранее, а затем сохраняет эти изменения на диске, экономя
циклы ЦП во время выполнения. Использование NGen для предварительной компиляции двоичных
файлов дает дополнительный выигрыш в производительности: собственные кодовые страницы могут
использоваться совместно между процессами, тогда как кодовые страницы, скомпилированные JIT,
являются частными для процесса и не могут быть использованы повторно.

Несмотря на преимущества, прекомпиляция не всегда является панацеей от времени запуска. Хотя


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

Другой метод — установить сборки со строгими именами в глобальный кэш сборок (GAC), чтобы
избежать проверки подписи строгого имени — дорогостоящего процесса, который затрагивает каждую
страницу сборки перед ее загрузкой. Если вы решите использовать NGen, еще важнее иметь в GAC
сборки со строгими именами и избегать перебазирования DLL.

Каждый двоичный файл (DLL или EXE) имеет предпочтительный базовый адрес — место в виртуальной
памяти, куда он должен быть загружен. Этот адрес указывается во время сборки. Если вы используете
компилятор Visual Basic® или C#, то все ваши двоичные файлы получат один и тот же базовый адрес
(0x400000), если не дано явных указаний на обратное. Таким образом, во время загрузки ваш EXE-файл
будет размещен по этому адресу, и все ваши библиотеки DLL придется разместить в другом месте в
виртуальной памяти (перебазировать), поскольку их предпочтительный базовый адрес занят.

Когда DLL перебазируется, загрузчик обновляет все абсолютные адреса в DLL, чтобы отразить новый
адрес загрузки. Это означает, что каждая страница, содержащая адрес, который необходимо изменить,
будет затронута при перебазировании DLL. Более того, чтобы стать доступной для записи, страницу
необходимо скопировать и сохранить в файле подкачки. На этом этапе страница становится частной для
процесса и не может быть передана другим процессам.

Чтобы избежать снижения производительности при перебазировании, вы можете явно указать


предпочтительный базовый адрес, используя ключ компилятора /baseaddress для каждой библиотеки
DLL в вашем приложении. Назначьте базовые адреса библиотекам DLL, начиная с некоторой заранее
определенной начальной точки (например, 0x10000000), и оставьте промежутки между адресами,
которые достаточно велики, чтобы дать некоторое пространство для роста DLL, которая будет лежать в
этом диапазоне.

Образы NGen, как правило, значительно больше, чем изображения промежуточного языка (IL), и это
увеличивает влияние перебазирования. NGen устанавливает для создаваемого образа тот же базовый
адрес, что и адрес, указанный в IL-образе. Поэтому, если вы решите использовать NGen для своих
сборок, то при назначении базовых адресов для ваших IL-образов вы должны оставить достаточно места
для размещения NGen-образов. Обычно для образа NGen потребуется в два-три раза больше места, чем
для эквивалентного образа IL.

Вы можете убедиться, что назначение базового адреса работает правильно, проверив фактические
адреса загрузки и сравнив их с предпочтительными адресами загрузки. Для этого можно использовать
пример приложения списка задач [Link] и дампер двоичных файлов Microsoft COFF [Link]. Эта
команда показывает фактический адрес загрузки для каждой DLL, загруженной в контексте приложения с
идентификатором процесса 1240:

Копировать
tlist 1240

Dumpbin позволяет вам проверить предпочтительный базовый адрес вашей DLL, как показано здесь:

Копировать
dumpbin /headers [Link]

Посмотрев на вывод этой команды, вы увидите ДОПОЛНИТЕЛЬНЫЕ ЗАГОЛОВКИ с указанием базы


изображений, примерно так:

Копировать
75F70000 image base (75F70000 to 75F78FFF).

Это означает, что предпочтительный базовый адрес [Link] — 75F70000 и что диапазон адресов от
75F70000 до 75F78FFF должен быть доступен для загрузки этой DLL по предпочтительному базовому
адресу.

Ускоренная загрузка пользовательского интерфейса

Восприятие производительности запуска во многом зависит от задержки до появления первого


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

Если вы используете дорогостоящие вызовы сети или базы данных, сделайте их асинхронными в другом
потоке, чтобы избежать блокировки пользовательского интерфейса. В Windows Forms 2.0 компонент
BackgroundWorker можно использовать для передачи работы в фоновый поток. На рисунке
1 BackgroundWorker используется для рекурсивного поиска всех файлов, соответствующих заданному
шаблону (например, «*.txt») в заданной папке и ее подпапках. Фоновый поток обновляет поток
пользовательского интерфейса по мере его выполнения, а поток пользовательского интерфейса
добавляет файлы или подпапки в представление TreeView в ответ на обновления хода
выполнения. Полученный TreeView фильтруется так, что подпапки, не содержащие соответствующих
файлов, не отображаются.

Рис. 1. Использование фонового потока

Копировать
Imports [Link] Public Class Form1 Private DirInfo As DirectoryInfo Private Pattern
As String Private Sub Form1_Load(ByVal sender As [Link], _ ByVal e As
[Link]) Handles [Link] DirInfo =
[Link]("C:\\") Pattern = "*.txt" [Link] =
"Searching for " & Pattern & " files" [Link] = True
[Link] = True
[Link]() End Sub Private Sub BackgroundWorker1_DoWork( _
ByVal sender As [Link], _ ByVal e As [Link]) _
Handles [Link] For Each f As FileInfo In [Link](Pattern)
Dim treeNode As TreeNode = New TreeNode([Link])
[Link](0, treeNode) Next For Each SubDir As
DirectoryInfo In [Link]() Dim treeNode As TreeNode =
GetMatchingFiles(SubDir) If Not treeNode Is Nothing Then
[Link](0, treeNode) End If Next End Sub Private Sub
BackgroundWorker1_ProgressChanged(_ ByVal sender As Object, _ ByVal e As
[Link]) _ Handles
[Link] [Link]((CType([Link],
TreeNode))) End Sub Private Sub BackgroundWorker1_RunWorkerCompleted( _ ByVal sender
As Object, _ ByVal e As [Link]) _ Handles
[Link] [Link] = "Ready" End Sub Private Function
GetMatchingFiles(ByVal dir As DirectoryInfo) _ As TreeNode Dim treeNode As TreeNode =
Nothing For Each f As FileInfo In [Link](Pattern) If treeNode Is Nothing Then
treeNode = New TreeNode([Link]) End If [Link](New TreeNode([Link]))
Next For Each SubDir As DirectoryInfo In [Link]() Dim subNode As TreeNode
= GetMatchingFiles(SubDir) If Not subNode Is Nothing Then If treeNode Is Nothing Then
treeNode = New TreeNode([Link]) End If [Link](subNode) End If Next
Return treeNode End Function End Class

Преимущество использования фонового потока заключается в том, что пользовательский интерфейс


полностью доступен во время поиска. Ваш пользователь может прокручивать список найденных файлов
и разворачивать узлы подпапок, которые были добавлены в TreeView.

Операции, которые должны быть выполнены, но не нужны для отображения первого пользовательского
интерфейса, могут выполняться во время простоя системы или по требованию после отображения
первого пользовательского интерфейса. Например, если у вас есть TabControl, при запуске заполняйте
только самую верхнюю страницу и при необходимости извлекайте информацию для других страниц. Код
на рис. 2 включает форму с TabControl, содержащую шесть страниц TabPages. Я поместил все элементы
управления, необходимые для каждой TabPage, в UserControl и добавил их в коллекцию элементов
управления TabPage, когда TabPage будет отображаться. Для простоты я использовал один и тот же
UserControl для каждой TabPage и лишь немного изменил контекст некоторых дочерних элементов
управления, но вы легко можете создать отдельный UserControl, соответствующий каждой TabPage.

Рис. 2. Инициализация элементов управления в режиме ожидания

Копировать
public partial class Form2 : Form { int currentIndex = 1; public Form2(bool onDemand)
{ InitializeComponent(); if (!onDemand) InitTab(-1); else { InitTab(0);
[Link] += new EventHandler(Application_Idle); } } private void InitTab(int
index) { if (index == -1) { for (int i = 0; i < [Link]; i++)
{ InitTab(i); } return; } // if the TabPage has already been populated if
([Link][index].[Link] > 0) return; UserControl control = new
myTabPage(index); if (control != null) { [Link] = [Link];
[Link][index].[Link](control); } } private void
tabControl1_SelectedIndexChanged( object sender, [Link] e)
{ InitTab([Link]); } private void Application_Idle(object sender,
EventArgs e) { if (currentIndex >= [Link]) { [Link] -= new
EventHandler(Application_Idle); } else InitTab(currentIndex++); } }
Для самой верхней страницы я добавил UserControl в конструктор формы. Для других страниц я сделал
это в ответ на срабатывание события [Link], когда соответствующая страница
будет показана. Пользователь испытает некоторую задержку при первом выборе TabPage. Тогда в
следующий раз, когда он будет выбран, задержки не будет, поскольку TabPage уже инициализирован.

В этом примере я намеренно замедлил создание TabPages, показывая ListView с большим количеством
элементов. Если я инициализирую все страницы вкладок заранее, время, необходимое для отображения
формы, в моей системе (Pentium III, 800 МГц, 512 МБ ОЗУ) составит девять секунд. При заполнении
TabPages по требованию отображение каждой TabPage в первый раз занимает около 1,5 секунд. Если
заполнена только самая верхняя страница, вся форма отображается за 1,5 секунды.

Я могу пойти еще дальше, прослушивая событие [Link] и заполняя одну TabPage за раз, когда
происходит событие Idle. Когда отображается форма или отображается новая TabPage, пользователю
потребуется некоторое время, чтобы посмотреть на форму и переместить мышь, чтобы выбрать другую
страницу. Во время этой органической задержки в работе приложения будет обработано событие Idle и
будут заполнены дополнительные страницы TabPages.

Если другие методы не могут в достаточной степени повысить производительность запуска, вы можете
рассмотреть возможность использования экранов-заставок и индикаторов выполнения, чтобы держать
пользователей в курсе, как в этом примере в разделе « Создание формы экрана-заставки в отдельном
потоке» . Если вы пишете свое приложение на Visual Basic, вы можете использовать свойство
[Link] .NET Framework 2.0 (см. Свойство [Link] ).

Контроль населения

Многие элементы управления Windows Forms, такие как ListView, TreeView и Combobox, отображают
коллекции элементов. Добавление слишком большого количества элементов в эти коллекции может
стать узким местом, если не будут применены оптимизации. К счастью, в Windows Forms в .NET
Framework 2.0 было включено несколько улучшений контрольной совокупности. К ним относятся
использование методов BeginUpdate и EndUpdate внутри методов AddRange, оптимизация заполнения
TreeView с помощью AddRange и оптимизация отсортированного заполнения всех элементов
управления.

Наиболее распространенной причиной медленного заполнения элемента управления является


перерисовка элемента управления после каждого изменения. Ряд элементов управления Windows Forms
реализуют методы BeginUpdate и EndUpdate, которые подавляют перерисовку при манипулировании
базовыми данными или свойствами элемента управления. Использование этих методов позволяет
вносить существенные изменения в элемент управления (например, добавлять элементы в его
коллекцию), избегая при этом постоянной перерисовки. Вот пример использования этих методов:

Копировать
[Link](); for(int i = 0; i < 10000; i++) { ListViewItem listItem = new
ListViewItem("Item"+[Link]() ); [Link](listItem); }
[Link]();

Массовые операции всегда более эффективны, чем отдельные изменения. Предпочтительный способ
добавления элементов в коллекции элементов управления, такие как ComboBox, ListView или TreeView, —
использовать метод AddRange, который позволяет одновременно добавлять массив предварительно
созданных элементов. Windows Forms выполняет все возможные оптимизации, чтобы сделать эту
операцию эффективной. Часто это просто означает, что вместо вас будут вызваны BeginUpdate и
EndUpdate. Однако в некоторых случаях будут выполнены дополнительные оптимизации. Например, для
элемента управления TreeView эти оптимизации значительны.

Заполнение ListView работает значительно быстрее, если оно выполняется после создания дескриптора
ListView. Если вам нужно отобразить форму, имеющую ListView со многими элементами, добавьте
элементы в ListView в обработчике событий [Link] или [Link]. На этом этапе дескриптор ListView
уже создан. Если вы добавите элементы ListView в конструктор формы (то есть до создания дескриптора
ListView), элементы будут добавлены быстро, но отображение формы займет значительное время из-за
медленной отрисовки ListView.

В отличие от ListView, TreeView заполняется значительно быстрее, если узлы добавляются до создания
дескриптора. Если вы используете [Link], вы не заметите разницы; ваш TreeView будет
быстро заполнен независимо от того, когда вы это сделаете. Однако если вы используете метод Add и
добавляете узлы в конструктор формы до создания дескриптора TreeView, ваш TreeView будет заполнен и
отображен так же быстро, как если бы вы использовали метод AddRange.

Оптимизация привязки данных

При заполнении элементов управления с привязкой к данным, таких как ComboBox или ListBox, более
эффективно устанавливать свойство DataSource последним, после установки ValueMember и
DisplayMember. В противном случае ваш элемент управления будет повторно заполнен в результате
изменения ValueMember:

Копировать
[Link] = "Name"; [Link] = "Name";
[Link] = test;

По этой причине только что показанный код более эффективен, чем этот:

Копировать
[Link] = test; [Link] = "Name"; [Link]
= "Name"

[Link] и [Link] — это два метода, которые позволяют


временно приостановить и возобновить привязку данных. SuspendBinding предотвращает передачу
изменений в источник данных до тех пор, пока не будет вызван ResumeBinding.

Эти методы предназначены для использования с простыми сценариями привязки, такими как привязка
данных TextBox или ComboBox. Код на рисунке 3 демонстрирует, что итерация, которая
приостанавливает и возобновляет привязку, работает примерно в пять раз быстрее, чем итерация,
которая этого не делает.

Рис. 3. Приостановка и возобновление привязки данных

Копировать
private void button1_Click(object sender, EventArgs e) { DataTable tbl =
[Link] as DataTable; Stopwatch sw1 = new Stopwatch();
[Link](); for (int i = 0; i < 1000; i++) { [Link][0][0] = "row " + [Link]();
} [Link](); Stopwatch sw2 = new Stopwatch(); [Link]();
[Link](); for (int i = 0; i < 1000; i++) { [Link][0][0]
= "suspend row " + [Link](); } [Link](); [Link]();
[Link]([Link]("Trial 1 {0}\r\nTrial 2 {1}", [Link],
[Link])); } private void Form1_Load(object sender, EventArgs e)
{ DataTable tbl = new DataTable(); [Link]("col1"); [Link]("one");
[Link]("two"); [Link] = tbl;
[Link]("Text", this.bindingSource1, "col1"); }

Элементы управления, реализующие сложную привязку данных, такие как элемент управления
DataGridView, обновляют свои значения на основе событий изменения, таких как ListChanged, поэтому
вызов SuspendBinding не помешает им получать изменения в источнике данных. Эти методы можно
использовать в сложных сценариях привязки, если вы подавляете события ListChanged, задав для
свойства RaiseListChangedEvents значение false.
Легкий макет

Такие изменения, как изменение размера или выравнивание элемента управления, также могут вызвать
проблемы. Например, когда дочерние элементы управления добавляются в форму или ToolStrip,
запускается событие [Link]. Изменение размера или местоположения дочернего элемента
управления вызовет событие Layout в родительском элементе управления. Изменение размера шрифта
также вызовет событие Layout. В ответ на событие Layout форма масштабируется и упорядочивает
элементы управления. Обработка слишком большого количества событий макета при создании или
изменении размера элемента управления может существенно повлиять на производительность.

Код компонента, созданный конструктором Windows Forms, начинается с SuspendLayout и заканчивается


ResumeLayout. Это сделано для того, чтобы избежать выполнения макета формы во время ее создания и
заполнения элементами управления. Метод SuspendLayout позволяет выполнять несколько действий с
элементом управления без создания события Layout для каждого изменения. Используйте этот метод,
когда это возможно, чтобы свести к минимуму количество событий макета.

Помните, что SuspendLayout предотвращает выполнение событий Layout только для этого конкретного
элемента управления. Например, если на панель добавляются элементы управления, SuspendLayout и
ResumeLayout должны вызываться для панели, а не для родительской формы.

Изменение таких свойств, как «Границы», «Размер», «Местоположение», «Видимый» и «Текст» для
элементов управления AutoSize, вызовет событие Layout. Изменить эти свойства в [Link] будет еще
дороже, поскольку к тому времени будут созданы все дескрипторы и, следовательно, будет обработано
много сообщений. Это еще одно место, где вам следует добавить SuspendLayout и ResumeLayout, чтобы
предотвратить возникновение дополнительных событий Layout. Если возможно, внесите все эти
изменения в InitializeComponent — таким образом потребуется только одно событие Layout.

Несколько свойств определяют размер или расположение элемента управления: Ширина, Высота,
Сверху, Снизу, Слева, Справа, Размер, Местоположение и Границы. Установка [Link], а затем
[Link] требует двойного объема работы по сравнению с установкой их обоих вместе через
[Link]. Выполнение кода, показанного на рисунке 4, демонстрирует разницу.

Рис. 4. Тестирование производительности макета

Копировать
private void button1_Click(object sender, EventArgs e) { Stopwatch sw = new
Stopwatch(); [Link](); for (int i = 1; i < 1000; i++) { [Link] = i;
[Link] = i; } [Link](); Stopwatch sw2 = new Stopwatch(); [Link](); for
(int i = 1; i < 1000; i++) { [Link] = new Size(i, i); } [Link]();
[Link]([Link]("Trial 1 {0}\r\nTrial 2 {1}", [Link],
[Link])); }

Если дескрипторы созданы, вы заметите разницу, даже если используете вызовы SuspendLayout и
ResumeLayout. SuspendLayout предотвращает вызов Windows Forms OnLayout. Это не помешает отправке
и обработке сообщений об изменении размера. Вам следует попытаться установить свойство, которое
отражает большую часть имеющейся у вас информации. Если вы просто меняете размер, установите
Size; если вы меняете размер и местоположение, измените границы.

Картина Производительность

Если ваше приложение Windows Forms интенсивно использует рисование, оно может выиграть от
повышения производительности рисования. Во-первых, сведите к минимуму работу, выполняемую при
каждом событии Paint. Сделайте обработчик событий Paint максимально облегченным, исключив из него
все дорогостоящие операции. Например, если вам нужно визуализировать текст на основе вашего
текущего ClientSize, вы можете создать объект Font соответствующего размера в обработчике событий
Resize, а не в каждом событии Paint. Если вам нужно создать кисть для рисования, вы можете кэшировать
ее, а не повторно создавать.

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

Если это не работает достаточно хорошо, вы можете использовать структуру ClipRectangle, включенную в
параметр PaintEventArgs события OnPaint. Например, вы можете нарисовать часть изображения, которое
было признано недействительным, выполнив следующие действия:

Копировать
Protected Overrides Sub OnPaint(_ ByVal e As [Link])
[Link]([Link], [Link], _ [Link],
[Link]) End Sub

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

Windows Forms и базовая архитектура Win32® предоставляют два уровня рисования для любого
элемента управления: фоновый и передний план. Функция [Link] отвечает за
рисование фоновых эффектов (обычно фонового изображения и цвета фона), а функция [Link]
отвечает за рисование эффектов переднего плана (изображений и текста). Если ваш элемент управления
не использует фоновое изображение или цвет фона, а вместо этого настраиваемо рисует все в OnPaint,
использование стиля элемента управления «Непрозрачный» может повысить производительность,
пропуская неиспользуемую логику рисования. В частности, если для элемента управления
[Link] установлено значение true, функция OnPaintBackground будет пропущена,
поскольку предполагается, что функция OnPaint выполнит всю работу по рисованию (включая рисование
фона). Кроме того,

Текст и изображения

Вы можете измерять и рисовать текст в элементе управления Windows Forms в .NET Framework 2.0 с
помощью класса TextRenderer. TextRenderer имеет два метода: MeasureText и DrawText, каждый из
которых имеет несколько перегрузок. Эффективность операций отрисовки зависит от выбранных вами
перегрузок и параметров, заданных в аргументе TextFormatFlags.

Для измерения однострочных строк лучше не использовать флаг [Link]. Когда этот
флаг установлен, GDI выполняет надежный, но дорогостоящий алгоритм для определения мест, на
которые необходимо разбить текст. Аналогичным образом, не используйте параметры
[Link] и [Link], если к
объекту Graphics не применялись обрезки или преобразования, поскольку эти параметры вызывают
выполнение некоторых дорогостоящих вычислений.

Лучше использовать перегрузки метода TextRenderer, которые не получают IDeviceContext в качестве


аргумента. Эти методы более эффективны, поскольку они используют контекст устройства памяти,
совместимый с кэшированным экраном, а не извлекают собственный дескриптор контекста устройства
из внутреннего DeviceContext и создают внутренний объект для его упаковки.

Если ваше приложение отображает файлы изображений и содержит альфа-компонент смешивания, вы


можете значительно повысить производительность, предварительно преобразуя изображения в
специальный предварительно умноженный растровый формат. Если изображение имеет альфа-
компонент, его цветовые компоненты должны быть умножены на альфа при отображении
изображения. Предварительная визуализация изображения с предварительно умноженной альфа-
каналом исключает несколько (три для изображений RGB) операций умножения для каждого пикселя
изображения.

Чтобы использовать этот метод, сначала загрузите изображение из файла, а затем преобразуйте его в
растровое изображение с помощью PixelFormat.Format32bppPArgb. На рис. 5 этот метод используется
для установки фонового изображения на элементе управления.

Рис. 5. Рендеринг фонового изображения

Копировать
Public Overrides Property BackgroundImage() As [Link] Get Return
[Link] End Get Set(ByVal value As [Link]) [Link] = New
Bitmap([Link], [Link], _ [Link].Format32bppPArgb) Dim g As Graphics
= [Link]([Link]) [Link] =
[Link] [Link](value, New Rectangle(0, 0,
[Link], _ [Link])) [Link]() End Set End Property

В Windows Forms 2.0 свойство BackgroundImage имеет сопутствующее свойство


BackgroundImageLayout. В предыдущих версиях .NET Framework фоновое изображение автоматически
размещалось Windows Forms, позволяя изображению повторяться по всей клиентской области. Новое
свойство BackgroundImageLayout позволяет вам установить макет фонового изображения на «Плитка»,
«Центр», «Растянуть» или «Масштаб». Чтобы сохранить совместимость приложения с предыдущими
версиями .NET Framework, макет плитки по-прежнему используется по умолчанию.

Помимо добавления богатого набора функций фонового изображения в Windows Forms, значения
BackgroundImageLayout, отличные от значений по умолчанию, повышают производительность
рисования фонового изображения. Основным недостатком настройки Tile является то, что для
повторения рисунка изображения требуется текстурная кисть. Создание текстурной кисти обходится
дорого, поскольку предполагает сканирование всего изображения. Другие макеты просто используют
метод [Link], что приводит к значительному повышению производительности.

Для макетов, отличных от Tile, установка свойств BackgroundImage и BackgroundImageLayout может


автоматически включить свойство DoubleBuffered. В частности, если изображение имеет некоторую
прозрачность (как обнаружено с помощью ImageFlagsHasAlpha), элемент управления начнет
использовать двойную буферизацию для повышения производительности. Для макета Tile свойство
DoubleBuffered не включается автоматически, но его всегда можно включить вручную.

Двойная буферизация — это метод, позволяющий ускорить рисование и сделать его более плавным за
счет уменьшения мерцания. Основная идея состоит в том, чтобы взять операции рисования,
используемые для рисования вашего элемента управления, и применить их к внеэкранному
буферу. После завершения всех операций рисования этот буфер рисуется в элементе управления как
одно изображение. Обычно это уменьшает мерцание и ускоряет работу приложения. Такого поведения
можно добиться в .NET Framework 2.0, установив для стиля элемента управления значение
OptimizedDoubleBuffer, что эквивалентно настройке стилей DoubleBuffer и UserPaint в предыдущих
версиях Framework.

Чтобы полностью включить двойную буферизацию, необходимо также установить для


AllPaintingInWmPaint значение true. Если он установлен, оконное сообщение WM_ERASEBKGRND
игнорируется, а методы OnPaintBackground и OnPaint вызываются непосредственно из сообщения
WM_PAINT. Если вы установите для DoubleBuffering и AllPaintingInWmPaint значение true, то
OnPaintBackground и OnPaint будут вызываться с одним и тем же буферизованным графическим
объектом, и все будет рисоваться за кадром вместе и обновляться одновременно.

Чтобы воспользоваться преимуществами двойной буферизации, вам необходимо установить для обоих
этих стилей значение true или установить свойство DoubleBuffered в вашем элементе управления,
например:
Копировать
SetStyle([Link] | [Link],
true); // OR DoubleBufferred = true; // sets both flags

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

Другим побочным эффектом установки AllPaintingInWmPaint в значение true может быть видимый белый
след на дочерних элементах управления вашего элемента управления, когда окно, принадлежащее
другому приложению, перемещается по вашему окну. Это может произойти из-за того, что дочерние
окна управления получают много сообщений WM_ERASEBKGND и только одно сообщение
WM_PAINT. Поскольку сообщения WM_ERASEBKGND игнорируются, перерисовка для них происходит
слишком редко.

Применение техник рисования

Экран на рис. 6 демонстрирует анимацию текста над пользовательским элементом управления, для
которого установлено фоновое изображение. Вы можете установить различные параметры рисования,
чтобы увидеть, как они влияют на плавность и скорость рисования (этот пример приложения доступен
для загрузки с веб-сайта журнала MSDN Magazine ).

Рис. 6** Пример приложения для тестирования производительности**

В основной форме имеется пользовательский элемент управления DrawingSurface, который имеет


фоновое изображение, текст («Hello World!» по умолчанию) и два таймера. Первый таймер управляет
скоростью анимации и по умолчанию установлен на 10 мс. Это можно изменить с помощью настройки
интервала. По истечении интервала текст перерисовывается в другом положении на поверхности
рисования. В зависимости от эффекта анимации текст либо подпрыгивает, либо вращается.

Второй таймер используется для обновления фактической скорости анимации, отображаемой в правом
нижнем углу поверхности рисования. Под фактической скоростью анимации я подразумеваю количество
событий рисования, обработанных за предыдущую секунду. Фактическая скорость анимации может быть
ниже желаемой скорости анимации. Например, интервал в 10 мс означает, что вы хотите, чтобы текст
менял свое положение 100 раз в секунду.
Параметры макета фонового изображения позволяют сравнивать производительность рисования между
флажками «Плитка» и другими флажками макета изображения. Я выбрал макет по центру только в
качестве примера (масштаб или растягивание будут иметь тот же эффект, что и центр). В этом
приложении я выровнял изображение так, чтобы оно соответствовало размеру поверхности рисования,
независимо от выбранного макета изображения. Однако когда фон нарисован, расположение плиток
влияет на производительность. Кроме того, когда выбран макет «Центр», для вас автоматически
устанавливается параметр DoubleBuffered. Это не тот случай, когда выбран макет плитки.

Вы можете увидеть, как различные параметры влияют на производительность, наблюдая за эффектами


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

В моих тестах наилучшие результаты были достигнуты, когда применялись методы двойной буферизации
и интеллектуальной отмены, для макета фонового изображения было установлено значение «Центр»
(или, точнее, любой макет, кроме плитки), а фоновое изображение предварительно визуализировалось с
использованием PixelFormat.Format32bppPArgb. При использовании всех этих техник картина получается
плавной и без мерцания. Кроме того, анимация работает быстрее.

Управление ресурсами

Физическая память, используемая вашим приложением, является важным показателем


производительности. Вам следует использовать как можно меньше памяти и ресурсов, чтобы как можно
больше оставалось для других процессов. Речь идет не только о том, чтобы быть хорошим
гражданином; ваше приложение выиграет от меньшего объема памяти, и это преимущество может быть
значительным, если использование памяти достаточно велико, чтобы потреблять доступную физическую
память и переводить машину в режим подкачки. Но даже если вы ориентируетесь на
высокопроизводительные машины и подкачка не является для вас основной угрозой, вам следует
использовать память с умом. Стоимость управления памятью в среде CLR может быть значительной для
приложений, которые небрежно распределяют память.

Объем памяти, сбор мусора и управление собственными ресурсами, такими как дескрипторы окон или
дескрипторы GDI, взаимозависимы для приложений Windows Forms. Неспособность освободить
неуправляемые ресурсы увеличивает нагрузку на сборщик мусора (GC). Windows Forms пытается
отслеживать использование дескрипторов и может принудительно выполнить дополнительные циклы
сборки мусора, вызывая [Link], если существует опасность нехватки ресурсов. Кроме того,
управляемые объекты, содержащие эти ресурсы, должны быть финализированы сборщиком мусора, что
обходится дороже, чем их упреждающее освобождение в вашем приложении через интерфейс
IDisposable.

Если время существования объекта явно известно, связанные с ним неуправляемые ресурсы должны
быть освобождены. Если используемый вами класс реализует шаблон Dispose и вы явно знаете, когда
закончите работу с объектом, обязательно вызовите Dispose. В методе Dispose вы вызовете тот же код
очистки, что и в Finalizer, и сообщите GC, что ему больше не нужно финализировать объект, вызвав
метод [Link]. Это повысит производительность, поскольку все объекты, на которые нет
ссылок в нулевом поколении, будут собраны в течение одного цикла сборки мусора. Если собираемый
объект необходимо финализировать, для его сбора потребуется как минимум два цикла GC.

Объекты, реализующие IDisposable, обычно делают это, потому что они удерживают ресурсы, которые
должны быть детерминированно освобождены. Например, элементы управления Windows Forms
содержат дескрипторы Windows, которые необходимо освободить. Если вы добавляете и удаляете
элементы управления из формы динамически, вам следует вызвать для них Dispose — в противном
случае вы накопите дополнительные нежелательные дескрипторы в своем процессе. Вам нужно удалить
только самый верхний элемент управления, поскольку его дочерние элементы будут удалены
автоматически.
Если вы показываете форму модально (с помощью метода ShowDialog), вам необходимо удалить ее
после закрытия. В этом случае он не удаляется автоматически, чтобы вы могли узнать его состояние
после закрытия. Обратите внимание, что другие формы автоматически удаляются при закрытии.

Графические объекты, такие как кисти, перья и шрифты, также реализуют IDisposable, поскольку они
удерживают дескрипторы GDI. Обычно эти объекты создаются в методе OnPaint, который вызывается
каждый раз, когда элемент управления необходимо перерисовать. Если вы не удалите их немедленно, вы
можете накопить большое количество этих объектов и удерживать множество дескрипторов GDI до того,
как эти объекты будут завершены сборщиком мусора. Если вы пишете код на C# или Visual Basic,
оператор using — удобный способ сделать это:

using (Pen p = new Pen([Link], 1)) { [Link](p, 0,0, 10,10); } Using p


As Pen = New Pen([Link], 1) [Link](p, 0,0, 10,10) End Using

Если вы используете C++, его семантика распределения стека также очень полезна в этом отношении:

Копировать
Pen p(Color::Red, 1); e->Graphics->DrawLine(%p, 0,0, 10,10);

Вам следует стараться избегать вызова [Link], если это возможно. ГХ является самонастраивающимся
и настраивается в соответствии с требованиями приложения к памяти. В большинстве случаев
программный вызов GC будет препятствовать автоматической настройке. Единственный случай, когда
[Link] может быть полезен, — это когда вы знаете, что только что освободили много памяти и хотите,
чтобы она была собрана немедленно. Однако в целом более эффективно позволить GC просто управлять
собой.

Двенадцать советов по производительности

 Загружайте меньше модулей при запуске


 Предварительная компиляция сборок с использованием NGen
 Размещайте сборки со строгими именами в GAC.
 Избегайте конфликтов базовых адресов
 Избегайте блокировки в потоке пользовательского интерфейса
 Выполнить ленивую обработку
 Заполнение элементов управления быстрее
 Больше контроля над привязкой данных
 Уменьшить перекраску
 Используйте двойную буферизацию
 Управление использованием памяти
 Используйте отражение с умом

Если ваша форма имеет более 200 дочерних элементов управления, вы можете подумать о том, как
сделать ее более эффективной. Каждый элемент управления потребляет собственные ресурсы, а
управление большим количеством элементов управления обходится дорого. Вместо этого рассмотрите
возможность использования ListView, TreeView, DataGridView, ListBox, ToolStrip и MenuStrip. Отдельные
элементы этих элементов управления обычно не требуют собственного дескриптора для поддержки
каждого элемента.

Если физическая память дефицитна и происходит подкачка, использование физического диска будет
высоким (поскольку страницы записываются на диск). В этом случае ваше приложение будет работать
невыносимо медленно, и вам будет крайне необходимо сократить использование памяти. В этом вам
поможет инструмент профилирования в Visual Studio Team System. Вы также можете использовать этот
инструмент для просмотра управляемого распределения памяти или воспользоваться поддержкой
профилирования CLR ( CLR Profiler: No Code Can Hide from the Profiling API в .NET Framework 2.0 ). Также
вы можете использовать инструмент Virtual Address Dump (VaDump), чтобы просмотреть рабочий набор
процессов:
Копировать
VaDump –sop <pid>

Заключение

Боковая панель «Двенадцать советов по производительности» предоставляет вам набор способов


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

Вам также может понравиться