image Привет, Хаброжители! В издании рассмотрены все темы, связанные с разработкой на C#. В начале книги вы ознакомитесь с основами C#, в том числе с объектно-ориентированным программированием, а также с новыми возможностями C# 8.0. Несколько глав посвящено .NET Standard API, применяемым для запроса данных и управления ими, отслеживания производительности и ее повышения, работы с файловой системой, асинхронными потоками, сериализацией и шифрованием. Кроме того, на примерах кроссплатформенных приложений вы сможете собрать и развернуть собственные. Например, веб-приложения с использованием ASP.NET Core или мобильные приложения на Xamarin Forms.

Также вы познакомитесь с технологиями, применяемыми при создании приложений Windows для ПК, в частности с Windows Forms, Windows Presentation Foundation (WPF) и Universal Windows Platform (UWP).

Улучшение производительности и масштабируемости с помощью многозадачности


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

В этой главе:

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

Процессы, потоки и задачи


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

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

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

Потоки имеют свойства Priority и ThreadState. Кроме того, существует класс ThreadPool, предназначенный для управления пулом фоновых рабочих потоков, как показано на следующей схеме (рис. 13.1).

image

Если вы как разработчик имеете дело со сложными действиями, которые должны быть выполнены вашим кодом, и хотите получить полный контроль над ними, то можете создавать отдельные экземпляры класса Thread и управлять ими. При наличии одного основного потока и нескольких небольших действий, которые можно выполнять в фоновом режиме, вы можете добавить экземпляры делегатов, указывающие на эти фрагменты, реализованные в виде методов в очередь, и они будут автоматически распределены по потокам с помощью пула потоков.
Дополнительную информацию о пуле потоков можно получить на сайте docs.microsoft.com/ru-ru/dotnet/standard/threading/the-managed-thread-pool.

Потокам, возможно, придется конкурировать и ждать доступа к общим ресурсам, таким как переменные, файлы и объекты баз данных.

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

image

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

Мониторинг производительности и использования ресурсов


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

Оценка эффективности типов


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

  • функциональность — для принятия решения проверяется, предоставляет ли данный тип необходимую вам функциональность;
  • объем памяти — для принятия решения проверяется, сколько байтов памяти потребляет данный тип;
  • производительность — для принятия решения проверяется, насколько быстро работает данный тип;
  • потребности в будущем — этот фактор зависит от возможных требований и возможности поддержки.

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

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

Если же необходимо сохранить только несколько чисел, но выполнить с ними большое количество операций, то лучше выбрать тип, который быстрее всего работает на конкретном ЦПУ.

Ранее упоминалась функция sizeof(), предназначенная для отображения количества байтов, занимаемых в памяти одним экземпляром конкретного типа. При сохранении большого количества значений в более сложных структурах данных, таких как массивы и списки, измерение использования памяти требует способа получше.

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

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

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

Следует принять во внимание еще один показатель — обслуживание. Данная мера показывает, сколько усилий потребуется приложить другому разработчику, чтобы понять и отредактировать ваш код. Если вы прибегаете к неочевидному выбору типа, не объясняя данное действие с помощью комментария, то это может запутать разработчика, открывшего ваш код, чтобы исправить ошибку или добавить какую-либо функциональность.

Мониторинг производительности и использования памяти


В пространстве имен System.Diagnostics реализовано большое количество полезных типов для мониторинга вашего кода. В первую очередь следует рассмотреть тип Stopwatch.

1. Создайте в папке Code папку Chapter13 с двумя подпапками MonitoringLib и MonitoringApp.

2. В программе Visual Studio Code сохраните рабочую область как Chapter13.code-workspace.

3. Добавьте в рабочую область папку MonitoringLib, откройте для нее новую панель TERMINAL (Терминал) и создайте новый проект библиотеки классов, как показано в следующей команде:

dotnet new classlib

4. Добавьте в рабочую область папку MonitoringApp, откройте для нее новую панель TERMINAL (Терминал) и создайте новый проект консольного приложения, как показано в следующей команде:

dotnet new console

5. В проекте MonitoringLib переименуйте файл Class1.cs на Recorder.cs.

6. В проекте MonitoringApp найдите и откройте файл MonitoringApp.csproj и добавьте ссылку на библиотеку MonitoringLib, как показано ниже (выделено полужирным шрифтом):

<Project Sdk="Microsoft.NET.Sdk">

   <PropertyGroup>
      <OutputType>Exe</OutputType>
      <TargetFramework>netcoreapp3.0</TargetFramework>
   </PropertyGroup>

   <ItemGroup>
      <ProjectReference
          Include="..\MonitoringLib\MonitoringLib.csproj" />
      </ItemGroup>

</Project>

7. На панели TERMINAL (Терминал) скомпилируйте проекты, как показано в следующей команде:

dotnet build

Реализация класса Recorder


Тип Stopwatch содержит несколько полезных членов, как показано в табл. 13.1.

image

Тип Process содержит несколько полезных членов, перечисленных в табл. 13.2.

image

Для реализации класса Recorder мы будем использовать классы Stopwatch и Process.

1. Откройте файл Recorder.cs и измените его содержимое, чтобы задействовать экземпляр класса Stopwatch в целях записи времени и текущий экземпляр класса Process для записи использованной памяти, как показано ниже:

using System;
using System.Diagnostics;
using static System.Console;
using static System.Diagnostics.Process;

namespace Packt.Shared
{
   public static class Recorder
{
   static Stopwatch timer = new Stopwatch();
   static long bytesPhysicalBefore = 0;
   static long bytesVirtualBefore = 0;

   public static void Start()

   {
      // очистка памяти, на которую больше нет ссылок,
      // но которая еще не освобождена
     GC.Collect();
     GC.WaitForPendingFinalizers();
     GC.Collect();

     // сохранение текущего использования физической
     // и виртуальной памяти
     bytesPhysicalBefore = GetCurrentProcess().WorkingSet64;
     bytesVirtualBefore = GetCurrentProcess().VirtualMemorySize64;
     timer.Restart();
   }

   public static void Stop()
   {
     timer.Stop();
     long bytesPhysicalAfter = GetCurrentProcess().WorkingSet64;
     long bytesVirtualAfter =
      GetCurrentProcess().VirtualMemorySize64;

    WriteLine("{0:N0} physical bytes used.",
       bytesPhysicalAfter - bytesPhysicalBefore);

    WriteLine("{0:N0} virtual bytes used.",
      bytesVirtualAfter - bytesVirtualBefore);

    WriteLine("{0} time span ellapsed.", timer.Elapsed);

     WriteLine("{0:N0} total milliseconds ellapsed.",
       timer.ElapsedMilliseconds);
   }
 }
}

В методе Start класса Recorder используется «сборщик мусора» (garbage collector, класс GC), позволяющий нам гарантировать, что вся выделенная в настоящий момент память будет собрана до записи количества использованной памяти. Это сложная техника, и ее стоит избегать при разработке прикладной программы.

2. В классе Program в метод Main добавьте операторы для запуска и остановки класса Recorder при генерации массива из 10 000 целых чисел, как показано ниже:

using System.Linq;
using Packt.Shared;
using static System.Console;

namespace MonitoringApp
{
    class Program
    {
      static void Main(string[] args)
      {
        WriteLine("Processing. Please wait...");
        Recorder.Start();

        // моделирование процесса, требующего ресурсов памяти...
        int[] largeArrayOfInts =
           Enumerable.Range(1, 10_000).ToArray();

        // ...и занимает некоторое время, чтобы завершить
        System.Threading.Thread.Sleep(
           new Random().Next(5, 10) * 1000);

        Recorder.Stop();
      }
   }
}

3. Запустите консольное приложение и проанализируйте результат:

Processing. Please wait...
655,360 physical bytes used.
536,576 virtual bytes used.
00:00:09.0038702 time span ellapsed.
9,003 total milliseconds ellapsed

Измерение эффективности обработки строк

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

1. Закомментируйте предыдущий код в методе Main, обернув его символами /* и */.

2. Добавьте в метод Main следующий код. Он создает массив из 50 000 переменных int, а затем конкатенирует их, используя в качестве разделителей запятые, с помощью классов string и StringBuilder:

int[] numbers = Enumerable.Range(1, 50_000).ToArray();

Recorder.Start();
WriteLine("Using string with +");
string s = "";
for (int i = 0; i < numbers.Length; i++)
{
   s += numbers[i] + ", ";
}
Recorder.Stop();

Recorder.Start();
WriteLine("Using StringBuilder");
var builder = new System.Text.StringBuilder();
for (int i = 0; i < numbers.Length; i++)
{
   builder.Append(numbers[i]); builder.Append(", ");
}
Recorder.Stop();

3. Запустите консольное приложение и проанализируйте результат:

Using string with +
11,231,232 physical bytes used.
29,843,456 virtual bytes used.
00:00:02.6908216 time span ellapsed.
2,690 total milliseconds ellapsed.
Using StringBuilder
4,096 physical bytes used.
0 virtual bytes used.
00:00:00.0023091 time span ellapsed.
2 total milliseconds ellapsed.

Исходя из результатов, мы можем сделать следующие выводы:

  • класс string вместе с оператором + использовал около 11 Мбайт физической памяти, 29 Мбайт виртуальной и занял по времени 2,7 с;
  • класс StringBuilder использовал 4 Кбайт физической памяти, 0 виртуальной и занял менее 2 мс.

В нашем случае при конкатенации текста класс StringBuilder выполняется примерно в 1000 раз быстрее и приблизительно в 10 000 раз эффективнее по затратам ресурсов памяти!
Избегайте использования метода String.Concat и оператора + внутри цикла. Вместо этого для конкатенации переменных, особенно в циклах, применяйте класс StringBuilder.

Теперь, когда вы научились измерять производительность и эффективность ресурсов вашего кода, изучим процессы, потоки и задачи.

Об авторе


imageМарк Дж. Прайс — обладатель сертификатов Microsoft Certified Trainer (MCT), Microsoft Specialist: Programming in C# и Microsoft Specialist: Architecting Microsoft Azure Infrastructure Solutions. За его плечами более 20 лет практики в области обучения и программирования.

С 1993 года Марк сдал свыше 80 экзаменов корпорации Microsoft по программированию и специализируется на подготовке других людей к успешному прохождению тестирования. Его студенты — как 16-летние новички, так и профессионалы с многолетним опытом. Марк ведет эффективные тренинги, делясь реальным опытом консультирования и разработки систем для корпораций по всему миру.

В период с 2001 по 2003 год Марк посвящал все свое время разработке официального обучающего программного обеспечения в штаб-квартире Microsoft в американском городе Редмонд. В составе команды он написал первый обучающий курс по C#, когда была только выпущена ранняя альфа-версия языка. Во время сотрудничества с Microsoft он преподавал на курсах повышения квалификации сертифицированных корпорацией специалистов, читая лекции по C# и .NET.

В настоящее время Марк разрабатывает и поддерживает обучающие курсы для системы Digital Experience Platform компании Episerver, лучшей .NET CMS в сфере цифрового маркетинга и электронной коммерции.

В 2010 году Марк получил свидетельство об окончании последипломной программы обучения, дающее право на преподавание. Он преподает старшеклассникам математику в двух средних школах в Лондоне. Кроме того, Марк получил сертификат Computer Science BSc. Hons. Degree в Бристольском университете (Англия).

О научном редакторе


Дамир Арх — профессионал с многолетним опытом разработки и сопровождения различного программного обеспечения: от сложных корпоративных программных проектов до современных потребительских мобильных приложений. Он работал со многими языками, однако его любимым остается C#. Стремясь к совершенствованию процессов, Дамир предпочитает разработку, основанную на тестировании, непрерывной интеграции и непрерывном развертывании. Он делится своими знаниями, выступая на конференциях, ведет блоги и пишет статьи. Дамир Арх семь раз подряд получал престижную премию Microsoft MVP за разработку технологий. В свободное время он всегда в движении: любит пеший туризм, геокэшинг, бег и скалолазание.

Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок

Для Хаброжителей скидка 25% по купону — .NET

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.