Русский
Русский
English
Статистика
Реклама

Net

Перевод Протекающие абстракции и код, оставшийся со времён Windows 98

09.06.2021 14:20:05 | Автор: admin

В конце 1990-х команды разработчиков Windows Shell и Internet Explorer внедрили множество потрясающих и сложных структур, позволяющих использовать расширение оболочки и браузера для обработки сценариев, создаваемых третьими сторонами. Например, Internet Explorer поддерживал концепцию подключаемых протоколов ("Что если какой-то протокол, допустим, FTPS станет таким же важным, как и HTTP?"), а Windows Shell обеспечивала чрезвычайно гибкое множество абстрактного использования пространств имён, что позволяло третьим сторонам создавать просматриваемые папки, в основе которых не лежит файловая система от WebDAV ("ваш HTTP-сервер это папка") до папок CAB ("ваш архив CAB это папка"). Работая в 2004 году проект-менеджером в команде по созданию клипарта, я создал приложение .NET для просмотра клипарта прямо из веб-сервисов Office, и набросал черновик расширения Windows Shell, благодаря которому бы казалось, что огромный веб-архив клипарта Microsoft был установлен в локальной папке системы пользователя.

Вероятно, самым популярным (или печально известным) примером расширения пространства имён оболочки является расширение Compressed Folders, обрабатывающее просмотр файлов ZIP. Compressed Folders, впервые появившиеся в составе Windows 98 Plus Pack, а позже и в Windows Me+, позволяли миллиардам пользователей Windows взаимодействовать с файлами ZIP без скачивания стороннего ПО. Вероятно, это может вас удивить, но эта функция была куплена у третьих лиц Microsoft приобрела интеграцию для Explorer, представлявшую собой побочный проект Дэйва Пламмера, а лежащий в её основе движок DynaZIP разработала компания InnerMedia.

К сожалению, этот код уже давно не обновляли. Очень давно. Судя по временной метке модуля, последний раз он обновлялся на День святого Валентина в 1998 году; я подозреваю, что с тех пор в него вносили незначительные изменения (и одну функцию поддержку имён файлов в Unicode, работающую только для извлечения), но всё равно не секрет, что, как сказал Реймонд Чен, этот код "остался на стыке веков". Это значит, что он не поддерживает такие современные функции, как шифрование AES, а его производительность (время выполнения и степень сжатия) сильно отстают от современных реализаций, созданных третьими сторонами.

Тогда почему же его не обновляли? Отчасти в этом виноват принцип "не сломано не чини": реализация ZIP Folders выживала в Windows в течение 23 лет, и при этом вопли пользователей не становились невыносимыми, то есть их вполне всё устраивало.

К сожалению, есть вырожденные случаи, в которых поддержка ZIP оказывается по-настоящему поломанной. С одной из них я столкнулся на днях. Я увидел интересный пост в Twitter о шестнадцатеричных редакторах с возможностью аннотаций (что полезно при исследовании форматов файлов) и решил попробовать некоторые из них (я решил, что больше всего мне нравится ReHex). Но в процессе этого исследования я скачал portable-версию ImHex и попробовал переместить её в папку Tools на своём компьютере.

Я дважды щёлкнул по файлу ZIP размером 11,5 МБ, чтобы открыть его. Затем я нажал CTRL+A, чтобы выбрать все файлы, а затем (это важно) нажал CTRL+X, чтобы вырезать файлы с буфера обмена.


Затем я создал новую папку внутри C:\Tools и нажал CTRL+V, чтобы вставить файлы. И тут всё пошло наперекосяк Windows больше минуты отображала окно "Calculating", но кроме создания одной подпапки с одним файлом на 5 КБ больше ничего не происходило:


Чего? Я знал, что движок ZIP, который используется в ZIP Folders, не был оптимизирован, но раньше я никогда не видел ничего настолько плохого. Спустя ещё несколько минут распаковался ещё один файл на 6,5 МБ:


Безумие какое-то. Я открыл Диспетчер задач, но никакие процессы не занимали мой 12-поточный процессор, 64 ГБ памяти и NVMe SSD. Наконец, я открыл SysInternals Process Monitor, чтобы разобраться, в чём дело, и вскоре увидел первоисточник происходящего.

После нескольких мелких операций считывания из конца файла (где у файла ZIP хранится индекс), весь файл размером 11 миллионов байт считывался с диска по одному байту за раз:


Присмотревшись повнимательнее, я понял, что почти все операции считывали по одному байту, но время от времени после считывания определённого байта выполнялось считывание 15 байт:


Что же находится в этих любопытных смещениях (330, 337)? Байт 0x50, то есть буква P.


В прошлом мне доводилось писать тривиальный код для восстановления ZIP, поэтому я знал, в чём особенность символа P в файлах ZIP это первый байт маркеров блоков формата ZIP, каждый из которых начинается с 0x50 0x4B. По сути, код считывает файл от начала до конца в поисках конкретного блока размером 16 байт. Каждый раз, когда он встречает P, то просматривает следующие 15 байт, чтобы проверить, соответствуют ли они нужной сигнатуре, и если нет, то продолжает побайтовое сканирование в поисках новой P.

Есть ли что-то особенное в этом конкретном файле ZIP? Да.

Формат ZIP состоит из последовательности записей файлов, за которой идёт список этих записей файлов (Central Directory).

Каждая запись файла имеет собственный локальный заголовок файла, содержащий информацию о файле, в том числе размер, размер в сжатом виде и CRC-32; те же данные повторяются в Central Directory.

Однако формат ZIP позволяет локальным заголовкам файлов пропускать эти данные и записывать их как хвост после сжатых данных. Эта функция полезна при потоковом сжатии мы не знаем окончательный размер в сжатом виде, пока не закончим сжимать данные. Вероятно, большинство файлов ZIP не используют эту функцию, однако в скачанном мной файле она использовалась. (Разработчик сообщил, что этот файл ZIP был создан GitHub CI.)

Мы видим, что в заголовке CRC и размеры равны 0, и что они появляются сразу после сигнатуры 0x08074b50 (дескриптора данных (Data Descriptor)), непосредственно перед локальным заголовком следующего файла:


Бит 0x08 во флаге General Purpose обозначает эту опцию; пользователи 7-Zip могут увидеть её как Descriptor в столбце записи Characteristics:


Исходя из размера операции считывания (1+15 байт), я предполагаю, что код подстраивается под блоки Data Descriptor. Почему он это делает (вместо того, чтобы просто считать те же данные из Central Directory), я не знаю.

Хуже того этот черепаший процесс побайтового считывания всего файла происходит не только один раз он повторяется по крайней мере по одному разу для каждого извлекаемого файла. Усугубляет ситуацию то, что эти данные считываются при помощи ReadFile, а не fread().

В конечном итоге, после 85 миллионов однобайтных считываний монитор процессов зависает:


Перезапустив его и сконфигурировав Process Monitor с Symbols, мы можем исследовать эти однобайтные операции считывания и получить представление о том, что же происходит:


Функцию GetSomeBytes перенагружают вызовы, передающие однобайтный буфер в коротком цикле внутри функции readzipfile. Но если посмотреть ниже по стеку, то становится очевидной первопричина этого хаоса это происходит потому, что после перемещения каждого файла из ZIP в папку файл ZIP должен обновиться для удаления перемещённого файла. Этот процесс удаления по сути своей не быстр (поскольку он приводит к изменению всех последующих байтов и обновлению индекса), а его реализация в функции readzipfile (с этим однобайтным буфером чтения) и вовсе чудовищно медленна.

Если вернуться назад, то стоит заметить, что я нажал CTRL+X, чтобы вырезать файлы, что привело к операции перемещения. Если бы вместо этого я нажал CTRL+C для копирования файлов, то ZIP не выполнял бы операцию удаления при извлечении каждого файла. Время, необходимое для распаковки файла ZIP снизилось бы с получаса до четырёх секунд. Для сравнения: 7-Zip распаковывает файл меньше чем за четверть секунды, хоть и немного жульничает.

И вот здесь и происходит протечка абстракции с точки зрения пользователя, копирование файлов из ZIP (с последующим удалением ZIP) и перемещение файлов из ZIP не кажутся сильно различающимися операциями. К сожалению, абстракция даёт сбой на самом деле, удаление из некоторых файлов ZIP оказывается чрезвычайно медленной операцией, а удаление файла с диска обычно происходит тривиально. Поэтому абстракция Compressed Folder хорошо работает с мелкими файлами ZIP, но даёт сбой с крупными файлами ZIP, которых в наше время становится всё больше.

Хотя довольно легко придумать способы значительного улучшения производительности в этой ситуации, подобный прецедент даёт понять, что вероятность совершенствования кода в Windows мала. Возможно, код изменят к его 25-й годовщине?



На правах рекламы


Если для работы необходим сервер на Windows, то вам однозначно к нам. Создавайте собственную конфигурацию в пару кликов, автоматическая установка винды на тарифах с 2 vCPU, 4 ГБ RAM, 20 ГБ NVMe или выше.

Присоединяйтесь к нашему чату в Telegram.

Подробнее..

Управляем контактами GPIO из C .NET 5 в Linux на одноплатном компьютере Banana Pi M64 (ARM64) и Cubietruck (ARM32)

10.05.2021 12:10:00 | Автор: admin
dotnet libgpiod

Когда заходит речь про программирование на C# .NET для одноплатных компьютеров, то разговоры крутятся только в основном вокруг Raspberry Pi на Windows IoT. А как же Banana/Orange/Rock/Nano Pi, Odroid, Pine64 и другие китайские одноплатные компьютеры работающие на Linux? Так давайте это исправим, установим .NET 5 на Banana Pi BPI-M64 (ARM64) и Cubietruck (ARM32), и будем управлять контактами GPIO из C# в Linux. В первой части серии постов, подключим светодиод и кнопку для отработки прерываний и рассмотрим библиотеку Libgpiod (спойлер, библиотеку так же можно использовать в C++, Python) для доступа к контактам GPIO.

Предисловие


Управление светодиодом и получение событий от кнопки будет реализовано через библиотеку Libgpiod, которая не является частью платформы .NET. Данная библиотека предоставляет доступ к GPIO из любого языка программирования, требуется лишь написание класса обертки.

Данный пост применим не только к платам Banana Pi BPI-M64 и Cubietruck, но и другим, основанных на процессоре ARM архитектуры armv71(32-bit) и aarch64 (64-bit). На Banana Pi BPI-M64 (ARM64) и Cubietruck (ARM32) установлена ОС Armbian версии 21.02.1, основанная на Ubuntu 18.04.5 LTS (Bionic Beaver), ядро Linux 5.10.12. uname: Linux bananapim64 5.10.12-sunxi64 #21.02.1 SMP Wed Feb 3 20:42:58 CET 2021 aarch64 aarch64 aarch64 GNU/Linux

Armbian это самый популярный дистрибутив Linux, предназначенный для одноплатных компьютеров построенных на ARM процессоре, список поддерживаемых плат огромен: Orange Pi, Banana Pi, Odroid, Olimex, Cubietruck, Roseapple Pi, Pine64, NanoPi и др. Дистрибутив Armbain основан на Debian и Ubuntu. Из большого перечня поддерживаемых одноплатных компьютеров можно выбрать то решение, которое лучше всего походит для вашего IoT проекта, от максимально энергоэффективных до высокопроизводительных плат с NPU. И на базе всех этих одноплатных компьютеров, вы сможете реализовать свое решения на платформе .NET и работать с периферийными устройствами из кода на C#.

Что такое GPIO


GPIO(general-purpose input/output) интерфейс ввода/вывода общего назначения. GPIOподключены напрямую к процессоруSoC (System-on-a-Chip Система на кристалле), и неправильное использование может вывести его из строя. Большинство одноплатных компьютеров, кроме обычных двунаправленных Input/Output портов, имеют один или более интерфейсов: UART,SPI,IC/TWI,PWM (ШИМ), но не имеютADC (АЦП). GPIO- порты обычно могут быть сконфигурированны на ввод или вывод (Input/Output), состояние по умолчанию обычноINPUT.

Некоторые GPIO-порты являются просто питающими портами 3.3V, 5V и GND, они не связаны сSoCи не могут использоваться как либо еще.

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

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

Работа с контактами GPIOосуществляется через виртуальную файловую систему sysfs. стандартный интерфейс для работы с контактами sysfs впервые появился с версии ядра 2.6.26, в Linux. Работа с GPIO проходит через каталог /sys/class/gpio путём обращения к файлам-устройствам.

К портам GPIO подключаются:

  • светодиоды;
  • кнопки;
  • реле;
  • температурные и другие датчики;
  • различные периферийные устройства.

Для программирования GPIO существует несколько способов обращения:

  • Посредством файл-устройства GPIO;
  • Используя языки программирования:
    • Через прямое обращение к регистрам чипа;
    • Используя уже готовые библиотеки (libgpiod).


Одноплатный компьютер Banana Pi BPI-M64


Banana Pi BPI-M64 это 64-битный четырехъядерный мини-одноплатный компьютер, поставляемый как решение с открытым исходном кодом. Ядром системы является процессор Allwinner A64 с 4-мя ядрами Cortex-A53 с частотой 1.2 ГГц. На плате размещено 2 ГБ DDR3 SDRAM 733МГц оперативной памяти и 8 ГБ eMMC.

На плате размещен 40-контактный совместимый с Raspberry Pi разъем, который содержит: GPIO (x28), Power (+5V, +3.3V and GND), UART, I2C, SPI. И 40-контактный интерфейс MIPI DSI.

dotnet libgpiod
Banana Pi BPI-M64 и 40-контактный разъем типа Raspberry Pi 3

Наличие 40-контактного разъема типа Raspberry Pi 3 GPIO, существенно облегчает подключение датчиков из-за совпадение назначение контактов с Raspberry Pi 3. Не приходится гадать к какому контакту подключать тот или иной датчик. Указанные в посте датчики (светодиод и кнопка) подключенные к Banana Pi BPI-M64, можно подключать на те же самые контакты другого одноплатного компьютера, на котором тоже есть 40-контактный разъем, типа Raspberry Pi 3 (или к самой Raspberry Pi 3, разницы нет никакой). Единственное, необходимо изменить номера контактов (линий, ножка процессора) в программном коде, т.к. они зависят от используемого процессора. Но легко определяются но названию контакта. Плата Cubietruck (ARM32) приведена для проверки совместимости и работы кода на 32-разрядных ARM процессорах.

Banana Pi BPI-M64 GPIO Header Position
Позиция [1] 3V3 power соответствует позиции на плате со стрелочкой

Формула для вычисления номера GPIOXX
Для обращение к контактам из C# кода необходимо знать порядковый номер (линия, порт) физической ножки процессора SoC(для Allwinner). Эти данные в спецификациях отсутствую, т.к. порядковый номер получаем путем простого расчета. Например, из схемы возьмем 32-контакт на разъеме типа Raspberry Pi. Название контакта PB7, для получения номера контакта на процессоре произведем расчет по формуле:
(позиция буквы в алфавите 1) * 32 + позиция вывода.Первая буква не учитывается т.к. P PORT, позиция буквы B в алфавите = 2, получаем (2-1) * 32 + 7 = 39. Физический номер контакта PB7является номер 39. У каждого разработчика SoC может быть свой алгоритм расчета номера контактов, должен быть описан в Datasheet к процессору.

Banana Pi BPI-M64 GPIOXX
Контакт PB7 на процессоре Allwiner A64, номер ножки 39

Библиотеки .NET IoT


До того как напишем первую программу на C# по управления GPIO, необходимо рассмотреть пространство имен входящих в dotnet/iot. Все используемые библиотеки добавляются через Nuget пакеты. Подробно рассмотрим драйвера для получения доступа к контактам GPIO одноплатного компьютера. Код на C# взаимодействует с GPIO через специальный драйвер, который является абстракцией доступа к GPIO и позволяет переносить исходный код от одного одноплатного компьютера к другому, без изменений.

Пространства имен .NET IoT:

  • System.Device.Gpio. Пакет System.Device.Gpio поддерживает множество протоколов для взаимодействия с низкоуровневыми аппаратными интерфейсами:
    • General-purpose I/O (GPIO);
    • Inter-Integrated Circuit (I2C);
    • Serial Peripheral Interface (SPI);
    • Pulse Width Modulation (PWM);
    • Serial port.


  • Iot.Device.Bindings. Пакет Iot.Device.Bindings содержит:
    • Драйвера и обертки над System.Device.Gpio для различных устройств которые упрощают разработку приложений;
    • Дополнительные драйвера поддерживаемые сообществом (community-supported).


dotnet IoT Library
Стек библиотек .NET IoT

Рассмотрим первую программу типа Hello World, мигание светодиода (Blink an LED):

using System;using System.Device.Gpio;using System.Threading;Console.WriteLine("Blinking LED. Press Ctrl+C to end.");int pin = 18;using var controller = new GpioController();controller.OpenPin(pin, PinMode.Output);bool ledOn = true;while (true){    controller.Write(pin, ((ledOn) ? PinValue.High : PinValue.Low));    Thread.Sleep(1000);    ledOn = !ledOn;}

Разбор кода:

  • using System.Device.Gpio пространство имен для использования контроллера GpioController доступа к аппаратным ресурсам;
  • using var controller = new GpioController() создает экземпляр контроллера для управления контактами GPIO;
  • controller.OpenPin(pin, PinMode.Output) инициализирует контакт pin = 18 на вывод, к 18 контакту подключен светодиод;
  • controller.Write(pin, ((ledOn)? PinValue.High: PinValue.Low)) если ledOn принимает значение True, то PinValue.High присваивает высокое значение 18 контакту и светодиод загорается. На 18 контакт подается напряжение в 3.3V. Если ledOn принимает значение False, то PinValue.Low присваивает низкое значение контакту 18 и светодиод гаснет. На 18 контакт подается напряжение в 0V (или минимальное пороговое для значения 0, может быть немного выше 0V).

Далее остается компиляция под ARM архитектуру: dotnet publish -r linux-arm или dotnet publish -r linux-arm64. Но так работает просто только для Raspberry Pi. При использование одноплатных компьютерах отличных от Raspberry Pi необходимо при инициализации GpioController выбирать драйвер доступа к GPIO.

Драйвера доступа к GPIO из .NET


Классы драйверов доступа к GPIO находятся в пространстве имен System.Device.Gpio.Drivers. Доступны следующие драйвера-классы:

  • HummingBoardDriver GPIO драйвер для платы HummingBoard на процессоре NXP i.MX 6 Arm Cortex A9;
  • LibGpiodDriver этот драйвер использует библиотеку Libgpiod для получения доступа к портам GPIO, заменяет драйвер SysFsDriver. Библиотека Libgpiod может быть установлена на Linux и Armbian, не является аппаратно-зависимой, что позволяет ее использовать для различных одноплатных компьютерах ARM32 и ARM64;
  • RaspberryPi3Driver GPIO драйвер для одноплатных компьютеров Raspberry Pi 3 или 4;
  • SysFsDriver GPIO драйвер работающий поверх интерфейса SysFs для Linux и Unux систем, предоставляет существенно меньше возможностей, чем драйвер LibGpiodDriver, но не требует установки библиотеки Libgpiod. Тот случай, когда хочется просто попробовать помигать светодиодом из C# без дополнительных действий;
  • UnixDriver базовый стандартный класс доступа к GPIO для Unix систем;
  • Windows10Driver GPIO драйвер для ОС Windows 10 IoT. Из поддерживаемых плат только Raspberry Pi, весьма ограниченное применение.

В данном посте будет рассматриваться доступ к GPIO через драйвер LibGpiodDriver. Драйвер SysFsDriver базируется на устаревшем методе работы с GPIO через виртуальную файловую систему SysFs. Для решений IoT, SysFs не подходит по трем серьезным причинам:

  • Низкая скорость работы I/O;
  • Есть проблемы с безопасной работой с GPIO при совместном доступе;
  • При контейнеризации приложения на C# в контейнер придется пробрасывать много путей из файловой системы Linux, что создается дополнительные сложности. При использование библиотеки Libgpiod этого не требуется.

Библиотека Libgpiod предназначена для работы с GPIO не только из .NET кода, но и из Python, C++, и т.д. Поэтому ниже изложенная инструкция по установке библиотеки Libgpiod позволит разработчикам на Python реализовывать подобную функциональность, как и на C#. В состав пакета Libgpiod входят утилиты для работы с GPIO. До создание программы на C#, поработаем с датчиками через эти утилиты.

Схема подключения светодиода (LED) и кнопки


Подключать светодиод и кнопку будем на 40-контактный разъем совместимый с Raspberry Pi 3. Светодиод будет подключен на 33 контакт разъема, название контакта PB4, номер линии 36. Кнопка будет подключен на 35 контакт разъема, название контакта PB6, номер линии 38. Необходимо обратить внимание на поддержку прерывания на контакте PB6 для кнопки. Поддержка прерывания необходима для исключения постоянного опроса линии с помощью CPU. На контакте PB6 доступно прерывание PB_EINT6, поэтому кнопку к этому контакту и подключим. Например, соседний контакт PL12 не имеет прерывание, поэтому подключать кнопку к нему кнопку не будем. Если вы подключаете кнопку и резистор напрямую, то не забывайте в цепь добавить резистор для сопротивления для избежания выгорания порта!

libgpiod Armbian
Схема подключения светодиода (LED) и кнопки к 40-контактному разъему совместимый с Raspberry Pi 3

libgpiod Armbian
Схема назначения контактов к которым подключается светодиод (LED) и кнопка

Интерфейс GPIO ядра Linux


GPIO (General-Purpose Input/Output) является одним из наиболее часто используемых периферийных устройств во встраиваемых системах (embedded system) Linux.

Во внутренней архитектуре ядро Linux реализует доступ к GPIO через модель производитель/потребитель. Существуют драйверы, которые предоставляют доступ к линиям GPIO (драйверы контроллеров GPIO) и драйверы, которые используют линии GPIO (клавиатура, сенсорный экран, датчики и т. д.).

В ядре Linux система gpiolib занимается регистрацией и распределением GPIO. Эта структура доступна через API как для драйверов устройств, работающих в пространстве ядра (kernel space), так и для приложений пользовательского пространства (user space).

libgpiod Armbian
Схема работы gpiolib

Старый путь: использование виртуальной файловой системы sysfs для доступа к GPIO


До версии ядра Linux 4.7 для управления GPIO в пользовательском пространстве использовался интерфейс sysfs. Линии GPIO были доступны при экспорте по пути /sys/class/gpio. Так, например, для подачи сигнала 0 или 1 на линию GPIO, необходимо:

  1. Определить номер линии (или номер ножки процессора) GPIO;
  2. Экспортировать номер GPIO, записав его номер в /sys/class/gpio/export;
  3. Конфигурировать линию GPIO как вывод, указав это в /sys/class/gpio/gpioX/direction;
  4. Установить значение 1 или 0 для линии GPIO /sys/class/gpio/gpioX/value;

Для наглядности установим для линии GPIO 36 (подключен светодиод) из пользовательского пространства, значение 1. Для этого необходимо выполнить команды:

# echo 36 > /sys/class/gpio/export# echo out > /sys/class/gpio/gpio36/direction# echo 1 > /sys/class/gpio/gpio36/value

Этот подход очень простой как и интерфейс sysfs, он неплохо работает, но имеет некоторые недостатки:

  1. Экспорт линии GPIO не связан с процессом, поэтому если процесс использующий линию GPIO аварийно завершит свою работу, то эта линия GPIO так и останется экспортированной;
  2. Учитываю первый пункт возможен совместный доступ к одной и той же линии GPIO, что приведет к проблеме совместного доступа. Процесс не может узнать у ОС используется ли та или иная линия GPIO в настоящий момент;
  3. Для каждой линии GPIO приходится выполнять множество операций open()/read()/write()/close(), а так же указывать параметры (export, direction, value, и т.д.) используя методы работы с файлами. Это усложняет программный код;
  4. Невозможно включить/выключить сразу несколько линий GPIO одним вызовом;
  5. Процесс опроса для перехвата событий (прерываний от линий GPIO) ненадежен;
  6. Нет единого интерфейса (API) для конфигурирования линий GPIO;
  7. Номера, присвоенные линиям GPIO непостоянны, их приходится каждый раз экспортировать;
  8. Низкая скорость работы с линиями GPIO;

Новый путь: интерфейс chardev


Начиная с ядра Linux версии 4.8 интерфейс GPIO sysfs объявлен как deprecated и не рекомендуется к использованию. На замену sysfs появился новый API, основанный на символьных устройствах для доступа к линиям GPIO из пользовательского пространства.

Каждый контроллер GPIO (gpiochip) будет иметь символьное устройство в разделе /dev, и мы можем использовать файловые операции (open(), read(), write(), ioctl(), poll(), close()) для управления и взаимодействия с линиями GPIO. контроллеры GPIO доступны по путям /dev/gpiochipN или /sys/bus/gpiochipN, где N порядковый номер чипа. Просмотр доступных контроллеров GPIO (gpiochip) на Banana Pi BPI-M64:

root@bananapim64:~# ls /dev/gpiochip*/dev/gpiochip0  /dev/gpiochip1  /dev/gpiochip2


libgpiod Armbian
Стек работы библиотеки libgpiod

Несмотря на то, что новый API предотвращает управление линиями GPIO с помощью стандартных инструментов командной строки, таких как echo и cat, он обладает весомыми преимуществами по сравнению с интерфейсом sysfs, а именно:

  • Выделение линий GPIO связано с процессом, который он его использует. При завершение процесса, так же в случае аварийного завершения, линии GPIO используемые процессом освобождаются автоматически;
  • Дополнительно, можно всегда определить какой процесс в данное время использует определенную линию GPIO;
  • Можно одновременно читать и писать в несколько линий GPIO одновременно;
  • Контроллеры GPIO и линии GPIO можно найти по названию;
  • Можно настроить состояние вывода контакта (open-source, open-drain и т. д.);
  • Процесс опроса для перехвата событий (прерывания от линий GPIO) надежен.

Библиотека libgpiod и инструменты управления GPIO


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

Libgpiod(LibraryGeneralPurposeInput/Outputdevice) предоставляет набор API для вызова из своих программ и несколько утилит для управления линиями GPIO из пользовательского режима.

В состав libgpiod входят следующие утилиты:

  • gpiodetect выведет список всех чипов GPIO, их метки и количество линий;
  • gpioinfo выведет информацию о линиях GPIO конкретного контроллера GPIO. В таблице вывода по колонкам будет указано: номер линии, название контакта, направление ввода/вывода, текущее состояние;
  • gpioget считает текущее состояние линии GPIO;
  • gpioset установит значение для линии GPIO;
  • gpiofind выполняет поиск контроллера GPIO и линии по имени;
  • gpiomon осуществляет мониторинг состояния линии GPIO и выводит значение при изменение состояния.

Например, следующая программа написанная на C использует libgpiod для чтения строки GPIO:

void main() {struct gpiod_chip *chip;struct gpiod_line *line;int req, value;chip = gpiod_chip_open("/dev/gpiochip0");if (!chip)return -1;line = gpiod_chip_get_line(chip, 3);if (!line) {gpiod_chip_close(chip);return -1;}req = gpiod_line_request_input(line, "gpio_state");if (req) {gpiod_chip_close(chip);return -1;}value = gpiod_line_get_value(line);printf("GPIO value is: %d\n", value);gpiod_chip_close(chip);}

Библиотеку можно вызывать так же и из кода на C++, Python, C#, и т.д.

Для управления линиями GPIO из терминала необходимо использовать инструменты командной строки, предоставляемые libgpiod. Библиотеку libgpiod и инструменты управления GPIO можно установить скомпилировать из исходного текста и установить.

Установка библиотеки libgpiod и инструментов управления GPIO


Репозитарий библиотеки libgpiod доступ по адресу libgpiod/libgpiod.git. В разделе Download опубликованы релизы библиотеки. На 28.04.2021 последний релиз: v1.6.3.

Библиотеку libgpiod можно установить из репозитария дистрибутива, но скорее всего будет доступна старая версия. Установка libgpiod:

$ sudo apt-get update$ sudo apt-get install -y libgpiod-dev gpiod

Для установки последней актуальной версии необходимо выполнить скрипт установки, который возьмет последнюю версию библиотеки из исходного репозитария. В строке вызова скрипта установки setup-libgpiod-arm64.sh, в качестве первого параметра указать номер версии библиотеки (например: 1.6.3), второй параметр (необязательный) папка установки скрипта. По умолчанию библиотека установится по пути: /usr/share/libgpiod.

Скрипт установки из исходного текста библиотеки libgpiod и утилит для ARM32/ARM64:

$ cd ~/$ sudo apt-get update$ sudo apt-get install -y curl $ curl -SL --output setup-libgpiod-armv7-and-arm64.sh https://raw.githubusercontent.com/devdotnetorg/dotnet-libgpiod-linux/master/setup-libgpiod-armv7-and-arm64.sh$ chmod +x setup-libgpiod-armv7-and-arm64.sh$ sudo ./setup-libgpiod-armv7-and-arm64.sh 1.6.3

Для удаления библиотеки выполнить скрипт: remove-libgpiod-armv7-and-arm64.sh

Если по итогу выполнения скрипта появится надпись Successfully, то значит библиотека и утилиты успешно установлены. Дополнительно для проверки, можно вызвать команду с выводом номера версии библиотеки:

root@bananapim64:~# gpiodetect -vgpiodetect (libgpiod) v1.6.3Copyright (C) 2017-2018 Bartosz GolaszewskiLicense: LGPLv2.1This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law.

Инструменты библиотеки libgpiod


Команда gpiodetect выведет список всех чипов GPIO, их метки и количество линий. Результат выполнения команды:

root@bananapim64:~# gpiodetectgpiochip0 [1f02c00.pinctrl] (32 lines)gpiochip1 [1c20800.pinctrl] (256 lines)gpiochip2 [axp20x-gpio] (2 lines)

gpiochip0 и gpiochip1, это чипы входящие в состав SoC Allwinner A64. gpiochip1 имеет выход на 40-контактный разъем совместимый с Raspberry Pi. Чип gpiochip2 отдельная микросхема управления электропитанием axp209 подключенная по интерфейсу I2C.

Для вывод справки к вызываемой команде необходимо добавлять параметр "--help". Вызов справки для команды gpiodetect. Результат выполнения команды:

root@bananapim64:~# gpiodetect --helpUsage: gpiodetect [OPTIONS]List all GPIO chips, print their labels and number of GPIO lines.Options:  -h, --help:           display this message and exit  -v, --version:        display the version and exit

Команда gpioinfo выведет информацию о линиях GPIO конкретного контроллера GPIO (или всех контроллеров GPIO, если они не указаны).Результат выполнения команды:

root@bananapim64:~# gpioinfo 1gpiochip1 - 256 lines:        line   0:      unnamed       unused   input  active-high...        line  64:      unnamed         "dc"  output  active-high [used]...        line  68:      unnamed "backlightlcdtft" output active-high [used]...        line  96:      unnamed   "spi0 CS0"  output   active-low [used]        line  97:      unnamed       unused   input  active-high        line  98:      unnamed       unused   input  active-high        line  99:      unnamed       unused   input  active-high        line 100:      unnamed      "reset"  output   active-low [used]...        line 120:      unnamed "bananapi-m64:red:pwr" output active-high [used]...        line 254:      unnamed       unused   input  active-high        line 255:      unnamed       unused   input  active-high

В таблице по колонкам указано: номер линии, название контакта, направление ввода/вывода, текущее состояние. Сейчас к Banana Pi BPI-M64 подключен LCD экран ILI9341 на SPI интерфейсе, для подключения используется вариант с управляемой подсветкой, файл DTS sun50i-a64-spi-ili9341-backlight-on-off.dts. В DTS файле контакт PC4 GPIO68 обозначен для управления подсветкой, название backlightlcdtft. Соответственно в выводе команды, указан номер линии 68, название backlightlcdtft, направление вывод, текущее состояние active-high (включено).

Команда gpioset установит значение для линии GPIO. Например, следующая команда попытается выключить подсветку на LCD ILI9341. Команда: gpioset 1 68=0, где 1 gpiochip1, 68 номер линии(контакта), 0 логическое значение, может быть 0 или 1. Результат выполнения команды:

root@bananapim64:~# gpioset 1 68=0gpioset: error setting the GPIO line values: Device or resource busyroot@bananapim64:~#

В результате мы получим ошибку линия занята, т.к. данная линия занята драйвером gpio-backlight.

Попробуем включить светодиод на линии 36, название PB4, номер контакта на 40-контактном разъеме (совместимый с Raspberry Pi) 33. Результат выполнения команды:

root@bananapim64:~# gpioset 1 36=1

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

Команда gpioget считывает текущее состояние линии GPIO. Результат выполнения команды:

root@bananapim64:~# gpioget 1 361

Получили значение 1, т.к. до этого включили светодиод командой gpioset.

Команда gpiomon будет осуществлять мониторинг состояния линии GPIO и выводить значение при изменение состояния. Будем мониторить состояние кнопки, которая подключена на линию 38, название PB4, номер контакта на 40-контактном разъеме (совместимый с Raspberry Pi) 35. Команда: gpiomon 1 38, где 1 gpiochip1, 38 номер линии (контакта). Результат выполнения команды:

root@bananapim64:~# gpiomon 1 38event:  RISING EDGE offset: 38 timestamp: [     122.943878429]event: FALLING EDGE offset: 38 timestamp: [     132.286218099]event:  RISING EDGE offset: 38 timestamp: [     137.639045559]event: FALLING EDGE offset: 38 timestamp: [     138.917400584]

Кнопка несколько раз нажималась. RISING повышение, изменение напряжения с 0V до 3.3V, кнопка нажата и удерживается состояние. FALLING понижение, изменение напряжения с 3.3V до 0V, происходит отпускание кнопки, и кнопка переходит в состояние не нажата.

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

Установка .NET 5.0 для ARM


Одно из лучших нововведений в .NET 5.0 стало увеличение производительности для архитектуры ARM64. Поэтому переход на новую версию не только увеличит производительность решения на базе ARM64, но и увеличит время автономной работы в случае использования аккумуляторной батареи.

Определение архитектуры ARM32 и ARM64 для SoC


.NET 5 устанавливается на одноплатный компьютер в соответствие с архитектурой SoC:

  • ARM32, ARMv7, aarch32, armhf 32-разрядная архитектура ARM. Первые процессоры ARM для встраиваемых систем разрабатывались именно на этой архитектуре. По заявлению компании ARM Holding, в 2022 поддержка 32-битных платформ прекратится, и будет поддерживаться только 64-битная архитектура. Это означает, что компания не будет поддерживать разработку ПО для 32-битных систем. Если конечный производитель устройства пожелает установить 32-битную ОС, то ему придется самостоятельно заняться портированием драйверов с 64-битной архитектуры на 32-битную.
  • ARM64, ARMv8, aarch64 64-разрядная архитектура ARM. Ядра Cortex-A53 и Cortex-A57, поддерживающие ARMv8, были представлены компанией ARM Holding 30 октября 2012 года.

Плата Banana Pi BPI-M64 построена на основе процессора Allwinner A64, содержит в себе 64-битные ядра Cortex-A53, поэтому поддерживает 64-разрядные приложения. Для платы Banana Pi BPI-M64 используется 64-разрядный образ ОС Armbian, поэтому на плату будем устанавливать .NET для 64-разрядных систем ARM.

Плата Cubietruck построена на основе процессора Allwinner A20 содержит в себе 32-битные ядра Cortex-A7, поэтому поддерживает только 32-разрядные приложения. Соответственно на плату устанавливается .NET для 32-разрядных систем.

Если вы не знаете какую версию .NET установить на одноплатный компьютер, то необходимо выполнить команду для получения информации об архитектуре системы: uname -m.

Выполним команду на Banana Pi BPI-M64:

root@bananapim64:~# uname -maarch64

Строка aarch64 говорит о 64-разрядной архитектуре ARM64, ARMv8, aarch64, поэтому установка .NET для 64-х разрядных ARM систем.

Выполним команду на Cubietruck:

root@cubietruck:~# uname -marmv7l

Строка armv7l говорит о 32-разрядной архитектуре ARM32, ARMv7, aarch32, armhf, поэтому установка .NET для 32-разрядных ARM систем.

Редакции .NET 5.0 на ARM


.NET 5.0 можно устанавливать в трех редакциях:

  • .NET Runtime содержит только компоненты, необходимые для запуска консольного приложения.
  • ASP.NET Core Runtime предназначен для запуска ASP.NET Core приложений, так же включает в себя .NET Runtime для запуска консольных приложений.
  • SDK включает в себя .NET Runtime, ASP.NET Core Runtime и .NET Desktop Runtime. Позволяет кроме запуска приложений, компилировать исходный код на языках C# 9.0, F# 5.0, Visual Basic 15.9.

Для запуска .NET программ достаточно установки редакции .NET Runtime, т.к. компиляция проекта будет на компьютере x86.

Загрузить .NET с сайта Microsoft можно по ссылке Download .NET 5.0.

Установка .NET Runtime


На странице Download .NET 5.0. можно узнать текущую актуальную версию .NET. В первой колонке Release information будет указана версия: v5.0.5 Released 2021-04-06. Версия номер: 5.0.5. В случае выхода более новый версии .NET, ниже в скрипте в строке export DOTNET_VERSION=5.0.5, нужно будет заменить номер версии на последний. Выполним скрипт установки, в зависимости от разрядности системы ARM32 (Cubietruck) или ARM64(Banana Pi BPI-M64):

ARM64

$ cd ~/$ apt-get update && apt-get install -y curl$ export DOTNET_VERSION=5.0.5$ curl -SL --output dotnet.tar.gz https://dotnetcli.azureedge.net/dotnet/Runtime/$DOTNET_VERSION/dotnet-runtime-$DOTNET_VERSION-linux-arm64.tar.gz \&& mkdir -p /usr/share/dotnet \&& tar -ozxf dotnet.tar.gz -C /usr/share/dotnet \&& rm dotnet.tar.gz$ ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet

ARM32
$ cd ~/$ apt-get update && apt-get install -y curl$ export DOTNET_VERSION=5.0.5$ curl -SL --output dotnet.tar.gz https://dotnetcli.azureedge.net/dotnet/Runtime/$DOTNET_VERSION/dotnet-runtime-$DOTNET_VERSION-linux-arm.tar.gz \&& mkdir -p /usr/share/dotnet \&& tar -ozxf dotnet.tar.gz -C /usr/share/dotnet \&& rm dotnet.tar.gz$ ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet


Проверим запуск .NET, командой (результат одинаков для Banana Pi BPI-M64 и Cubietruck): dotnet --info

root@bananapim64:~# dotnet --infoHost (useful for support):  Version: 5.0.5  Commit:  2f740adc14.NET SDKs installed:  No SDKs were found..NET runtimes installed:  Microsoft.NETCore.App 5.0.5 [/usr/share/dotnet/shared/Microsoft.NETCore.App]To install additional .NET runtimes or SDKs:  https://aka.ms/dotnet-download

.NET установлен в системе, для запуска приложений в Linux необходимо воспользоваться командой: dotnet ConsoleApp1.dll

Обновление .NET 5.0


При выходе новых версий .NET необходимо сделать следующее:

  1. Удалить папку /usr/share/dotnet/
  2. Выполнить скрипт установки, указав новую версию .NET в строке export: DOTNET_VERSION=5.0.5. Номер последней версии .NET можно посмотреть на странице Download .NET 5.0. Строку скрипта создания символической ссылки выполнять не надо: ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet


Удаленная отладка приложения на .NET 5.0 в Visual Studio Code для ARM


Удаленная отладка в Visual Studio Code позволяет в интерактивном режиме видеть ошибки и просматривать состояние переменных, без необходимости постоянного ручного переноса приложения на одноплатный компьютер, что существенно облегчает разработку. Бинарные файлы копируются в автоматическом режиме с помощью утилиты Rsync. Для работы с GPIO, настройка удаленной отладки не является обязательной задачей. Более подробно можно почитать в публикации Удаленная отладка приложения на .NET 5.0 в Visual Studio Code для ARM на примере Banana Pi BPI-M64 и Cubietruck (Armbian, Linux).

Создание первого приложения для управления (вкл/выкл светодиода) GPIO на C#, аналог утилиты gpioset


Поздравляю тебя %habrauser%! Мы уже подходим к финалу, осталось буквально чуть-чуть. Разрабатывать и компилировать приложение будем на x86 компьютере в в Visual Studio Code. Находясь в этой точке, подразумевается, что на одноплатном компьютере уже установлена платформа .NET 5 и библиотека Libgpiod, а на компьютере x86 .NET 5 и Visual Studio Code. Итак приступаем:

Шаг 1 Создание приложения dotnet-gpioset


Действия выполняются на x86 компьютере. В командной строке создаем проект с названием dotnet-gpioset: dotnet new console -o dotnet-gpioset, где dotnet-gpioset название нового проекта. Результат выполнения команды:

D:\Anton\Projects>dotnet new console -o dotnet-gpiosetGetting ready...The template "Console Application" was created successfully.Processing post-creation actions...Running 'dotnet restore' on dotnet-gpioset\dotnet-gpioset.csproj...  Определение проектов для восстановления...  Восстановлен D:\Anton\Projects\dotnet-gpioset\dotnet-gpioset.csproj (за 68 ms).Restore succeeded.

После выполнения команды будет создана папка \Projects\dotnet-gpioset\, в этой папке будет расположен наш проект: папка obj, файл программы Program.cs и файл проекта dotnet-gpioset.csproj.

Шаг 2 Установка расширения C# for Visual Studio Code (powered by OmniSharp) для Visual Studio Code


Запустим Visual Studio Code и установим расширение C# for Visual Studio Code (powered by OmniSharp), для возможности работы с кодом на C#. Для этого нажмем на закладке: 1. Extensions, затем 2. в поле ввода напишем название расширения C# for Visual Studio Code, выберем пункт 3. C# for Visual Studio Code (powered by OmniSharp). 4. Перейдем на страницу описание расширения и нажмем на кнопку Install.

.NET Visual Studio Code ARM
C# for Visual Studio Code (powered by OmniSharp)

После установки можно выполнить настройку расширения.

.NET Visual Studio Code ARM
Настройка расширения C# for Visual Studio Code

После установки расширения, перезапустим Visual Studio Code.

Шаг 3 Открытие проекта в Visual Studio Code и добавление NuGet пакетов


Откроем проект в Visual Studio Code. Меню: File =>Open Folder, и выберем папку с проектом \Projects\dotnet-gpioset\

dotnet libgpiod
Проект в Visual Studio Code

Откроем файл dotnet-gpioset.csproj, убедимся что версия .NET выставлена верно, должно быть следующее содержание:

dotnet libgpiod
Содержание файла dotnet-gpioset.csproj

NuGet пакеты можно добавить через командную строку или расширение NuGet Package Manager. Установим данное расширение, и добавим пакеты: Iot.Device.Bindings и System.Device.Gpio. Для этого нажмем комбинацию Ctrl+Shift+P, затем в поле введем: Nuget, выберем Nuget Packet Managet: Add Package.

dotnet libgpiod
Запуск расширения NuGet Package Manager

В поле ввода укажем название пакета Iot.Device.Bindings, нажмем Enter, затем выберем версию 1.4.0 и нажмем Enter. Так же сделать и для пакета System.Device.Gpio. В результате добавление пакетов, содержимое файла dotnet-gpioset.csproj должно быть следующим:

dotnet libgpiod
Содержание файла dotnet-gpioset.csproj

Шаг 4 Добавление обработки аргументов в код


Утилита dotnet-gpioset как и оригинальная gpioset будет принимать на вход точно такие же аргументы. Вызов: dotnet-gpioset 1 36=1, включит светодиод на gpiochipX 1, номер линии 36, значение 1. В режиме отладки будут заданы значения по умолчанию int_gpiochip=1, int_pin=36, pin_value = PinValue.High. Подключим пространство имен System.Device.Gpio для использование структуры PinValue.

Обработка входящих аргументов:

static void Main(string[] args){  //run: dotnet-gpioset 1 36=1  //-----------------------------------------------                          int? int_gpiochip=null,int_pin=null;  PinValue? pin_value=null;    #if DEBUG    Console.WriteLine("Debug version");    int_gpiochip=1;    int_pin=36;    pin_value = PinValue.High;  #endif  if (args.Length==2)    {      //Read args      if (int.TryParse(args[0], out int output)) int_gpiochip = output;      Regex r = new Regex(@"\d+=\d+");//36=1      if (r.IsMatch(args[1])) //check: 36=1        {          var i = args[1].Split("=");          if (int.TryParse(i[0], out output)) int_pin = output;          if (int.TryParse(i[1], out output))            {              pin_value=(output != 0) ? PinValue.High : PinValue.Low;                                         }        }      }  Console.WriteLine($"Args gpiochip={int_gpiochip}, pin={int_pin}, value={pin_value}");  //next code  Console.WriteLine("Hello World!");}

Запускаем выполнение кода для проверки, меню Run => Start Debugging, все работает отлично!

Загружено "C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.5\System.Text.Encoding.Extensions.dll". Загрузка символов пропущена. Модуль оптимизирован, включен параметр отладчика "Только мой код".Debug versionArgs gpiochip=1, pin=36, value=HighHello World!Программа "[8528] dotnet-gpioset.dll" завершилась с кодом 0 (0x0).

Шаг 5 Добавление контроллера управления GPIO c драйвером LibGpiodDriver


Для управления GPIO необходимо создать объект GpioController и указать драйвер LibGpiodDriver, для этого добавим пространство имен System.Device.Gpio.Drivers.

Добавление контроллера:

//next codeGpioController controller;var drvGpio = new LibGpiodDriver(int_gpiochip.Value);            controller = new GpioController(PinNumberingScheme.Logical, drvGpio);

Описание кода:

  • GpioController класс контроллера для управления контактами GPIO;
  • LibGpiodDriver(int_gpiochip.Value) драйвер обертки библиотеки Libgpiod, в качестве аргумента указываем номер gpiochip;
  • GpioController(PinNumberingScheme.Logical, drvGpio) инициализация контроллера, PinNumberingScheme.Logical формат указания контактов. Есть два варианта, по названию контакта или по его номеру. Но т.к. названия контактов не заданы, то обращение будет только по номеру.

Шаг 6 Управление контактом GPIO


Добавление кода для задания значения контакту:

//set value            if(!controller.IsPinOpen(int_pin.Value))  {    controller.OpenPin(int_pin.Value,PinMode.Output);    controller.Write(int_pin.Value,pin_value.Value);                      } 

Описание кода:

  • controller.IsPinOpen проверка открытия контакта, может быть занят или недоступен;
  • controller.OpenPin открытие контакта и задание ему режима работы, PinMode.Output на вывод;
  • controller.Write(int_pin.Value,pin_value.Value) выставление контакту int_pin значение pin_value.

Шаг 7 Публикация для архитектуры ARM


Открыть командную строку, и перейти в папку \Projects\dotnet-gpioset\.

Для ARM32 выполнить команду:

  • параметр --runtime задает архитектуру выполнения программы (берется из списка Runtime Identifiers (RIDs));
  • параметр --self-contained указывает на необходимость добавление в каталог всех зависимых сборок .NET, при выставление значение в False, копируются только дополнительные сборки не входящие в .NET Runtime (в данном случае будут скопированы сборки из дополнительных NuGet пакетов).

dotnet publish dotnet-gpioset.csproj --configuration Release --runtime linux-arm --self-contained false

Файлы для переноса на одноплатный компьютер будут в папке: \Projects\dotnet-gpioset\bin\Release\net5.0\linux-arm\publish\.

Для ARM64 выполнить команду:

dotnet publish dotnet-gpioset.csproj --configuration Release --runtime linux-arm64 --self-contained false

Файлы для переноса на одноплатный компьютер будут в папке: \Projects\dotnet-gpioset\bin\Release\net5.0\linux-arm64\publish\.

Шаг 8 Перенос папки \publish\


Содержимое папки \publish\ необходимо перенести в домашний каталог Linux пользователя на одноплатном компьютере. Это можно сделать используя терминал MobaXterm.

Шаг 9 Запуск dotnet-gpioset на одноплатном компьютере


Содержимое папки \publish\ было скопировано в папку /root/publish-dotnet-gpioset. Исполняемым файлом будет файл с расширением *.dll. В самом начале, светодиод был подключен на контакт 33, 40-контактного разъема совместимого с Raspberry P, название контакта PB4, номер линии 36. Поэтому в качестве аргумента номера контакта указываем 36. Для запуска программы необходимо выполнить команду:

dotnet dotnet-gpioset.dll 1 36=1

Результат выполнения команды:

root@bananapim64:~# cd /root/publish-dotnet-gpiosetroot@bananapim64:~/publish-dotnet-gpioset# dotnet dotnet-gpioset.dll 1 36=1Args gpiochip=1, pin=36, value=HighOK

Светодиод включился!



Проект доступен на GitHub dotnet-gpioset.

Создание приложения обработки прерывания от кнопки


Теперь реализуем программу обработки прерываний от GPIO. Задача будет заключаться в переключение светодиода по нажатию кнопки. Первое нажатие кнопки включит светодиод, последующее, выключит светодиод, и так до бесконечности. Программа основана на примере Push button.

Светодиод подключен контакту с номером 36. Кнопка подключена на контакт с номером 38. Итак приступаем:

Шаг 1 Создание приложения dotnet-led-button


Действия выполняются на x86 компьютере. В командной строке создаем проект с названием dotnet-led-button: dotnet new console -o dotnet-led-button, где dotnet-led-button название нового проекта.

D:\Anton\Projects>dotnet new console -o dotnet-led-buttonGetting ready...The template "Console Application" was created successfully.Processing post-creation actions...Running 'dotnet restore' on dotnet-led-button\dotnet-led-button.csproj...  Определение проектов для восстановления...  Восстановлен D:\Anton\Projects\dotnet-led-button\dotnet-led-button.csproj (за76 ms).Restore succeeded.

После выполнения команды будет создана папка с файлами проекта \Projects\dotnet-led-button\.

Шаг 2 Открытие проекта в Visual Studio Code и добавление NuGet пакетов


Точно так же, как и в предыдущем проекте добавим Nuget пакеты: Iot.Device.Bindings и System.Device.Gpio.

Шаг 3 Добавление контроллера управления GPIO c драйвером LibGpiodDriver


Добавим контроллер для управления GPIO, и выставим режим работы контактов:

private const int GPIOCHIP = 1;private const int LED_PIN = 36;private const int BUTTON_PIN = 38;       private static PinValue ledPinValue = PinValue.Low;     static void Main(string[] args){                          GpioController controller;  var drvGpio = new LibGpiodDriver(GPIOCHIP);  controller = new GpioController(PinNumberingScheme.Logical, drvGpio);  //set value  if(!controller.IsPinOpen(LED_PIN)&&!controller.IsPinOpen(BUTTON_PIN))    {      controller.OpenPin(LED_PIN,PinMode.Output);      controller.OpenPin(BUTTON_PIN,PinMode.Input);    }  controller.Write(LED_PIN,ledPinValue); //LED OFF

Описание кода:

  • controller.OpenPin(LED_PIN,PinMode.Output) - открывает контакт светодиода, и выставляет режим работы на вывод;
  • controller.OpenPin(BUTTON_PIN,PinMode.Input) - открывает контакт кнопки, и выставляет режим работы на ввод (сигнал поступает от кнопки.

Шаг 4 Добавление обработки прерывания кнопки


Обработка прерывания реализуется путем добавление Callback на изменение состояние контакта. Callback регистрируется в контроллере GPIO:

controller.RegisterCallbackForPinValueChangedEvent(BUTTON_PIN,PinEventTypes.Rising,(o, e) =>  {    ledPinValue=!ledPinValue;    controller.Write(LED_PIN,ledPinValue);    Console.WriteLine($"Press button, LED={ledPinValue}");          });

Описание кода:

  • RegisterCallbackForPinValueChangedEvent регистрация Callback на контакт BUTTON_PIN, будет срабатывать при нажатие на кнопку Rising. Так же доступно срабатывание на событие отпускание кнопки.

Шаг 5 Публикация для архитектуры ARM


Открыть командную строку, и перейти в папку \Projects\dotnet-led-button\.

Для ARM32 выполнить команду:

dotnet publish dotnet-led-button.csproj --configuration Release --runtime linux-arm --self-contained false

Файлы для переноса на одноплатный компьютер будут в папке: \Projects\dotnet-led-button\bin\Release\net5.0\linux-arm\publish\.

Для ARM64 выполнить команду:

dotnet publish dotnet-led-button.csproj --configuration Release --runtime linux-arm64 --self-contained false

Файлы для переноса на одноплатный компьютер будут в папке: \Projects\dotnet-led-button\bin\Release\net5.0\linux-arm64\publish\.

Шаг 6 Перенос папки \publish\


Содержимое папки \publish\ необходимо перенести в домашний каталог Linux пользователя на одноплатном компьютере.

Шаг 7 Запуск dotnet-led-button на одноплатном компьютере


Содержимое папки \publish\ было скопировано в папку /root/publish-dotnet-led-button. Для запуска программы необходимо выполнить команду:

dotnet dotnet-led-button.dll

Результат выполнения команды:

root@bananapim64:~/publish-dotnet-led-button# dotnet dotnet-led-button.dllCTRL+C to interrupt the read operation:Press any key, or 'X' to quit, or CTRL+C to interrupt the read operation:Press button, LED=LowPress button, LED=HighPress button, LED=LowPress button, LED=HighPress button, LED=Low

Кнопка работает!

Проект доступен на GitHub dotnet-led-button.

Теперь поговорим о скорости


Замеры скорости управления GPIO на Banana Pi BPI-M64 не проводились из-за отсутствия осциллографа. Но не так давно, пользователь ZhangGaoxing опубликовал результаты замеров скорости на Orange Pi Zero: ОС Armbian buster, ядро Linux 5.10.16, .NET 5.0.3. Тест заключался в быстром переключение контакта GPIO с 0 на 1 и наоборот, по сути осуществлялась генерация сигнала ШИМ (в Arduino аналог SoftPWM). Чем больше частота, тем быстрее переключатся контакт. Для замера был разработан проект SunxiGpioDriver.GpioSpeed. ZhangGaoxing для доступа к контактам разработал драйвер SunxiDriver, который напрямую обращается к регистрам памяти для управления GPIO. Код этого драйвера так же можно адаптировать к любой плате, путем изменения адресов регистров памяти из datasheet к процессору. Минус такого подхода заключается в отсутствие контроля к GPIO со стороны ОС, можно влезть в контакт используемой ОС и вызвать сбой работы.

Таблица замеров:
Драйвер Язык Версия библиотеки Средняя частота
SunxiDriver C# - 185 KHz
SysFsDriver C# System.Device.Gpio 1.3.0 692 Hz
LibGpiodDriver C# System.Device.Gpio 1.3.0
libgpiod 1.2-3
81 KHz
wiringOP C 35de015 1.10 MHz

Результаты подтвердили, что самым медленным интерфейсом является SysFs, и его не стоит использовать для серьезных проектов. wiringOP является С оберткой доступа к GPIO. Непосредственно управление GPIO из C кода существенно быстрее, чем из приложения на .NET, разница скорости в ~13 раз. Это и есть плата за Runtime.

Итог


Управлять контактами GPIO в C# оказалось не сложнее чем на Arduino. В отличие от Arduino в нашем распоряжение Linux с поддержкой полноценной графики, звуком, и большими возможностями подключения различной периферии. В далеком 2014 году с хабровчанином prostosergik был спор о целесообразности использовании Raspberry Pi в качестве школьного звонка. Мною был реализован подобный функционал на C# .NET Micro Framework, отладочная плата FEZ Domino. С того времени многое что изменилось. Сейчас вариант использования для подобных индивидуальных задач, одноплатных компьютеров на Linux более оправдан, чем использование микроконтроллера. Первое существенное изменение это .NET теперь работает на Linux нативно. Второе появились библиотеки которые упрощают и скрывают под капотом все сложную работу. Третье цена, сейчас одноплатный компьютер с 256 Мб ОЗУ, Ethernet и Wi-Fi в известном китайском магазине можно приобрести за 18$. За такие деньги МК, с поддержкой полноценного Web-интерфейса и шифрования сетевого трафика, вряд ли найдешь. Платформа .NET IoT позволяет работать с GPIO на достаточно высоком уровне абстракции, что существенно снижает порог вхождения. В результате любой разработчик .NET платформы, может с легкостью реализовать свое решение для IoT не вдаваясь в детали как это работает внутри. Установка платформы .NET и библиотеки Libgpiod было приведено для понимания, как это работает, но такой подход не является самым удобным. Гораздо удобнее все разворачивать в Docker контейнере, тем более это mainstream для Linux. В продолжении посмотрим как упаковывать приложение на C# вместе с .NET 5 и Libgpiod в один контейнер, для дальнейшей удобной дистрибьюции нашего решения потенциальному клиенту, задействуем LCD для вывода информации из .NET кода.



На правах рекламы


Прямо сейчас вы можете заказать мощные серверы, которые используют новейшие процессоры AMD Epyc. Гибкие тарифы от 1 ядра CPU до безумных 128 ядер CPU, 512 ГБ RAM, 4000 ГБ NVMe.

Подписывайтесь на наш чат в Telegram.

Подробнее..

C vs Kotlin

01.06.2021 08:12:59 | Автор: admin

Когда речь заходит о сахаре и модных фичах в языках программирования, среди первых вариантов на ум приходят C# и Kotlin. Поскольку эти два языка занимают схожие ниши, то есть, строго типизированы, обладают сборкой мусора, кроссплатформенны, применяются как на бекенде, так и в мобильной разработке, то сегодня мы попытаемся сравнить их синтаксические возможности и устроить небольшое голосование. Чтобы сравнение прошло честно, будем рассматривать последние версии обоих языков. Оговорюсь о своей непредвзятости: мне одинаково нравятся оба языка, они находятся в непрерывном развитии и не отстают друг от друга. Эта статья является сравнительной, а не обучающей, поэтому некоторые заурядные синтаксические возможности могут быть опущены.

Начнем с точки входа

В C# эту роль играет статический метод Main или top-level entry point, например

using static System.Console;WriteLine("Ok");

В Kotlin нужна функция main

fun main() = println("Ok")

По этим небольшим двум примерам в первую очередь заметно, что в Kotlin можно опускать точку с запятой. При более глубоком анализе видим, что в C#, несмотря на лаконичность показательного entry point, статические методы в остальных файлах по прежнему требуется оборачивать в класс и явно импортировать из него (using static System.Console), а Kotlin идет дальше и разрешает создавать полноценные функции.

Обьявление переменных

В C# тип пишется слева, а для создания экземпляра используется ключевое слово new. В наличии есть специальное слово var, которым можно заменить имя типа слева. При этом переменные внутри методов в C# остаются подвержены повторному присваиванию.

Point y = new Point(0, 0); var x = new Point(1, 2);x = y; // Нормально

В Kotlin типы пишутся справа, однако их можно опускать. Помимо var, доступен и val который не допускает повторного присваивания. При создании экземляров не нужно указывать new.

val y: Point = Point(0, 0)val x = Point(1, 2)x = y // Ошибка компиляции!

Работа с памятью

В C# нам доступны значимые (обычно размещаются на стеке) и ссылочные (обычно размещаются в куче) типы. Такая возможность позволяет применять низкоуровневые оптимизации и сокращать расход оперативной памяти. Для объектов структур и классов оператор '==' будет вести себя по разному, сравнивая значения или ссылки, впрочем это поведение можно изменить благодаря перегрузке. При этом на структуры накладываются некоторые ограничения связанные с наследованием.

struct ValueType {} // структура, экземпляры попадут на стекclass ReferenceType {} // ссылочный тип, экземпляры будут в куче

Что до Kotlin, то у него нет никакого разделения по работе с памятью. Сравнение '==' всегда происходит по значению, для сравнения по ссылке есть отдельный оператор '==='. Объекты практически всегда размещаются в куче, и только для некоторых базовых типов, например Int, Char, Double, компилятор может применить оптизмизации сделав их примитивами jvm и разместив на стеке, что никак не отражается на их семантике в синтаксисе. Складывается впечатление что рантайм и работа с памятью это более сильная сторона .NET в целом.

Null safety

В C# (начиная с 8ой версии) есть защита от null. Однако ее можно явно обойти с помощью оператора !

var legalValue = maybeNull!;// если legalValue теперь null, // то мы получим exception при первой попытке использования

В Kotlin для использования null нужно использовать два восклицания, но есть и другое отличие

val legalValue = maybeNull!! // если maybeNull == null, // то мы получим exception сразу же

Свойства классов

В C# доступна удобная абстракция вместо методов get/set, то есть всем известные свойства. При этом традиционные поля остаются доступны.

class Example{     // Вычислено заранее и сохранено в backing field  public string Name1 { get; set; } = "Pre-calculated expression";    // Вычисляется при каждом обращении  public string Name2 => "Calculated now";    // Традиционное поле  private const string Name3 = "Field"; }

В Kotlin полей нет вообще, по умолчанию доступны только свойства. При этом в отличие от C# public это область видимости по умолчанию, поэтому это ключевое слово рекомендукется опускать. Для разницы между свойствами, допускающими set и без него, используются все те же ключевые var/val

class Example {    // Вычислено заранее и сохранено в backing field  val name1 = "Pre-calculated expression"    // Вычисляется при каждом обращении  val name2 get() = "Calculated now"}

Классы данных

В C# достаточно слова record чтобы создать класс для хранения данных, он будет обладать семантикой значимых типов в сравнении, однако по прежнему остается ссылочным (будет размещаться в куче):

class JustClass{  public string FirstName { init; get; }  public string LastName { init; get; }}record Person(string FirstName, string LastName);...   Person person1 = new("Nancy", "Davolio");Person person2 = person1 with { FirstName = "John" };

В Kotlin нужно дописать ключевое слово data к слову class

class JustClass(val firstName: String, val lastName: String)data class Person(val firstName: String, val lastName: String)...val person1 = Person("Nancy", "Davolio")val person2 = person1.copy(firstName = "John")

Расширения типов

В C# такие типы должны находиться в отдельном статическом классе и принимать вызывающий первым аргументом, помеченным this

static class StringExt{  public static Println(this string s) => System.Console.WriteLine(s)      public static Double(this string s) => s + s}

В Kotlin расширямый тип должен находиться слева от метода, который можно разместить в любом месте. При этом расширить тип можно не только методом, но и свойством

fun String.println() = println(this)fun String.double get() = this * 2

Лямбда выражения

В C# для них есть специальный оператор =>

numbers.Any(e => e % 2 == 0);numbers.Any(e =>   {    // объемная логика ...    return calculatedResult;  })

В Kotlin лямбды органично вписываются в Си-подобный синтаксис, кроме того во многих случаях компилятор заинлайнит их вызовы прямо в используемый метод. Это позволяет создавать эффективные и красивые DSL (Gradle + Kotlin например).

numbers.any { it % 2 == 0 }numbers.any {  // объемная логика ...  return calculatedResult}

Условия и шаблоны

У C# есть очень мощный pattern matching c условиями (пример из документации)

static Point Transform(Point point) => point switch{  { X: 0, Y: 0 }                    => new Point(0, 0),  { X: var x, Y: var y } when x < y => new Point(x + y, y),  { X: var x, Y: var y } when x > y => new Point(x - y, y),  { X: var x, Y: var y }            => new Point(2 * x, 2 * y),};

У Kotlin есть аналогичное switch выражение when, которое, несмотря на наличие возможности сопоставления с образцом, не может одновременно содержать деконструкции и охранных условий, но благодаря лаконичному синтаксису можно выкрутиться:

fun transform(p: Point) = when(p) {  Point(0, 0) -> Point(0, 0)  else -> when {    x > y     -> Point(...)    x < y     -> Point(...)    else      -> Point(...)  }}// или такfun transform(p: Point) = when {  p == Point(0, 0) -> Point(0, 0)  p.x < y          -> Point(p.x + y, p.y)  p.x > y          -> Point(p.x - p.y, p.y)  else             -> Point(2 * p.x, 2 * p.y)}

Подводя итоги

Уложить в одной статье все отличия обоих языков практически нереально. Однако кое какие выводы сделать уже можем. Заметно что Kotlin-way скорее в том чтобы минимизировать количество ключевых слов, реализуя весь сахар поверх базового синтаксиса, а C# стремится стать более удобным увеличивая количество доступных выражений на уровне самого языка. У Kotlin преимущество в том что его создатели могли оглядываться на удачные фичи C# и лаконизировать их, а C# выигрывает за счет мощной поддержки в лице Microsoft и лучшего рантайма.

Подробнее..

Photon это не только log4net

12.05.2021 12:08:48 | Автор: admin

... но и любой другой логгер.

Традиционно Photon Server SDK поставляется с log4net. Но это не значит что все им должны пользоваться. Пользоваться можно практически любым логгером. Всё что нужно это создать свою сборку-адаптер, которая будет содержать класс прокси и фабрику для него.

Для примера возьмём модный нынче Serilog. Я с ним не знаком, так что возможно что-то будет сделано не лучшим методом или неправильно.

И так приступим.

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

В SDK 4.0 интерфейсы ExitGames.Logging.ILogger и ExitGames.Logging.ILoggerFactory находятся в ExitGamesLibs.dll. В SDK 5.0 их вынесли в ExitGames.Logging.dll. ExitGames.Logging находится в nuget пакете с тем же именем. Эти библиотеки должны быть добавлены в зависимости.

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

    class SerilogLogger : ILogger    {        private readonly global::Serilog.ILogger logger;        public bool IsDebugEnabled => this.logger.IsEnabled(LogEventLevel.Debug);        // not sure whether this is right implementation        public string Name => this.logger.ToString();............................................................        public SerilogLogger(global::Serilog.ILogger logger)        {            this.logger = logger;        }        public void Debug(object message)        {            if (message is string str)            {                this.logger.Debug(str);                return;            }            throw new NotSupportedException("only strings are allowed");        }        public void Debug(object message, Exception exception)        {            if (message is string str)            {                this.logger.Debug(exception, str);                return;            }            throw new NotSupportedException("only strings are allowed");        }        public void DebugFormat(string format, params object[] args)        {            this.logger.Debug(format, args);        }        public void DebugFormat(IFormatProvider formatProvider, string format, params object[] args)        {            this.logger.Debug(format, args);        }......................................................        

Следующее, что нам нужно это класс фабрики.

    public class SerilogLoggerFactory : ILoggerFactory    {        /// <summary>        /// Provides a static singleton instance for the <see cref="SerilogLoggerFactory"/> class.        /// </summary>        public static readonly SerilogLoggerFactory Instance = new SerilogLoggerFactory();        public ILogger CreateLogger(string name)        {            var serilogLogger = Log.ForContext(Constants.SourceContextPropertyName, name);            return new SerilogLogger(serilogLogger);        }    }

Последний штрих это установка нашей фабрики

ExitGames.Logging.LogManager.SetLoggerFactory(SerilogLoggerFactory.Instance);

Сделать это необходимо перед тем как будут инициализироваться логгеры в Photon.SocketServer.dll. Для этого лучше всего подходит статический конструктор вашего photon-приложения

Что ещё нужно знать про логгинг

Используйте if (log.IsDebugEnabled)

Хотя я соглашусь с тем, что это очевидно, но я сам так не делал, когда только столкнулся с фотоном. Обычно отладочных сообщений в коде много и, если их не накрывать таким if-ом, они могут сильно просадить скорость выполнения. Для Info это уже решать самим. Warning обычно всегда включён поэтому можно этот трюк не использовать.

Защищённое логгирование

Много раз мы наступали на грабли, что какой-нибудь варнинг начинает валиться в лог сотнями сообщений в секунду. Эта ситуация неприемлема по многим причинам и производительность не самая последняя из них. Поэтому изобрели LogCountGuard. Ему задают интервал и количество сообщений, которое он может вывести за этот интервал. Остальные не выводятся, а только копится счётчик пропущенных. Когда интервал времени заканчивается сообщение снова выводится с указанием числа пропущенных до него. Вот тут есть тонкость в реализации. Если интервал закончился, а новые сообщения не поступали, то пропущенные выводится не будут

Для того, чтобы этим инструментом было удобно пользоваться, были добавлены методы расширения. Теперь это всё выглядит следующим образом. Допустим нам надо, чтобы какое-то сообщение не появлялось чаще, чем 10 раз в минут. Необходимо сделать следующее,

  // объявление  private static readonly LogCountGuard msgLogGuard = new LogCountGuard(new TimeSpan(0, 0, 6), 1);    // использование  log.Warn(msgLogGuard, "message");

Заключение

В заключении хочется пожелать всем успеха в использовании Photon Server SDK и пусть отсутствие вашего любимого логгера вас не пугает.

Подробнее..

11 анонсов конференции Microsoft Build для разработчиков

27.05.2021 10:19:54 | Автор: admin

Привет, Хабр! Сегодня, как и обещали*, делимся подборкой самых интересных для разработчиков конференции Microsoft Build 2021. Их получилось 11, но это не значит, что это все. Чтобы узнать еще больше, изучайте сайт конференции.

* пообещали это мы во вчерашней подборке 8 анонсов конференции Microsoft Build 2021, которую подготовила наша бизнес-команда.

1. Представлен Windows Terminal Preview 1.9

Поздравляем с Microsoft Build 2021 и вторым днем рождения Windows Terminal! Этот выпуск представляет версию 1.9 для Windows Terminal Preview и переносит Windows Terminal в версию 1.8. Как всегда, вы можете установить обе сборки из Microsoft Store, а также со страницы выпусков GitHub.

Среди новинок:

  • Дефолтный терминал

  • Quake mode

  • Обновления Cascadia Code

  • Обновления интерфейса настроек

  • Другие улучшения

Подробнее здесь.

2. Представляем Visual Studio 2019 v16.10 и v16.11 Preview 1

Мы рады объявить о выпуске Visual Studio 2019 v16.10 GA и v16.11 preview 1. Этот выпуск делает нашу тему продуктивности и удобства разработчиков общедоступной для пользователей Visual Studio! Мы добавили функции C ++ 20, улучшили интеграцию с Git, улучшили инструменты профилирования и множество функций, повышающих продуктивность.

Подробнее здесь.

3. Представляем .NET 6 Preview 4

Мы рады выпустить .NET 6 Preview 4. Мы почти наполовину закончили выпуск .NET 6. Это хороший момент, чтобы еще раз взглянуть на .NET 6 в полном объеме, как и в первом Preview. Многие функции находятся в близкой к окончательной форме, а другие появятся в ближайшее время, когда основные блоки будут готовы к выпуску. Предварительная версия 4 создает прочную основу для выпуска в ноябре финальной сборки .NET 6 с готовыми функциями и возможностями. Она также готова к тестированию в реальных условиях, если вы еще не пробовали .NET 6 в своей среде.

Говоря о финальном выпуске, у нас теперь запланирована дата! Добавьте в календарь даты с 9 по 11 ноября и .NET Conf 2021. Мы выпустим .NET 6 9-го числа с множеством подробных докладов и демонстраций, которые расскажут вам все, что вы хотите знать о .NET 6.

Подробнее здесь.

4. Представляем .NET MAUI Preview 4

Сегодня мы рады объявить о доступности .NET Multi-platform App UI (.NET MAUI) Preview 4. Каждая предварительная версия представляет больше элементов управления и функций для этого многоплатформенного инструментария, который станет общедоступным в ноябре этого года на .NET Conf. .NET MAUI теперь имеет достаточно блоков для создания функциональных приложений для всех поддерживаемых платформ, новые возможности для поддержки запуска Blazor на настольных компьютерах и впечатляющий прогресс в Visual Studio для поддержки .NET MAUI.

Подробнее здесь.

5. Обновления ASP.NET Core в .NET 6 Preview 4

Версия .NET 6 Preview 4 уже доступна и включает много новых крутых улучшений в ASP.NET Core.

Вот что нового в этой предварительной версии:

  • Добавлены minimal APIs

  • Async streaming

  • HTTP logging middleware

  • Использование Kestrel для профиля запуска по умолчанию в новых проектах

  • IConnectionSocketFeature

  • Илучшенные шаблоны single-page app (SPA)

  • Обновления .NET Hot Reload

  • Ограничения generic type в компонентах Razor

  • Blazor error boundaries

  • Компиляция Blazor WebAssembly ahead-of-time (AOT)

  • Приложения .NET MAUI Blazor

  • Другие улучшения производительности

Подробнее здесь.

6. Представляем Entity Framework Core 6.0 Preview 4: Performance Edition

Группа Entity Framework Core анонсировала четвертый предварительный выпуск EF Core 6.0. Основная тема этого выпуска - производительность.

Что нового:

  • Производительность EF Core 6.0 теперь на 70% выше в стандартном для отрасли тесте TechEmpower Fortunes по сравнению с 5.0.

  • Улучшение производительности полного стека, включая улучшения в тестовом коде, среде выполнения .NET и т. д. Сам EF Core 6.0 на 31% быстрее выполняет запросы.

  • Heap allocations уменьшены на43%.

Подробнее здесь.

7. Представляем .NET Hot Reload для редактирования кода во время выполнения

Рады представить вам возможность горячей перезагрузки .NET в Visual Studio 2019 версии 16.11 (предварительная версия 1) и с помощью инструментария командной строки dotnet watch в .NET 6 (предварительная версия 4). В полной статье коллеги познакомят вас с тем, что такое .NET Hot Reload, как вы можете начать использовать эту функцию, каково наше видение будущих запланированных улучшений и проснят, какой тип редактирования и языки в настоящее время поддерживаются.

Подробнее здесь.

8. SecretManagement Module v1.1.0

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

Подробнее здесь.

9. Новый бесплатный курс: создание бессерверных приложений с полным стеком в Azure

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

Подробнее здесь.

10. .NET Framework May 2021

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

Подробнее здесь.

11. Представляем сборку OpenJDK от Microsoft

Объявлена общая доступность сборки OpenJDK от Microsoft, нового бесплатного дистрибутива OpenJDK с открытым исходным кодом, доступного бесплатно для всех, с возможностью развертывания его где угодно. Корпорация Майкрософт активно использует Java, внутри компании работает более 500 000 JVM. Группа разработчиков Java очень гордится тем, что вносит свой вклад в экосистему Java и помогает управлять такими рабочими нагрузками, как LinkedIn, Minecraft и Azure!

Сборка включает двоичные файлы для Java 11, основанные на OpenJDK 11.0.11 + 9 x64 server и настольных средах в macOS, Linux и Windows. Мы также публикуем новый двоичный файл раннего доступа для Java 16 для Linux и Windows на ARM, основанный на последней версии OpenJDK 16.0.1 + 9.

Этот новый выпуск Java 16 уже используется миллионами игроков Minecraft с последней версией Minecraft Java Edition Snapshot 21W19A, которая была обновлена для объединения среды выполнения Java 16 на основе сборки Microsoft OpenJDK.

Посетите страницу, чтобы узнать подробности.

Подробнее здесь.

Подробнее..

Путь казахстанского разработчика как я пришел к Java

01.06.2021 14:09:01 | Автор: admin
Привет! Меня зовут Бинали, я руководитель отдела разработки в Beeline Казахстан, работаю в компании почти год. Пришёл в Beeline 1-го июня 2020-го года на позицию Java-разработчика, сейчас я менеджер отдела по разработке ESB. Менеджмент начинает занимать много времени, но пока ещё есть время, чтобы иногда взять задачу в разработку, дабы не потерять навыки программирования.

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



История становления


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

Я с детства интересовался техникой. Магией для меня было вставлять пластмассовую деталь в магнитофон и слышать как играет музыка. Позже я заинтересовался электричеством. Один раз пытался починить неисправную переноску, подсмотрев, как это делает дядя-электрик. Изолента, нож и моя гениальная идея соединить провода друг с другом привела к короткому замыканию, выбитым пробкам и паре шлепков от мамы. В 2007 году, когда я был семиклассником, у меня появился первый собственный мобильный телефон Nokia 6151.


Через пару месяцев мне уже хотелось сделать какой-то аналог сайтика tegos.ru. На телефоне был только WAP (олды, думаю, вспомнили), а компьютера с интернетом у меня не было. Но я все равно искал варианты реализации идеи. Мне попался конструктор wap-сайтов wen.ru максимально примитивный, но в этом и была его особенность. Так мне пришлось осваивать разметку WML расширение XML для WAP. Да, в то время был уже xHTML, но я ещё ничего не понимал.


Нашлось в архиве :)

Я сделал сайт, на котором в основном хостил полезные ссылки для владельцев Nokia, а еще нашел сервис для организации чата. Так и началась мини-тусовка фанатов Nokia на сайте, который содержал всего пару десятков страниц. Здесь я познакомился со множеством единомышленников из Беларуси, Азербайджана и России. Кстати, если здесь есть кто-то из той тусовки, отзовитесь в комментах.

Следующий этап моей технической эволюции сайты с формой регистрации, гостевые книги и формы обратной связи. Главная фишка в них обилие цветов и фоновых картинок. Тут произошло знакомство с xHTML и PHP4. К этому моменту у меня появился компьютер пекарня на базе процессора AMD Athlon XP, 2Гб ОЗУ, 128Гб HDD. Его купила руководительница моей мамы, которая заметила мое рвение к технологиям. В 2008 году б/у комп обошелся 18 000 тенге (сейчас это около $ 40). Началось мое путешествие в мир настоящего программирования с прочтения кучи статей о PHP, а писать код я начал, чтобы найти решение разных проблем.

Уже в 11 классе занимался фриланс-проектами приложений на PHP, а со второго курса нашел официальную работу в небольшой IT-компании, занимавшейся разработкой продуктов. Программировать нужно было на С#. Чтобы пройти собеседование я сам для себя создал и выполнил тестовое задание: описание тут, код тут. Реализовано оно было на PHP, спасибо сеньорам, которые в тот момент просто хотели понять, умею ли я писать хоть на чем-то.

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

Потом еще пара переходов из компании в компанию, собственный gamedev-проект, работа над системой менеджмента обслуживания (ТОиР или MMS) с активным использованием RFID-технологий. Кстати, в этом проекте мы с коллегами создали фреймворк для фреймворка, который руководители нам разрешили вывести в OpenSource. Код тут.

Сейчас я работаю в Beeline Казахстан, где мы пишем на Java, но понял, что готов рассказать всю историю моих переходов с языка на язык. Возможно, она будет кому-то полезна.

Мой первый проект на .NET


С .NET я столкнулся на первом официальном месте работы: небольшой IT-компании, сотрудничающей с нефтянкой. Мой первый проект был про расчёт наработки труб в нефтяных скважинах. Я получил рабочий образец реализации этой идеи, написанный на ASP.NET, .NET 4.1. Честно, тогда я вообще не понимал, как работают эти технологии.


У нас есть проект который работает, но мы не можем его собрать

Я целую неделю безуспешно собирал проект, после чего решил обратиться к старшему разработчику за помощью по сборке. Было нелегко. Сейчас можно вспомнить это как огромную проблему классического ASP.NET про запутанную систему привязи зависимостей.

После сборки меня ожидал ещё один сюрприз: проект предоставлял интерфейс для загрузки excel-файла. Для чтения он запускал полноценный MS Office и бегал по ячейкам, считывая значения в память. После чтения в память и проведения расчётов, приложение снова открывало Office и записывала данные по клеткам. Да да, прямо графическое приложение через interoperability.

Я был в шоке, ведь мне сказали, что проект уже работает и нужно просто развернуть его на IIS под Windows Server. Тут-то и началось мое настоящее знакомство с миром .NET.

На сервере, естественно, ничего не работало. Причина очевидна сервер IIS не имеет доступа к графической подсистеме, поэтому и не может что-либо запускать, в том числе и взаимодействовать с рабочим столом.

Засучив рукава, я начал искать решение как вызывать графический Office и разбирать excel-файл вручную.Так я освоил библиотечку OpenXML, которая далась мне нелегко, но быстро решила все вопросы. Теперь файл разбирался с помощью OpenXML, после чего данные сохранялись в БД. Выходные данные можно было получать в виде отчета с нужными фильтрами.

Я был очень рад, что смог будучи джуном убрать костыль.

.NET со временем показался мне ещё более крутой технологией, и я совсем перестал писать на PHP. В какой-то момент начал участвовать в спорах и обсуждениях крутости той или иной технологии, и выступал именно за .NET. Сейчас мое мнение все-таки поменялось.

Самое ужасное с чем мне пришлось столкнуться это разработка под Windows Mobile на .NET Compact Framework. Кстати, это было в рамках компании о которой я писал выше. Такие проекты были настолько сложны в разработке, что сеньор постоянно отвечал нашему менеджеру это невозможно на запрос о любой фиче :)

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

Как я выбирал между .NET и Java

02.06.2021 10:16:01 | Автор: admin
С .NET я познакомился на первом официальном месте работы: небольшой IT-компании, сотрудничающей с нефтянкой. Это продолжение истории, начало можно почитать здесь.



Чем мне понравился .NET


.NET имеет обширную историю. Не такую как у Java, конечно, но тоже интересную. Еще нужно разделять .NET Framework и .NET Core. Второе ИМХО то, чем .NET должен был быть изначально. Давайте договоримся, что когда я говорю просто .NET имею ввиду .NET Framework. Про .NET Core буду писать с дополнением.

В самом .NET мне понравилось наличие экосистемы, худо-бедно, но, зная один язык программирования, ты можешь без особых проблем писать:
  • серверные;
  • десктопные;
  • мобильные (Windows Mobile);
  • веб (привет, Silverlight, LightSwitch, ASP.NET WebForms, ASP.NET MVC);
  • киоск приложения;
  • игры.


Порог вхождения в .NET довольно низкий, чему всегда способствовали обширные мероприятия и гайды от Майкрософт. Наличие экосистемы позволяет разработчику не думать о том, какую библиотеку ему выбрать, всё уже известно.
Хочешь веб? Бери ASP.NET. Мы, как большая компания (Microsoft), пользуемся и тебе подойдёт. И так во всем.

С# это улучшенная Java, тут тебе и легкая жизнь с auto-property, и легкая модель асинхронного программирования, LINQ который вдобавок можно расширять реализацией провайдеров. Например, LINQ to SQL, LINQ to XML и так далее.

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

Nuget стал центром всего .NET-сообщества: обширное количество библиотек от Microsoft и комьюнити.

.NET Core можно считать работой над ошибками Microsoft. Все изменения произошли, в том числе благодаря CEO Microsoft Сатья Наделле, который показал всему миру, что MS loves Linux. Мы получили конкурента Java.

Возможно кто-то скажет: А просто .NET Framework?
Я отвечу так: Java обрела популярность именно из-за OpenSource-нацеленности. Бизнес не боялся, что завтра придут какие-то чудаки из Sun или Oracle и начнут качать свои права. .NET Framework изначально проприетарная платформа, о благодаря адекватному менеджменту MS, они исправили этот недостаток.

Самый важный аргумент в сравнении двух платформ это, конечно же, возраст Java и его стабильность. Под стабильностью я имею ввиду стандарты внутри комьюнити, больший процент опытных разработчиков и большое количество крупных компаний, использующих Java. И еще Java это compile once run everywhere.

Я рассматриваю .NET Core как полноценного конкурента Java. Язык и инструментарий доступен на GitHub под лицензией MIT.

Что ещё добавилось с момента выхода .NET Core:
  • появилась поддержка OS Linux, macOS;
  • была улучшена работа в средах контейнеризации (.NET Core подбирает подходящие параметры в рантайме в зависимости от среды запуска);
  • началось активное развитие Xamarin. Разработчики получили возможность писать шустрые приложения на iOS и Android;
  • начало развиваться направление IoT;
  • WPF стал опенсорс проектом и появилась большая надежда на его кроссплатформенность;
  • WEB разработка стала еще доступнее благодаря Blazor (можно как WebAssembly, так и рендерить все на стороне сервера).


В сухом остатке имеем следующее: в 2020 году, зная язык программирования C#, можно написать все, что хочешь и без костылей как, например, браузер под капотом электрона :)

Что мне не понравилось в .NET?


Честно? Понимание того, что Microsoft нагло скопировал Java по многим фронтам :) Напомню, что до .NET товарищи из MS пытались реализовать свое представление Java: J++ в последствии J#.

Казалось бы, намерения благие. Хотели, чтобы у джавистов был выбор. Но, MS в то время ничего не делал во благо разработчиков. Достаточно почитать эту статью на Википедии (Поддержать, надстроить и уничтожить).

В .NET Framework на момент моего первого опыта работы, не понравилось:
  • сложная структура файлов проектов;
  • вечные проблемы с биндингом зависимостей в рантайме;
  • VisualStudio она реально медленная и тормознутая :D;
  • только Windows, на тот момент я уже вовсю интересовался OS GNU/Linux;
  • Windows Mobile разработка: она ужасна во всем.


Знакомство с Java



Согласны?)

В феврале 2015 года я устроился на работу Java-разработчиком. Опыта разработки приложений на Java у меня не было, но я был в теме, потому что много читал про язык. Писали на Java 7, а первый день программирования показался не очень сложным. Это как C#, только неудобный, подумал я.

Мой проект был реализован в JavaEE (запускались под TomEE), фронтенд на Vaadin. В целом я не испытывал особых проблем взаимодействия с новой для меня технологией, скорее местами был в шоке.

Поражало обилие конфигурационных XML-файлов, настройки Maven'a на 300 строк. Пугало наличие большого количества аннотаций. Здесь же нельзя не сказать о любви джавистов к аспектно-ориентированному программированию.

Параллельно в этой же компании велась разработка на JavaSE, в качестве фреймворка использовался Spring 4. Я так до конца и не понял, почему всем не нравился проект на Spring, но тимлид постоянно ругался с разработчиком на Spring. По моим наблюдениям там было слишком много кодогенерации, а из-за этого сложным был сам UI.

Сборка проекта (JavaEE) в Maven первый раз у меня заняла минут 20. Было ощущение, что я скачиваю все библиотеки мира. За это можно сказать спасибо Maven, как самому родному сборщику. На самом деле, просто в то время я не знал о существовании Gradle.

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

Чем мне понравилась Java 7


  • Java стабильна;
  • Java это обратная совместимость;
  • есть много реализаций различного инструментария. У разработчика есть выбор, на чем собирать проект: maven, Gradle или вообще `javac` :);
  • в интернете много статей и знаний о решении проблем, с которыми может столкнуться разработчик;
  • Java имеет опенсорсную реализацию в лице OpenJDK;
  • Java активно используется при разработке финансовых систем; Порог вхождения в Java, особенно после опыта в .NET, показался мне не слишком высоким;
  • Конечно же IDE: IntelliJ IDEA она прекрасна во всем.

Что не понравилось в Java 7


на момент моего первого опыта в 2015 году
  • отсутствие экосистемы: разработчику приходится искать подходящую библиотеку среди сотни;
  • комьюнити зачастую сильно расходятся во мнениях;
  • бардак в API при работе с датами и временем;
  • Maven: почему так медленно и многословно?
  • JavaEE: идея супер, реализация плохая. Кто придумал столько декларативной настройки в XML?
  • медленно развивающееся API;
  • отсутствие функций высшего порядка и альтернативы LINQ;
  • сама Java 7 очень многословна.


Поэтому я вернулся в .NET


Не совсем поэтому, конечно, но когда я устраивался на работу Java-разработчиком на проект, мне обещали highload, интересные задачи и кучу сложностей. По факту: уныло, недостартапно, никакого highload и в помине нет.

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

Что изменилось в .NET


Я перешел обратно на .NET в 2016. Ровно в тот момент, когда мои коллеги с нового старого места работы начали с нуля писать бизнес-платформу. Замысел был следующий: разработать систему оперативного учета и контроля, которая могла бы расширятся с помощью модулей. То есть что-то типа SAP PM, только в бюджетной категории. Почитать про SAP PM можно тут.

Стек был такой: .NET 4.5, ASP.NET MVC 5 (Owin), EF Core, MS SQL Server. Фронтенд на SAP UI5 это такой опенсорс JS-фреймворк, который позволял строить бизнес приложения, используя готовые контролы.

Параллельно активно развивался .NET Core, поэтому передо мной встала задача по переносу проекта с .NET Framework 4.5 на .NET Core 2.1. Это было очень увлекательно и сопровождалось немалым количеством рефакторинга. Параллельно мы распиливали монолит на какие-никакие, но отдельные сервисы.

Собственно, пока я занимался рефакторингом и собирал пожелания моих коллег, в стенах компании родился небольшой web-фреймворк. Я назвал его NextApi.

Почему NextApi? Когда в прошлой версии системы мы с синьор-программистом разрабатывали новое API, назвали его следующий Next. И название нового фреймворка это небольшая дань уважения совместной работе. Ссылка на проект тут.

На этом моя миссия была выполнена: компания получила сервисы, работающие на едином инструментарии. Нам удалось переиспользовать бизнес-логику на мобильных клиентах и десктопах, Offline first. Также получилось полностью уйти от Windows Server. Пришлось оставить небольшую виртуалку, чтобы билдить WPF приложение, но это мелочи.

Пришло время идти дальше


После того, как я перенёс проект на .NET Core и распилил его на сервисы, мне перестало хватать чего-то нового в разработческой жизни. Я был в должности тимлида, мы успешно запустили продукт у нескольких крупных заказчиков, задач хватало, проблемы решались, но хотелось челленджа и возможности раскрыть себя.

Параллельно в голове была мысль о востребованности .NET-разработчиков. В Казахстане, в отличии от стран запада, не такой высокий спрос на эту технологию. Чего нельзя сказать о а Java-разработчиках.

Так я вышел из проекта и договорился о выводе разработанного фреймворка в OpenSource. Сделать это было очень тяжело, как говориться, пришлось выйти из зоны комфорта. Но я твердо решил нужно двигаться вперед.

Мой выбор пал на Beeline Казахстан мне хотелось поработать над популярными сервисами. К тому же я понимал, какие здесь задачи и клиентская база, а Java была мне довольно близка. Также, было интересно иметь возможность посмотреть на всю разработку со стороны .NET-разработчика, дополнительно изучить аспекты проектирования highload-систем и оставить хороший след в истории компании.

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

Что сейчас



Было и такое)

В Beeline мы в основном на Java 8, но уже начали смотреть на Java 11, используем Spring Boot, начали активно писать на Kotlin. Я вижу, что Java реально стала двигаться вперед, релизы каждые полгода. Скажи Java-разработчику об этом в начале 2010, он бы покрутил пальцем у виска. На мой взгляд, Java меняется в лучшую сторону.

В Java 8 появились функциональные интерфейсы, которые позволяют сделать код красивее и реализовывать функции высшего порядка. Также подъехал Stream API, который немного облегчил жизнь. Хотя до LINQ далековато, но и это уже радует.

Это я ещё не описал фишки которые появились в более свежих вервиях Java :)

Мне удалось познакомиться с системой сборки Gradle. Она настолько мне понравилась, что я начал писать переиспользуемые скрипты для сборки, чтобы реализовать нечто похожее на файлы проектов в .NET Core.

В Java более прозрачная работа с асинхронностью: когда пишешь код действительно приходится о многом думать. Это и хорошо, и плохо. Мне этот момент нравится, потому что, имея любовь к ОС и железу, приятно иметь возможность влиять на JVM как хочешь.

Что касается личных планов, мне интересна тема highload-приложений. Пока она не раскрыта для меня до конца, но я активно её изучаю.

Стараюсь не фанатеть от DRY, но по возможности делаю все, чтобы переиспользовать знания.

И, конечно, я хочу подтянуть знания в Kotlin, чтобы начать писать крутые сервисы на корутинах. Сам Kotlin это то, чем должна была быть Java. Андрей Бреслав и Ко проделали отличную работу.

Разница между Java и .NET по большей части компенсируется появлением в моей жизни Kotlin. Но я скучаю по многому из .NET.

Основные моменты:
  • скучаю по консольному тулсету dotnet. Там можно и проект сбилдить, и создать новый из шаблона, и много чего другого;
  • мне не хватает нормальной альтернативы для EntityFramework с LINQ;
  • Java действительно кажется более прожорливой по ресурсам, чем .NET. Компьютер иногда просто уходит в себя.


Но, в реальной жизни Java, пожалуй, самое интересное, что случалось со мной в последнее время.

Выводы и напутствие


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

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

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

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

Моё мнение, что перед тем как вступать в споры о крутости технологии, нужно попробовать альтернативную. Например, в Beeline Казахстан у меня была возможность использовать .NET 5 для реализации одного микросервиса. То есть использовать его в компании, где основным языком для серверного ПО является Java. Микросервис вписался в весь ландшафт без проблем. Моим коллегам было интересно делать код-ревью, мы даже обсуждали принципиальные различия. В общем ребятам тоже стало интересно расширить свой кругозор.

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

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

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

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

Бросайте себе вызов и всегда будьте на вершине!

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

Как реляционная СУБД делает JOIN?

03.06.2021 14:23:54 | Автор: admin

О чем эта статья и кому адресована?

С SQL работают почти все, но даже опытные разработчики иногда не могут ответить на простой вопрос. Каким образом СУБД выполняет самый обычный INNER JOIN?

С другой стороны - разработчики на C# или других ООП языках часто воспринимают СУБД как всего лишь хранилище. И размещать какие-то бизнес-правила в SQL - плохо. В противовес им создаются библиотеки вродеLinq2Db(не путаем сLinq2Sql- совершенно разные авторы и разные библиотеки). При ее использовании весь код пишется на C# и вы получаете все преимущества типизированного языка. Но это формальность. Затем этот код транслируется на SQL и выполняется на стороне СУБД.

Для того чтобы лучше разобраться как работает одинаковый код на SQL и на C# мы попробуем реализовать одно и то же на первом и на втором, а затем разберем как это работает. Если вы хорошо знаете что такоеNested Loop,Merge Join,Hash Join- вам скорее всего имеет смысл прочитать статью по диагонали. А вот если не знаете - статья для вас должна быть полезной.

Работа с несколькими коллекциями

Предположим, что у нас есть некоторый сервисный центр по техническому обслуживанию автомобилей - станция технического обслуживания (СТО). Есть две сущности:Person- клиенты сервисного центра иVisit- конкретное посещение данного центра.Personкроме идентификатора содержит имя, фамилию и статус активности (например, если клиент поменял машину на другую марку - он переводится в статус не активного и уже не будет в ближайшем времени посещать нас).Visitкроме идентификатора содержит в себе ссылку на клиента, дату визита и сумму, которую заплатил клиент за этот визит. Все вышеперечисленное можно было бы оформить с помощью следующих классов на C# для самого простейшего случая:

internal sealed class Person{    internal int Id { get; set; }    internal string FirstName { get; set; }    internal string LastName { get; set; }    internal bool IsActive { get; set; }}internal sealed class Visit{    internal int Id { get; set; }    internal int PersonId { get; set; }    internal DateTime Date { get; set; }    internal decimal Spent { get; set; }}// ...internal Person[] persons = new Person[];internal Visit[] visits = new Visit[];// ...

В базе данных (в дальнейшем мы будем использоватьPostgreSQL) для двух этих сущностей есть две таблицы с аналогичными полями:

create table public.visit(    id integer,    person_id integer,    visit_datetime timestamp without time zone,    spent money) tablespace pg_default;create table public.person(    id integer,    first_name character varying(100) COLLATE pg_catalog."default",    last_name character varying(100) COLLATE pg_catalog."default",    is_active boolean) tablespace pg_default;

Исходный код для данной статьи находитсяздесь. Если у вас есть какие-то замечания - можете сразу править.

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

Nested Loop

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

public decimal NestedLoop(){    decimal result = 0;    var upperLimit = new DateTime(2020, 12, 31);    foreach (var person in persons)    {        if (person.IsActive == false)        {            continue;        }                foreach (var visit in visits)        {            if (person.Id == visit.PersonId && visit.Date <= upperLimit)            {                result += visit.Spent;            }        }    }    return result;}

Эта идея анимирована ниже:

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

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

select setseed(0.777);delete from public.person;insert into public.person(id, first_name, last_name, is_active)select row_number() over () as id,substr(md5(random()::text), 1, 10) as first_name,substr(md5(random()::text), 1, 10) as last_name,((row_number() over ()) % 5 = 0) as is_activefrom generate_series(1, 5000);/*<-- 5000 это число клиентов*/delete from public.visit;insert into public.visit(id, person_id, visit_datetime, spent)select row_number() over () as id,(random()*5000)::integer as person_id, /*<-- 5000 это число клиентов*/DATE '2020-01-01' + (random() * 500)::integer as visit_datetime,(random()*10000)::integer as spentfrom generate_series(1, 10000); /* 10000 - это общее число визитов в СТО*/

В данном случае число клиентов CTOPравно 5000, число их визитовV- 10000. Дата визита, а также сам факт визита для клиента генерируются случайным образом из указанных диапазонов. Признак активности клиента выставляется для каждого пятого. В итоге мы получаем некоторый тестовый набор данных, приближенный к реальному. Для тестового набора нам интересна характеристика - число клиентов и посещений. Или(P,V)равное в нашем случае(5000, 10000). Для этого тестового набора мы сделаем следующее: выгрузим его в обьекты C# и с помощью цикла в цикле (Nested Loop) посчитаем суммарные траты наших посетителей. Как это определено в постановке задачи. На моем компьютере получаем приблизительно20.040 миллисекунд, затраченное на подсчет. При этом время получение данных из БД составило все те же самые20.27 миллисекунд. Что в сумме дает около40 миллисекунд. Посмотрим на время выполнения SQL запроса на тех же данных.

select sum(v.spent) from public.visit v                    join public.person p on p.id = v.person_idwhere v.visit_datetime <= '2020-12-31' and p.is_active = True

Все на том же компьютере получилось порядка2.1 миллисекундына все. И кода заметно меньше. Т.е. в 10 раз быстрее самого метода, не считая логики по извлечению данных из БД и их материализации на стороне приложения.

Merge Join

Разница в скорости работы в 20 раз наталкивает на размышления. Скорее всего Nested Loop не очень нам подходити мы должны найти что-то получше. И есть такой алгоритм НазываетсяMerge JoinилиSort-Merge Join. Общая суть в том, что мы сортируем два списка по ключу на основе которого происходит соединение. И делаем проход всего в один цикл. Инкрементируем индекс и если значения в двух списках совпали - добавляем их в результат. Если в левом списке идентификатор больше, чем в правом - увеличиваем индекс массива только для правой части. Если, наоборот, в левом списке идентификатор меньше, то увеличиваем индекс левого массива. Затратность такого алгоритмаO(N*log(N)).

Результат работы такой реализации радует глаз -1.4 миллисекундыв C#. Правда данные из базы данных еще нужно извлечь. А это все те же самые дополнительные20 миллисекунд. Но если вы извлекаете данные из БД, а затем выполняете несколько обработок, то недостаток постепенно нивелируется. Но можно ли подсчитать заданную сумму еще быстрее? Можно!Hash Joinпоможет нам в этом.

Hash Join

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

Видео работы Hash Join (на англ. языке)

Затратность алгоритмаO(N). В .NET стандартный Linq метод как раз его и реализует. В реляционных СУБД часто используются модификации этого алгоритма (Grace hash join,Hybrid hash join) - суть которых сводится к работе в условиях ограниченной оперативной памяти. Замер скорости работы в C# показывает, что этот алгоритм еще быстрее и выполняется за0.9 миллисекунды.

Динамический выбор алгоритма

Отлично! Похоже мы нашли универсальный алгоритм, который самый быстрый. Нужно просто использовать его всегда и не беспокоиться более об этом вопросе. Но если мы учтем еще и расход памяти все станет немного сложнее. Для Nested Loop - память не нужна, Merge Join - нужна только для сортировки (если она будет). Для Hash Join - нужна оперативная память.

Оказывается расход памяти - это еще не все. В зависимости от общего числа элементов в массивах скорость работы разных алгоритмов ведет себя по-разному. Проверим для меньшего числа элементов (P, V) равному (50, 100). И ситуация переворачивается на диаметрально противоположную:Nested Loopсамый быстрый -2.202 микросекунды, Merge Join -4.715 микросекунды, Hash Join -7.638 микросекунды. Зависимость скорости работы каждого алгоритма можно представить таким графиком:

Для нашего примера можно провести серию экспериментов на C# и получить следующую таблицу:

Method

Nested Loop

Merge Join

Hash Join

(10, 10)

62.89 ns

293.22 ns

1092.98 ns

(50, 100)

2.168 us

4.818 us

7.342 us

(100, 200)

8.767 us

10.909 us

16.911 us

(200, 500)

38.77 us

32.75 us

40.75 us

(300, 700)

81.36 us

52.54 us

54.29 us

(500, 1000)

189.58 us

87.10 us

82.85 us

(800, 2000)

606.8 us

173.4 us

172.7 us

(750, 5000)

1410.6 us

428.2 us

397.9 us

А что если узнать значения X1 и X2 и динамически выбирать алгоритм в зависимости от его значения для данных коллекций? К сожалению не все так просто. Наша текущая реализация исходит из статичности коллекции. Что нужно сделать, чтобы вставить еще один визит за 2020 год? В массив в коде на C#. В массив фиксированного размера он, очевидно, не поместится. Нужно выделять новый массив размером на один элемент больше. Скопировать туда все данные, вставлять новый элемент. Понятно, что это дорого. Как насчет того, чтобы заменить Array на List? Уже лучше, т.к. он предоставляет все необходимое API. Как минимум удобно, но если посмотреть на его реализацию - под капотом используется все тот же массив. Только резервируется памяти больше чем надо С запасом. Для нас это означает лишние траты памяти. LinkedList? Здесь должно быть все нормально. Давайте поменяем коллекцию и посмотрим что из этого получится.

Method

Nested Loop

Nested Loop with Linked List

(10, 10)

62.89 ns

262.97 ns

(50, 100)

2.188 us

8.160 us

(100, 200)

8.196 us

32.738 us

(200, 500)

39.24 us

150.92 us

(300, 700)

80.99 us

312.71 us

(500, 1000)

196.3 us

805.20 us

(800, 2000)

599.3 us

2359.1 us

(750, 5000)

1485.0 us

5750.0 us

Время выполнения не только изменилось. Сама кривая стала более крутой и с числом элементов время растет:

Таким образом мы приходим к понимаю, что время доступа к каждому конкретному элементу коллекции крайне важно. Одно из главных преимуществ реляционных СУБД в том, что они всегда готовы к добавлению новых данных в любой диапазон. При этом это добавление произойдет максимально эффективным образом - не будет релокации всего диапазона данных или т.п. Кроме того данные СУБД часто хранится в одном файле - таблицы и их данные. Если утрировать, то здесь также используется связанный список. В случае с PostgreSQL данные представлены в страницах (page), внутри страницы располагаются кортежи данных (tuples). В общих чертах вы можете себе это увидеть на картинках ниже. А если захотите узнать больше деталей, то ниже также есть и ссылка.

Более детально описано в первоисточникеЗдесь

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

Более детально описано в первоисточникеЗдесь.

В оперативную память попадают страницы, а попадают они вbuffer poolчерезbuffer manager. Все это сказывается на стоимости доступа к каждому конкретному значению таблицы. Вне зависимости от того что используетсяNested Loop,Merge JoinилиHash Join. Другой вопрос, что в зависимости от алгоритма число обращений может отличаться в разы. Поэтому реляционные СУБД подходят динамически к выбору алгоритма в каждом конкретном запросе и строят план запроса (Query Plan).

Сравним для большого числа элементов насколько будет отличаться время обработки с одним и тем же алгоритмом в БД и на C#. (P, V) будет равно (50000, 100000). В коде на C# загрузка данных из БД занимает145.13 миллисекунд. Дополнительно к этому выполнение самой логики сNested Loopна основе обычного массива -305.38 миллисекунд,Hash Join-36.59 миллисекунд. Для того чтобы проверить в СУБД такую же реализацию мы будем использовать такой скрипт:

set enable_hashjoin to 'off';--Заставляем БД использовать Nested Loopset enable_mergejoin to 'off';set enable_material to 'off';select sum(v.spent) from public.visit vjoin public.person p on p.id = v.person_idwhere v.visit_datetime <= '2020-12-31' and p.is_active = True

На аналогичных данных в БД сNested Loopзапрос выполнится за11247.022 миллисекунд. Что может говорить о сильно большем времени доступа к каждому конкретному элементу:

Но СУБД приходится заставлять работать так, чтобы она использовалаNested Loop. Изменим наш скрипт таким образом:

set enable_hashjoin to 'on';set enable_mergejoin to 'on';set enable_material to 'on';select sum(v.spent) from public.visit vjoin public.person p on p.id = v.person_idwhere v.visit_datetime <= '2020-12-31' and p.is_active = True

По-умолчанию для такого объема данных будет, конечно выбранHash Join:

И мы видим, что время выполнение составило25.806 миллисекунды, что сопоставимо по скорости с реализацией на C# и даже немного быстрее.

Как мы видим, СУБД может динамически подстраиваться под данные и автоматически выбирать алгоритм, наиболее подходящий в данной конкретной ситуации. Процесс выбора такого способа выполнения запроса возложен напланировщик запросов. В итоге он выдает план запроса, где четко расписано какой алгоритм использовать, какой использовать индекс и т.п.

Выводы

На примере простейшей задачи мы в общих чертах разобрали как работает типичная реляционная СУБД при реализации JOIN. Сравнивать коллекции C# и SQL не очень корректно, за внешней схожестью скрывается серьезное различие в предназначении. Реляционная СУБД призвана обеспечитьконкурентный доступ к данным максимально эффективным способом(при этом подразумевается, что сами данные могут постоянно модифицироваться). Кроме того, данные могут не помещаться в оперативную память и частично храниться на диске.

Более того, СУБД обязана обеспечить сохранность данных на постоянном носителе - одно из основных ее предназначений. При этом на получение данных СУБД динамически выбирает алгоритм, наиболее эффективный в данном случае. В C# аналагичных библиотек или реализаций просто нет И это показательно, т.к. лишь свидетельствует об отсутствии такой необходимости. Linq метод Join реализуетHash Join, который потенциально тратит больше оперативной памяти, но это просто не берется в расчет. Т.к. мало кого интересует применительно к решаемым задачам.

На практике вопрос производительности не является единственным - сопровождаемость кода, покрытие его тестами, скорость работы этих тестов - все тоже очень важно.

Подробнее..

Возможные неопределенности в карьере программиста. Часть 2

10.06.2021 20:10:36 | Автор: admin

Доброго времени суток, Хабровчане!

Спустя аж три года с момента написания первой части статьи Возможные неопределенности в карьере программиста решил, что, возможно, будет интересно а кому то, я надеюсь, даже полезно (в первую очередь начинающим программистам) узнать продолжение истории. Что начал делать Ваня чтобы повысить свои навыки? Как прошел интервью в новую компанию? Обо всем по порядку.

Так больше нельзя, пора... уходить в другую компанию

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

Бывает обидно, когда подходишь к руководителю, сообщаешь ему о своем решении изменить мир к лучшему сменить место работы, а он просто соглашается с твоим решением. "Т.е. как, в смысле? Вы даже не будете умолять меня остаться?!". Так и произошло с Ваней. Интересно, что на его место не взяли и даже не стали никого искать - этот момент очень задел чувства нашего разработчика, но не все так плохо как кажется. Стоит понимать, что если работодатель соглашается с вашим решением об уходе, это не значит, что вы плохой сотрудник или не нужны компании, вероятнее всего есть другие причины. В любом случае, стоит сохранять позитивный настрой, дабы сохранить свои нервы, и не искать виноватых. Если вы уходите из компании, уходите красиво, возможно, вам еще придется вернуться или встретиться с коллегами в будущем, мир, как известно, круглый, а ваша репутация бесценна.

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

Поиск новой работы и заполнение пробелов в знаниях

Хотелось бы сказать, что Ваня начал свои поиски с заполнения резюме, и учетом своих достижений (да-да, было и такое), полученных во время работы в строительной компании, но нельзя так сказать. Ваня начал с того, что постарался объективно оценить свои возможности, локализовать и устранить пробелы в знаниях, и изучить требования к вакансиям. Последний аспект, кстати, оказался очень важным, т.к. занимаясь мониторингом требований, при желании можно не только разобраться с тем, что актуально в данный момент на рынке, но и подтянуть какие-то общие моменты и заполнить пробелы. Например, Ваня позиционирует себя как веб-разработчик C#/PHP - именно эти два языка он использовал в свой работе, но по факту, веб-приложения разрабатывал только на php/laravel. В связи с этим, он решает восполнить свои знания ASP.NET стеке, и без каких-либо промедлений изучает книгу ASP.NET MVC 5 для профессионалов Адама Фримена. Безусловно, знание технологии важны, но так как у нашего героя уже был опыт с MVC фреймворком, в этой книге Ваню очень зацепил архитектурный момент. Это очень подогрело интерес не только к технологии, но сформировало желание погрузиться в изучение нового.

Параллельно c изучением книг и статей, Ваня начал рассылать резюме в различные компании, откликаясь и на вакансии где требовались php и c# разработчики. Первая компания, которая согласилась продолжить диалог, выслала тестовое задание где нужно было разобраться в коде какого-то легаси, и выполнить определенные требования. Максимальная полезность для Вани была в том, что он увидел настоящий код проекта, написанного другими разработчиками, посмотрел как и что устроено, познакомился с DI, начал разбираться с паттернами (кстати, это была одна из задач - написать какие паттерны проектирования используются в проекте), и еще больше погрузился аспекты разработки ПО. Этот опыт был похож на снежный ком - знания потихоньку накапливались, наслаиваясь друг на друга, приходилось многое изучать и пробовать разрабатывать свое. Постепенно складывалась более четкая картинка того, что ждет начинающего разработчика в ИТ компании, какие навыки требуются и почему. На этом этапе я бы отметил два момента: если вы хотите научиться программировать, обязательно читайте чужой код, любой, разбирайте его по частям, изучайте логику проекта, а то, что для вас не понятно - гуглите и запоминайте, второй момент - пиши код, много кода, сами.

В итоге у Ваня было 4 интервью в 4 ИТ компаниях города. Стоит отметить, что 3 из них были на позицию C# разработчика и только 1 на php девелопера. На всех технических собеседованиях, когда его спрашивали про предыдущие проекты, Ваня успешно рассказывал над чем приходилось работать, и отвечал на вопросы, связанные с реализацией каких-то моментов. Это была его фишка, четко и без запинок, как будто бы заучено. Об этих проектах он знал все, так как был единственный разработчик в строительной компании. И все же, Ваня испытывал небольшие трудности на все четырех интервью, где-то ему даже казалось, что фидбэка от компании можно не ждать, но не стоит делать поспешные выводы и плохие мысли лучше сразу гнать в сторону. Не буду томить - статья и так уже получилась довольно большой, как мне кажется, от всех четырех компаний Ваня получил предложения о работе. Это может показаться фантастикой, выдумкой автора статьи, но ,тем не менее, так все и было на самом деле. Я бы хотел выделить два момента: обязательно мониторьте рынок вакансий и подготавливайтесь к каждому интервью - таким образом закрепите знания и будете чувствовать себя увереннее, и второй момент, никогда не расстраивайтесь если получите отказ после собеседования, продолжайте устранять пробелы в знаниях и проходить собеседования на новые позиции, ваша компания обязательно найдется!

Все ясно, но что было дальше?

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

Заключение

Спасибо, что дочитали статью до конца! Если она будет полезна хоть кому-то, значит я потратил свое время не зря :) Сейчас вспомнил, что не упомянул еще пару моментов, которые будут полезны новичкам не только в программировании, но и вообще в ИТ. Изучайте английский (без него ну прям никак) и прокачивайте ваши soft skills, это позволит вам стать более ценным специалистом.

Подробнее..

Powershell настоящий язык программирования. Скрипт оптимизации рутины в техподдержке

20.06.2021 14:08:21 | Автор: admin

Работая в компании IT-аутсорса в качестве руководителя 3 линии поддержки, задумался, как автоматизировать подключение сотрудников по RDP, через VPN к серверам десятков клиентов.

Таблички с адресами, паролями и прочими настройками серверов, конечно, хорошо, но поиск клиента и вбивание адресов с аккаунтами занимает довольно существенное время.
Держать все подключения к VPN в Windows не самая лучшая идея, да и при переустановке оного, создавать VPNы тоже не доставляет удовольствие.
Плюс к тому, в большинстве случаев, требуется установить VPN подключение к клиенту без использования шлюза. дабы не гонять весь интернет-трафик через клиента.
Задача, к тому же, осложняется тем, что у некоторых клиентов pptp, у кого-то l2tp, у некоторых несколько подсетей, туннели и т.п.

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

До написания этого скрипта-приложения программированием не занимался вообще, разве что лет 20 назад что-то пописывал на VBS в MS Excel и MS Access, поэтому не гарантирую красивость кода и принимаю критику от опытных программистов, как можно было бы сделать красивее.

В Powershell, начиная с Windows 8 и, конечно в Windows 10, появилась прекрасная возможность создавать VPN подключения командой Add-VpnConnection и указывать какие маршруты использовать с этими соединениями командой Add-VpnConnectionRoute, для использования VPN без шлюза.

На основании этих команд и создано данное приложение. Но, обо всем по порядку.

Для начала, создаем в Google Disk таблицу с именованными столбцами:
Number; Name; VPNname; ServerAddress; RemoteNetwork; VPNLogin; VPNPass; VPNType; l2tpPsk; RDPcomp; RDPuser; RDPpass; DefaultGateway; PortWinbox; WinboxLogin; WinboxPwd; Link; Inform

  • VPNname произвольное имя для VPN соединения

  • ServerAddress адрес VPN сервера

  • RemoteNetwork адреса подсети или подсетей клиента, разделенные ;

  • VPNLogin; VPNPass учетная запись VPN

  • VPNType -тип VPN (пока используется pptp или l2tp)

  • l2tpPsk PSK для l2tp, в случае pptp оставляем пустым

  • RDPcomp адрес сервера RPD

  • RDPuser; RDPpass учетная запись RPD

  • DefaultGateway принимает значение TRUE или FALSE и указывает на то, использовать ли Шлюз по умолчанию для этого соединения. В 90% случаев = FALSE

  • PortWinbox; WinboxLogin; WinboxPwd порт, логин и пароль для Winbox, поскольку у нас большинство клиентов использует Mikrotik)

  • Link ссылка на расширенную информацию о компании, например, на диске Google, или в любом другом месте, будет выводиться в информационном поле для быстрого доступа к нужной информации

Inform примечание

Пример таблицы доступен по ссылке

Number

Name

VPNname

ServerAddress

RemoteNetwork

VPNLogin

VPNPass

VPNType

l2tpPsk

RDPcomp

RDPuser

RDPpass

DefaultGateway

PortWinbox

WinboxLogin

WinboxPwd

Link

Inform

1

Тест1

Test1

a.b.c.d

192.168.10.0/24: 10.10.0.0/24

vpnuser

passWord

pptp

none

192.168.10.1

user

passWord

TRUE

8291

Admin

Admin

http://yandex.ru

тест

2

Тест2

Test2

e.f.j.k

192.168.2.0/24

vpnuser

passWord

l2tp

KdoSDtdP

192.168.2.1

user

passWord

FALSE

8291

Admin

Admin

Скриншот работающего приложения с затертыми данными:

Далее следует листинг приложения с комментариями и пояснениями. Если интересно, но непонятно, задавайте вопросы, постараюсь прокомментировать

function Get-Clients #Функция принимает строку адреса файла в Google Drive и возвращает в виде массива данных о клиентах{param([string]$google_url = "")[string]$xlsFile = $google_url$csvFile = "$env:temp\clients.csv"$Comma = ','Invoke-WebRequest $xlsFile -OutFile $csvFile$clients = Import-Csv -Delimiter $Comma -Path "$env:temp\clients.csv"Remove-Item -Path $csvFilereturn $clients}function Main {<#    Функция, срабатываемая при запуске скрипта#>Param ([String]$Commandline)#Иннициализируем переменные и присваиваем начальные значения. Здесь же, указываем путь к таблице с клиентами$Global:Clients = $null$Global:Current$Global:CurrentRDPcomp$Global:google_file = "https://docs.google.com/spreadsheets/d/1O-W1YCM4x3o5W1w6XahCJZpkTWs8cREXVF69gs1dD0U/export?format=csv" # Таблица скачивается сразу в виде csv-файла$Global:Clients = Get-Clients ($Global:google_file) # Присваиваем значения из таблицы массиву #Скачиваем Winbox64 во временную папку$download_url = "https://download.mikrotik.com/winbox/3.27/winbox64.exe"$Global:local_path = "$env:temp\winbox64.exe"If ((Test-Path $Global:local_path) -ne $true){$WebClient = New-Object System.Net.WebClient$WebClient.DownloadFile($download_url, $Global:local_path)}  #Разрываем все текущие VPN соединения (на всякий случай)foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }){Rasdial $item.Name /disconnect}  #Удаляем все, ранее созданные программой временные соединения, если вдруг не удалились при некорректном закрытии приложенияget-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force#Запускаем приложениеShow-MainForm_psf}#Собственно, само приложениеfunction Show-MainForm_psf{[void][reflection.assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')[void][reflection.assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')#Создаем форму и объекты формы[System.Windows.Forms.Application]::EnableVisualStyles()$formКлиентыАльбус = New-Object 'System.Windows.Forms.Form'$statusbar1 = New-Object 'System.Windows.Forms.StatusBar'$groupboxTools = New-Object 'System.Windows.Forms.GroupBox'$buttonPing = New-Object 'System.Windows.Forms.Button'$buttonВыход = New-Object 'System.Windows.Forms.Button'$buttonWindox = New-Object 'System.Windows.Forms.Button'$buttonПеречитатьДанные = New-Object 'System.Windows.Forms.Button'$buttonPingAll = New-Object 'System.Windows.Forms.Button'$groupboxRDP = New-Object 'System.Windows.Forms.GroupBox'$comboboxRDP = New-Object 'System.Windows.Forms.ComboBox'$textboxRDPLogin = New-Object 'System.Windows.Forms.TextBox'$textboxRdpPwd = New-Object 'System.Windows.Forms.TextBox'$buttonПодключитьRDP = New-Object 'System.Windows.Forms.Button'$groupboxVPN = New-Object 'System.Windows.Forms.GroupBox'$buttonПодключитьVPN = New-Object 'System.Windows.Forms.Button'$buttonОтключитьVPN = New-Object 'System.Windows.Forms.Button'$checkboxШлюзПоумолчанию = New-Object 'System.Windows.Forms.CheckBox'$richtextboxinfo = New-Object 'System.Windows.Forms.RichTextBox'$listbox_clients = New-Object 'System.Windows.Forms.ListBox'$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'  #----------------------------------------------# Обработчики событий#----------------------------------------------$formКлиентыАльбус_Load = {#При загрузке формы очистить поле информации и заполнить поле с клиентами (их названиями) $richtextboxinfo.Clear()$Global:Clients | ForEach-Object {[void]$listbox_clients.Items.Add($_.Name)} # В листбокс добавляем всех наших клиентов по именам и массива при загрузке формы}$listbox_clients_SelectedIndexChanged = {#Прочитать из массива информацию о клиенте при выборе его в поле listbox_clients (массив, как мы помним считан из файла с диска Google)$statusbar1.Text = 'Выбран клиент: ' + $listbox_clients.SelectedItem.ToString() # Пишем клиента в статусбар$Global:Current = $Global:Clients.Where({ $_.Name -eq $listbox_clients.SelectedItem.ToString() })If ($Current.PortWinbox -ne 0) # Если порт Winbox указан, то у клиента Mikrotik, включаем соответствующую кнопку{$buttonWindox.Enabled = $true$buttonWindox.Text = "Winbox"}$VPNname = $Global:Current.VPNname + "-tmp" #Добавляем к имени VPN соединения "-tmp" для указания метки временного соединения, чтобы при выходе удалить только ихswitch ($Global:Current.VPNType) #В зависимости от типа VPN пишем на кнопке "Подключить pptp VPN" или "Подключить l2tp VPN", если у клиента нет VPN, то пишем "Здесь нет VPN"{"pptp" {$buttonПодключитьVPN.Enabled = $true$buttonПодключитьVPN.Text = "Подключить pptp VPN"}"l2tp" {$buttonПодключитьVPN.Enabled = $true$buttonПодключитьVPN.Text = "Подключить l2tp VPN"}DEFAULT{$buttonПодключитьVPN.Enabled = $false$buttonПодключитьVPN.Text = "Здесь нет VPN"}}switch ($Global:Current.DefaultGateway) #Смотрим в массиве, используется ли у клиента "Шлюз по-умолчанию" и заполняем соответствующий чекбокс{"FALSE"{ $checkboxШлюзПоумолчанию.Checked = $false }"Нет"{ $checkboxШлюзПоумолчанию.Checked = $false }"TRUE"{ $checkboxШлюзПоумолчанию.Checked = $true }"Да"{ $checkboxШлюзПоумолчанию.Checked = $true }DEFAULT{ $checkboxШлюзПоумолчанию.Checked = $false }}$VPNStatus = (ipconfig | Select-String $VPNname -Quiet) #Проверяем, не установлено ли уже это VPN соединение?If ($VPNStatus) #Если установлено, то разблокируем кнопку "Подключить RDP"{$buttonПодключитьRDP.Enabled = $true}else{$buttonПодключитьRDP.Enabled = $false}$richtextboxinfo.Clear() #Очищаем информационное поле # И заполняем информацией о клиенте из массива$richtextboxinfo.SelectionColor = 'Black'$richtextboxinfo.Text = "Клиент: " + $Global:Current.Name + [System.Environment]::NewLine + `"Имя VPN: " + $Global:Current.VPNname + [System.Environment]::NewLine + `"Тип VPN: " + $Global:Current.VPNType + [System.Environment]::NewLine + `"Адрес сервера: " + $Global:Current.ServerAddress + [System.Environment]::NewLine + `"Подсеть клиента: " + $Global:Current.RemoteNetwork + [System.Environment]::NewLine + `"Адрес сервера RDP: " + $Global:Current.RDPcomp + [System.Environment]::NewLine + [System.Environment]::NewLine + `"DefaultGateway: " + $Global:Current.DefaultGateway + [System.Environment]::NewLine + [System.Environment]::NewLine + `"Примечание: " + [System.Environment]::NewLine + $Global:Current.Inform + [System.Environment]::NewLine + `"Connection '" + $VPNname + "' status is " + $buttonПодключитьRDP.Enabled + [System.Environment]::NewLine$richtextboxinfo.AppendText($Global:Current.Link)$RDPServers = $Global:Current.RDPcomp.Split(';') -replace '\s', '' #Считываем и разбираем RDP серверы клиента из строки с разделителем в массив#Добавляем из в выпадающее поле выбора сервера$comboboxRDP.Items.Clear()$comboboxRDP.Text = $RDPServers[0]foreach ($RDPServer in $RDPServers){$comboboxRDP.Items.Add($RDPServer)}#Заполняем поля имени и пароля RDP по умолчанию из таблицы о клиенте (при желании, их можно поменять в окне программы)$textboxRdpPwd.Text = $Global:Current.RDPpass$textboxRdpLogin.Text = $Global:Current.RDPuser} # Форма заполнена, при смене выбранного клиента произойдет перезаполнение полей в соответствии с выбранным клиентом$buttonWindox_Click = {#Обработка нажатия кнопки WinboxIf ($Global:Current.PortWinbox -ne 0) #Если порт Winbox заполнен, то открываем скачанный ранее Winbox, подставляем туда имя и пароль к нему и запускаем{$runwinbox = "$env:temp\winbox64.exe"$ServerPort = $Global:Current.ServerAddress + ":" + $Global:Current.PortWinbox$ServerLogin = " """ + $Global:Current.WinboxLogin + """"$ServerPass = " """ + $Global:Current.WinboxPwd + """"$Arg = "$ServerPort $ServerLogin $ServerPass "Start-Process -filePath $runwinbox -ArgumentList $Arg}}$buttonПодключитьVPN_Click = {#Обработка нажатия кнопки ПодключитьVPN$VPNname = $Global:Current.VPNname + "-tmp" #Добавляем к имени VPN соединения "-tmp" для указания метки временного соединения, чтобы при выходе удалить только их$richtextboxinfo.Clear() #Очищаем информационное поля для вывода туда информации о процессе подключения$richtextboxinfo.Text = "Клиент: " + $Global:Current.Name + [System.Environment]::NewLineforeach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }) #Разрываем все установленные соединения{$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLineRasdial $item.Name /disconnect}Remove-VpnConnection $VPNname -Force #Удаляем соединение, если ранее оно было создано$RemoteNetworks = $Global:Current.RemoteNetwork.Split(';') -replace '\s', '' #Считываем и разбираем по строкам в массив список подсетей клиента разделенный ;switch ($Global:Current.VPNType) #В зависимости от типа VPNа создаем pptp или l2tp соединение{"pptp" {$richtextboxinfo.Text = $richtextboxinfo.Text + "Создаем pptp подключение " + $VPNname + [System.Environment]::NewLineIf ($checkboxШлюзПоумолчанию.Checked -eq $false) #Если не используется "Шлюз по-умолчанию", то создаем VPN соединение без него и прописываем маршруты{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -SplitTunneling -Force -RememberCredential -PassThru) #Здесь происходит создание VPNforeach ($RemoteNetwork in $RemoteNetworks) #Добавляем все подсети клиента к этому VPN{$richtextboxinfo.AppendText('Добавляем маршрут к ' + $RemoteNetwork + [System.Environment]::NewLine)Add-VpnConnectionRoute -ConnectionName $VPNname -DestinationPrefix $RemoteNetwork -PassThru}}else #Если используется "Шлюз по-умолчанию", то создаем VPN соединение с ним и маршруты к клиенту не нужны{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -Force -RememberCredential -PassThru)}}"l2tp" {$richtextboxinfo.Text = $richtextboxinfo.Text + "Создаем l2tp подключение " + $Global:Current.VPNname + [System.Environment]::NewLineIf ($checkboxШлюзПоумолчанию.Checked -eq $false) #Если не используется "Шлюз по-умолчанию", то создаем VPN соединение без него и прописываем маршруты{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -L2tpPsk $Global:Current.l2tpPsk -SplitTunneling -Force -RememberCredential -PassThru) #Здесь происходит создание VPNforeach ($RemoteNetwork in $RemoteNetworks) #Добавляем все подсети клиента к этому VPN{$richtextboxinfo.AppendText('Добавляем маршрут к ' + $RemoteNetwork + [System.Environment]::NewLine)Add-VpnConnectionRoute -ConnectionName $VPNname -DestinationPrefix $RemoteNetwork -PassThru}}else #Если используется "Шлюз по-умолчанию", то создаем VPN соединение с ним и маршруты к клиенту не нужны{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -L2tpPsk $Global:Current.l2tpPsk -Force -RememberCredential -PassThru)}}}$richtextboxinfo.AppendText("Устанавливаем " + $Global:Current.VPNType + " подключение к " + $VPNname + [System.Environment]::NewLine)$Errcon = Rasdial $VPNname $Global:Current.VPNLogin $Global:Current.VPNPass #Устанавливаем созданное VPN подключение и выводим информацию в поле$richtextboxinfo.Text = $richtextboxinfo.Text + [System.Environment]::NewLine + $Errcon + [System.Environment]::NewLineIf ((ipconfig | Select-String $VPNname -Quiet)) #Проверяем успешность соединения и, если все удачно, разблокируем кнопку RDP  и кнопку "Отключить VPN"{$buttonПодключитьRDP.Enabled = $true$buttonОтключитьVPN.Visible = $true$buttonОтключитьVPN.Enabled = $true$statusbar1.Text = $Global:Current.Name + ' подключен'}}$formКлиентыАльбус_FormClosing = [System.Windows.Forms.FormClosingEventHandler]{#При закрытии формы подчищаем за собой. Разрываем и удаляем все созданные соединения. foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }){$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLineRasdial $item.Name /disconnect}$richtextboxinfo.Text = $richtextboxinfo.Text + "Удаляем все временные соединения" + [System.Environment]::NewLineget-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force#Удаляем информацию о RPD-серверах из реестра$Global:Clients | ForEach-Object {$term = "TERMSRV/" + $_.RDPcompcmdkey /delete:$term}}$buttonПодключитьRDP_Click = {#Обработка кнопки ПодключитьRDP$RDPcomp = $comboboxRDP.Text$RDPuser = $textboxRDPLogin.Text$RDPpass = $textboxRdpPwd.Textcmdkey /generic:"TERMSRV/$RDPcomp" /user:"$RDPuser" /pass:"$RDPpass"mstsc /v:$RDPcomp}$buttonОтключитьVPN_Click = {#При отключении VPN подчищаем за собой и оповещаем о процессе в поле информацииforeach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }){$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLineRasdial $item.Name /disconnect}$richtextboxinfo.Text = $richtextboxinfo.Text + "Удаляем все временные соединения" + [System.Environment]::NewLineget-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force$buttonОтключитьVPN.Visible = $false$buttonПодключитьRDP.Enabled = $false$statusbar1.Text = $Global:Current.Name + ' отключен'}$buttonPingAll_Click={#Пингуем всех клиентов и оповещаем о результатах$I=0$richtextboxinfo.Clear()$richtextboxinfo.SelectionColor = 'Black'$clientscount = $Global:Clients.count$Global:Clients | ForEach-Object {if ((test-connection -Count 1 -computer $_.ServerAddress -quiet) -eq $True){$richtextboxinfo.SelectionColor = 'Green'$richtextboxinfo.AppendText($_.Name +' ('+ $_.ServerAddress +') доступен' + [System.Environment]::NewLine)}else{$richtextboxinfo.SelectionColor = 'Red'$richtextboxinfo.AppendText($_.Name + ' (' + $_.ServerAddress + ')  недоступен (или закрыт ICMP)' + [System.Environment]::NewLine)}$richtextboxinfo.ScrollToCaret()$I = $I + 1Write-Progress -Activity "Ping in Progress" -Status "$i clients of $clientscount pinged" -PercentComplete ($i/$clientscount*100)}$richtextboxinfo.SelectionColor = 'Black'Write-Progress -Activity "Ping in Progress" -Status "Ready" -Completed}$buttonПеречитатьДанные_Click={#Перечитываем данные из таблицы Google$Global:Clients = Get-Clients ($Global:google_file)$listbox_clients.Items.Clear()$Global:Clients | ForEach-Object {[void]$listbox_clients.Items.Add($_.Name)}}$buttonВыход_Click = {#Выход$formКлиентыАльбус.Close()}$richtextboxinfo_LinkClicked=[System.Windows.Forms.LinkClickedEventHandler]{#Обработка нажатия на ссылку в окне информацииStart-Process $_.LinkText.ToString()}$buttonPing_Click={#Пингуем ip текущего клиента и выводим результат в поле информацииif ((test-connection -Count 1 -computer $Global:Current.ServerAddress -quiet) -eq $True){$richtextboxinfo.AppendText([System.Environment]::NewLine)$richtextboxinfo.SelectionColor = 'Green'$richtextboxinfo.AppendText($Global:Current.Name + ' (' + $Global:Current.ServerAddress + ') доступен' + [System.Environment]::NewLine)}else{$richtextboxinfo.AppendText([System.Environment]::NewLine)$richtextboxinfo.SelectionColor = 'Red'$richtextboxinfo.AppendText($Global:Current.Name + ' (' + $Global:Current.ServerAddress + ')  недоступен (или закрыт ICMP)' + [System.Environment]::NewLine)}}#----------------------------------------------#Описание объектов формы#----------------------------------------------## formКлиентыАльбус#$formКлиентыАльбус.Controls.Add($statusbar1)$formКлиентыАльбус.Controls.Add($groupboxTools)$formКлиентыАльбус.Controls.Add($groupboxRDP)$formКлиентыАльбус.Controls.Add($groupboxVPN)$formКлиентыАльбус.Controls.Add($richtextboxinfo)$formКлиентыАльбус.Controls.Add($listbox_clients)$formКлиентыАльбус.AutoScaleDimensions = '6, 13'$formКлиентыАльбус.AutoScaleMode = 'Font'$formКлиентыАльбус.AutoSize = $True$formКлиентыАльбус.ClientSize = '763, 446'$formКлиентыАльбус.FormBorderStyle = 'FixedSingle'$formКлиентыАльбус.MaximizeBox = $False$formКлиентыАльбус.Name = 'formКлиентыАльбус'$formКлиентыАльбус.SizeGripStyle = 'Hide'$formКлиентыАльбус.StartPosition = 'CenterScreen'$formКлиентыАльбус.Text = 'Клиенты Альбус'$formКлиентыАльбус.add_FormClosing($formКлиентыАльбус_FormClosing)$formКлиентыАльбус.add_Load($formКлиентыАльбус_Load)## statusbar1#$statusbar1.Location = '0, 424'$statusbar1.Name = 'statusbar1'$statusbar1.Size = '763, 22'$statusbar1.TabIndex = 17## groupboxTools#$groupboxTools.Controls.Add($buttonPing)$groupboxTools.Controls.Add($buttonВыход)$groupboxTools.Controls.Add($buttonWindox)$groupboxTools.Controls.Add($buttonПеречитатьДанные)$groupboxTools.Controls.Add($buttonPingAll)$groupboxTools.Location = '308, 258'$groupboxTools.Name = 'groupboxTools'$groupboxTools.Size = '147, 163'$groupboxTools.TabIndex = 10$groupboxTools.TabStop = $False$groupboxTools.Text = 'Tools'$groupboxTools.UseCompatibleTextRendering = $True## buttonPing#$buttonPing.Location = '7, 44'$buttonPing.Name = 'buttonPing'$buttonPing.Size = '133, 23'$buttonPing.TabIndex = 12$buttonPing.Text = 'Ping'$buttonPing.UseCompatibleTextRendering = $True$buttonPing.UseVisualStyleBackColor = $True$buttonPing.add_Click($buttonPing_Click)## buttonВыход#$buttonВыход.Location = '7, 125'$buttonВыход.Name = 'buttonВыход'$buttonВыход.Size = '133, 23'$buttonВыход.TabIndex = 15$buttonВыход.Text = 'Выход'$buttonВыход.UseCompatibleTextRendering = $True$buttonВыход.UseVisualStyleBackColor = $True$buttonВыход.add_Click($buttonВыход_Click)## buttonWindox#$buttonWindox.Enabled = $False$buttonWindox.Location = '7, 17'$buttonWindox.Name = 'buttonWindox'$buttonWindox.Size = '133, 23'$buttonWindox.TabIndex = 11$buttonWindox.Text = 'Windox'$buttonWindox.UseCompatibleTextRendering = $True$buttonWindox.UseVisualStyleBackColor = $True$buttonWindox.add_Click($buttonWindox_Click)## buttonПеречитатьДанные#$buttonПеречитатьДанные.Location = '7, 98'$buttonПеречитатьДанные.Name = 'buttonПеречитатьДанные'$buttonПеречитатьДанные.Size = '133, 23'$buttonПеречитатьДанные.TabIndex = 14$buttonПеречитатьДанные.Text = 'Перечитать данные'$buttonПеречитатьДанные.UseCompatibleTextRendering = $True$buttonПеречитатьДанные.UseVisualStyleBackColor = $True$buttonПеречитатьДанные.add_Click($buttonПеречитатьДанные_Click)## buttonPingAll#$buttonPingAll.Location = '7, 71'$buttonPingAll.Name = 'buttonPingAll'$buttonPingAll.Size = '133, 23'$buttonPingAll.TabIndex = 13$buttonPingAll.Text = 'Ping All'$buttonPingAll.UseCompatibleTextRendering = $True$buttonPingAll.UseVisualStyleBackColor = $True$buttonPingAll.add_Click($buttonPingAll_Click)## groupboxRDP#$groupboxRDP.Controls.Add($comboboxRDP)$groupboxRDP.Controls.Add($textboxRDPLogin)$groupboxRDP.Controls.Add($textboxRdpPwd)$groupboxRDP.Controls.Add($buttonПодключитьRDP)$groupboxRDP.Location = '308, 128'$groupboxRDP.Name = 'groupboxRDP'$groupboxRDP.Size = '147, 126'$groupboxRDP.TabIndex = 5$groupboxRDP.TabStop = $False$groupboxRDP.Text = 'RDP'$groupboxRDP.UseCompatibleTextRendering = $True## comboboxRDP#$comboboxRDP.FormattingEnabled = $True$comboboxRDP.Location = '7, 17'$comboboxRDP.Name = 'comboboxRDP'$comboboxRDP.Size = '133, 21'$comboboxRDP.TabIndex = 6$comboboxRDP.Text = 'IP RDP сервера'## textboxRDPLogin#$textboxRDPLogin.Location = '7, 44'$textboxRDPLogin.Name = 'textboxRDPLogin'$textboxRDPLogin.Size = '133, 20'$textboxRDPLogin.TabIndex = 7$textboxRDPLogin.Text = 'RDP-login'## textboxRdpPwd#$textboxRdpPwd.Location = '7, 69'$textboxRdpPwd.Name = 'textboxRdpPwd'$textboxRdpPwd.PasswordChar = '*'$textboxRdpPwd.Size = '133, 20'$textboxRdpPwd.TabIndex = 8$textboxRdpPwd.Text = 'RDP-Password'## buttonПодключитьRDP#$buttonПодключитьRDP.Enabled = $False$buttonПодключитьRDP.Location = '7, 94'$buttonПодключитьRDP.Name = 'buttonПодключитьRDP'$buttonПодключитьRDP.Size = '133, 20'$buttonПодключитьRDP.TabIndex = 9$buttonПодключитьRDP.Text = 'Подключить RDP'$buttonПодключитьRDP.UseCompatibleTextRendering = $True$buttonПодключитьRDP.UseVisualStyleBackColor = $True$buttonПодключитьRDP.add_Click($buttonПодключитьRDP_Click)## groupboxVPN#$groupboxVPN.Controls.Add($buttonПодключитьVPN)$groupboxVPN.Controls.Add($buttonОтключитьVPN)$groupboxVPN.Controls.Add($checkboxШлюзПоумолчанию)$groupboxVPN.Location = '308, 27'$groupboxVPN.Name = 'groupboxVPN'$groupboxVPN.Size = '147, 98'$groupboxVPN.TabIndex = 1$groupboxVPN.TabStop = $False$groupboxVPN.Text = 'VPN'$groupboxVPN.UseCompatibleTextRendering = $True## buttonПодключитьVPN#$buttonПодключитьVPN.Enabled = $False$buttonПодключитьVPN.Location = '7, 45'$buttonПодключитьVPN.Name = 'buttonПодключитьVPN'$buttonПодключитьVPN.Size = '133, 20'$buttonПодключитьVPN.TabIndex = 3$buttonПодключитьVPN.Text = 'Подключить VPN'$buttonПодключитьVPN.UseCompatibleTextRendering = $True$buttonПодключитьVPN.UseVisualStyleBackColor = $True$buttonПодключитьVPN.add_Click($buttonПодключитьVPN_Click)## buttonОтключитьVPN#$buttonОтключитьVPN.Enabled = $False$buttonОтключитьVPN.Location = '7, 67'$buttonОтключитьVPN.Name = 'buttonОтключитьVPN'$buttonОтключитьVPN.Size = '133, 20'$buttonОтключитьVPN.TabIndex = 4$buttonОтключитьVPN.Text = 'Отключить VPN'$buttonОтключитьVPN.UseCompatibleTextRendering = $True$buttonОтключитьVPN.UseVisualStyleBackColor = $True$buttonОтключитьVPN.Visible = $False$buttonОтключитьVPN.add_Click($buttonОтключитьVPN_Click)## checkboxШлюзПоумолчанию#$checkboxШлюзПоумолчанию.Location = '7, 19'$checkboxШлюзПоумолчанию.Name = 'checkboxШлюзПоумолчанию'$checkboxШлюзПоумолчанию.Size = '133, 24'$checkboxШлюзПоумолчанию.TabIndex = 2$checkboxШлюзПоумолчанию.Text = 'Шлюз по-умолчанию'$checkboxШлюзПоумолчанию.TextAlign = 'MiddleRight'$checkboxШлюзПоумолчанию.UseCompatibleTextRendering = $True$checkboxШлюзПоумолчанию.UseVisualStyleBackColor = $True## richtextboxinfo#$richtextboxinfo.Cursor = 'Default'$richtextboxinfo.ForeColor = 'WindowText'$richtextboxinfo.HideSelection = $False$richtextboxinfo.Location = '461, 27'$richtextboxinfo.Name = 'richtextboxinfo'$richtextboxinfo.ReadOnly = $True$richtextboxinfo.ScrollBars = 'ForcedVertical'$richtextboxinfo.ShowSelectionMargin = $True$richtextboxinfo.Size = '290, 394'$richtextboxinfo.TabIndex = 16$richtextboxinfo.Text = ''$richtextboxinfo.add_LinkClicked($richtextboxinfo_LinkClicked)## listbox_clients#$listbox_clients.FormattingEnabled = $True$listbox_clients.Location = '12, 27'$listbox_clients.Name = 'listbox_clients'$listbox_clients.Size = '290, 394'$listbox_clients.TabIndex = 0$listbox_clients.add_SelectedIndexChanged($listbox_clients_SelectedIndexChanged)#Save the initial state of the form$InitialFormWindowState = $formКлиентыАльбус.WindowState#Init the OnLoad event to correct the initial state of the form$formКлиентыАльбус.add_Load($Form_StateCorrection_Load)#Clean up the control events$formКлиентыАльбус.add_FormClosed($Form_Cleanup_FormClosed)#Store the control values when form is closing$formКлиентыАльбус.add_Closing($Form_StoreValues_Closing)#Show the Formreturn $formКлиентыАльбус.ShowDialog()}#Запуск приложения!Main ($CommandLine) 

Скрипт можно запускать как скрипт ps1 или скомпилировать в exe через ps2exe и использовать как полноценное приложение

Подробнее..

Recovery mode Задача о рюкзаке (Knapsack problem) простыми словами

05.06.2021 00:21:43 | Автор: admin

Пару лет назад столкнулся с так называемой задачей о рюкзаке на одном из собеседований, нашел быстренько решение в интернете. Попытался разобрать и.... ничего не понял. Кое как поменял названия переменных, а кто так не делает когда находит готовое решение для home таски? Отправил и забыл как страшный сон. Недавно друг скинул подобную задачу про монеты и в этот раз я уже быстренько разобрался с этой когда то, как казалось мне неподъемной задачкой.

Хотел бы отметить книгу Grokking Algorithms автора Aditya Bhargava. У нее прям максимально простым языком расписаны основы алгоритмов. Так что если, вы как и я в универе думали что алгоритмы вам никогда не пригодятся, потому что FAANG это не для вас. То я вас и разочарую и обрадую, попасть туда может каждый, если конечно приложит достаточно усилий, ну а разочарую тем что вам конечно придется поднапрячься и осилить алгоритмы и чем раньше вы начнете это делать, тем лучше.

На хабре, уже есть одна статья на эту тему: Алгоритм решения задачи о рюкзаке ( версия 2, исправленная) / Хабр (habr.com) . Но, да простит меня автор, на мой взгляд она совершенно непонятная.

И так, перейду к делу. Сначала расскажу обо всем на пальцах, а потом мы рассмотрим решение на нашем любимом C#.

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

Вещи которые есть в магазинеВещи которые есть в магазине

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

Есть несколько способов решения. Один из них это перебор всех вариантов.

Для простоты, предмета будет только три, поскольку количество различных комбинаций в которых их можно унести растет очень быстро и даже для 3 предметов уже будет равно 8. Оно вычисляется по формуле 2n где n - количество предметов, то есть если предмета будет 4, то количество возможных комбинаций достигнет уже 16 и так далее. Такой вариант нас не устроит поскольку решая эту задачу онлайн на каком-нибудь Codility ваше решение зарубят с Timeout Exceeded. Нужно что-то получше.

Мы будем решать эту задачу методом динамического программирования

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

Заполним небольшую табличку:

1

2

3

4

Ожерелье / 4000 / 4 ед

Кольцо / 2500 / 1 ед

Подвеска / 2000 / 3 ед

С левой стороны у нас будут все вещи. При этом порядок в котором они расположены, в данной задаче нас не интересует. Количество колонок равно количеству потенциальных рюкзаков размером от 1, минимально возможного целого положительного числа, до размера нашего рюкзака, с шагом 1. Таким образом мы пытаемся решить ряд более мелких задач, для рюкзаков размером 1, 2, 3 и 4. Всегда ведь проще начинать с малого?)

1

2

3

4

Ожерелье / 4000 / 4 ед

0

0

0

4000

Сверху у нас заполненный первый ряд. Мы можем использовать в каждом ряду, только вещь из этого ряда или вещи из верхних рядов.

Рюкзак размером 1: Ожерелье не влезет в рюкзак, значит стоимость того что мы можем унести равна 0.

Рюкзак размером 2: Ожерелье не влезет в рюкзак, значит стоимость того что мы можем унести равна 0.

Рюкзак размером 3: Ожерелье не влезет в рюкзак, значит стоимость того что мы можем унести равна 0.

Рюкзак размером 4: Здесь все становится немного интересней, вес ожерелья 4 и размер рюкзака 4. Ура, наконец то успех, мы не зря ворвались в магазин, кладем ожерелье в большой рюкзак и убегаем.

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

1

2

3

4

Ожерелье / 4000 / 4 ед

0

0

0

4000

Кольцо / 2500 / 1 ед

2500

2500

2500

4000

Добавилось кольцо. Делаем подсчеты для второго ряда:

Рюкзак размером 1: У нас добавилось кольцо и его размер как раз равен размеру даже самого маленького рюкзака. Но у нас ведь еще есть и ожерелье, мы уже проверили, оно не влезет. Кладем только кольцо.

Рюкзак размером 2: То же что и для размера 1. Кладем только кольцо.

Рюкзак размером 3: То же что и для размера 1. Кладем только кольцо.

Рюкзак размером 4: Здесь у нас есть выбор, либо положить только одно маленькое кольцо, либо взять ожерелье, которое тяжелее, но и стоит дороже. Забываем про кольцо, и возвращаемся за ожерельем!

Так, наконец добавим третий ряд

1

2

3

4

Ожерелье / 4000 / 4 ед

0

0

0

4000

Кольцо / 2500 / 1 ед

2500

2500

2500

4000

Подвеска / 2000 / 3 ед

2500

2500

2500

4500

Рюкзак размером 1: Кольцо дороже подвески, да и подвеска не влезет, она по всем параметрам проигрывает вещам выше, берем кольцо размером 1.

Рюкзак размером 2: Тоже самое что и для размера 1.

Рюкзак размером 3: Несмотря на то что мы можем здесь взять подвеску, кольцо выигрывает ее по параметрам, снова кладем ее.

Рюкзак размером 4: А вот тут у нас, весь рюкзак, и все возможные вещи. И мы видим что кольцо и подвеска вместе стоят на 500 долларов дороже ожерелья и при этом все так же влезут в рюкзак. Значит мы возьмем и кольцо и подвеску стоимостью 4500 и весом как раз 4 единицы.

В правом нижнем углу у нас верный результат.

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

Представим что для количества вещей у нас счетчик i, а для рюкзаков j.

На примере последней ячейки рассмотрим формулу в действии

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

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

И собственно алгоритм задачи на языке C#:

public static int[] weights = { 4, 1, 3 };public static int[] values = { 4000, 2500, 2000 };public static int CountMax(int[] weights, int[] values, int maxCapacity){    //строим массив и закладываем место на ячейки пустышки     //выходящие из левого верхнего угла    int[,] arr = new int[weights.Length + 1, maxCapacity + 1];    //проходим по всем вещам    for (int i = 0; i <= weights.Length; i++)    {        //проходим по всем рюкзакам        for (int j = 0; j <= maxCapacity; j++)        {            //попадаем в ячейку пустышку            if (i == 0 || j == 0)            {                arr[i, j] = 0;            }            else            {                   //если вес текущей вещи больше размера рюкзака                //казалось бы откуда значение возьмется для первой вещи                 //при таком условии. А оно возьмется из ряда пустышки                if (weights[i - 1] > j)                {                    arr[i, j] = arr[i - 1, j];                }                else                {                    //здесь по формуле. Значение над текущей ячейкой                    var prev = arr[i - 1, j];                    //Значение по вертикали: ряд вверх                    //и по горизонтали: вес рюкзака - вес текущей вещи                    var byFormula = values[i - 1] + arr[i - 1, j - weights[i - 1]];                    arr[i, j] = Math.Max(prev, byFormula);                }            }        }    }    // возвращаем правую нижнюю ячейку    return arr[weights.Length, maxCapacity];}

Всем побед и удачных собесов!

Подробнее..
Категории: Алгоритмы , C , Net , Algorithms , Knapsack problem

MEX (Minimum EXcluded) Алгоритм поиска минимального отсутствующего числа

13.06.2021 20:13:49 | Автор: admin
Добрый день. Сегодня хочется поговорить о том, как найти MEX (минимальное отсутствующие число во множестве).


Мы разберем три алгоритма и посмотрим на их производительность.

Добро пожаловать под cat

Предисловие


Перед тем как начать, хотелось бы рассказать почему я вообще за этот алгоритм взялся?
Всё началось с задачки на OZON.


Как видно из задачи, в математике результатом работы функции MEX на множестве чисел является наименьшим значением из всего набора, который не принадлежит этому множеству. То есть это минимальное значение набора дополнений. Название MEX является сокращением для Minimum EXcluded значение.

И покопавшись в сети, оказалось, что нет общепринятого алгоритма нахождения MEX

Есть решения в лоб, есть варианты с дополнительными массивами, графами, но, как-то всё это раскидано по разным углам интернета и нет единой нормальной статьи по этому поводу. Вот и родилась идея написать эту статью. В этой статье мы разберем три алгоритм нахождения MEX и посмотрим, что у нас получиться по скорости и по памяти.
Код будет на языке C#, но в целом там не будет специфичных конструкций.
Базовый код для проверок будет таким.

static void Main(string[] args)        {            //MEX = 2            int[] values = new[] { 0, 12, 4, 7, 1 };                        //MEX = 5            //int[] values = new[] { 0, 1, 2, 3, 4 };                        //MEX = 24            //int[] values = new[] { 11, 10, 9, 8, 15, 14, 13, 12, 3, 2, 0, 7, 6, 5, 27, 26, 25, 4, 31, 30, 28, 19, 18, 17, 16, 23, 22, 21, 20, 43, 1, 40, 47, 46, 45, 44, 35, 33, 32, 39, 38, 37, 36, 58, 57, 56, 63, 62, 60, 51, 49, 48, 55, 53, 52, 75, 73, 72, 79, 77, 67, 66, 65, 71, 70, 68, 90, 89, 88, 95, 94, 93, 92, 83, 82, 81, 80, 87, 86, 84, 107, 106, 104 };                        //MEX = 1000            //int[] values = new int[1000];            //for (int i = 0; i < values.Length; i++) values[i] = i;                        //Импровизированный счетчик итераций            int total = 0;            int mex = GetMEX(values, ref total);            Console.WriteLine($"mex: {mex}, total: {total}");            Console.ReadKey();        }

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

1) Решение в лоб


Как нам найти минимальное отсутствующие число? Самый простой вариант сделать счетчик и перебирать массив до тех пор, пока не найдем число равное счетчику.

  static int GetMEX(int[] values, ref int total)        {            for (int mex = 0; mex < values.Length; mex++)            {                bool notFound = true;                for (int i = 0; i < values.Length; i++)                {                    total++;                    if (values[i] == mex)                    {                        notFound = false;                        break;                    }                }                if (notFound)                {                    return mex;                }            }            return values.Length;        }    }


Максимально базовый случай. Сложность алгоритма составляет O(n*cell(n/2)) Т.к. для случая { 0, 1, 2, 3, 4 } нам нужно будет перебрать все числа т.к. совершить 15 операций. А для полностью заполного ряда из 100 числе 5050 операций Так себе быстродейственность.

2) Просеивание


Второй по сложности вариант в реализации укладывается в O(n) Ну или почти O(n), математики хитрят и не учитывают подготовку данных Ибо, как минимум, нам нужно знать максимальное число во множестве.
С точки зрения математики выглядит так.
Берется битовый массив S длинной m (где m максимально число в изначально массиве (V) + 1) заполненный 0. И в один проход исходному множеству (V) в массиве (S) ставятся 1. После этого в один проход находим первое пустое значение.
static int GetMEX(int[] values, ref int total)        {            //Не учитываем в сложности             var max = values.Max() + 1;            bool[] sieve = new bool[max];            for (int i = 0; i < values.Length; i++)            {                total++;                sieve[values[i]] = true;            }            for (int i = 0; i < sieve.Length; i++)            {                total++;                if (!sieve[i])                {                    return i;                }            }            return values.Length;        }

Т.к. математики хитры люди. То они говорят, что алгоритм O(n) ведь проход по массиву исходному массиву всего один
Т.к. математики хитры люди. И говорят, что алгоритм O(n) ведь проход по массиву исходному массиву всего один
Вот сидят и радуются, что такой крутой алгоритм придумали, но правда такова.
Первое нужно найти максимально число в исходном массиве O1(n)
Второе нужно пройтись по исходному массиву и отметить это значение в массиве S O2(n)
Третье нужно пройтись массиве S и найти там первую попавшеюся свободную ячейку O3(n)
Итого, т.к. все операции в целом не сложные можно упростить все расчеты до O(n*3)
Но это явно лучше решения в лоб Давайте проверим на наших тестовых данных:
1) Для случая { 0, 12, 4, 7, 1 }: В лоб: 11 итераций, просеивание: 13 итераций
2) Для случая { 0, 1, 2, 3, 4 }: В лоб: 15 итераций, просеивание: 15 итераций
3) Для случая { 11,}: В лоб: 441 итерация, просеивание: 191 итерация
4) Для случая { 0,,999}: В лоб: 500500 итераций, просеивание: 3000 итераций

Дело в том, что если отсутствующие значение является небольшим числом, то в таком случае решение в лоб оказывается быстрее, т.к. не требует тройного прохода по массиву. Но в целом, на больших размерностях явно проигрывает просеиванью, что собственно неудивительно.
С точки зрения математика алгоритм готов, и он великолепен, но вот с точки зрения программиста он ужасен из-за объема оперативной памяти, израсходованной впустую, да и финальный проход для поиска первого пустого значения явно хочется ускорить.
Давайте сделаем это, и оптимизируем код.
static int GetMEX(int[] values, ref int total)        {            total = values.Length;            var max = values.Max() + 1;            var size = sizeof(ulong) * 8;            ulong[] sieve = new ulong[(max / size) + 1];            ulong one = 1;            for (int i = 0; i < values.Length; i++)            {                total++;                sieve[values[i] / size] |= (one << (values[i] % size));            }            var maxInblock = ulong.MaxValue;            for (int i = 0; i < sieve.Length; i++)            {                total++;                if (sieve[i] != maxInblock)                {                    for (int j = 0; j < size; j++)                    {                        total++;                        if ((sieve[i] & (one << j)) == 0)                        {                            return i * size + j;                        }                    }                }            }            return values.Length;        }

Что мы тут сделали. Во-первых, в 64 раза уменьшили количество оперативной памяти, которая необходима.
var size = sizeof(ulong) * 8;ulong[] sieve = new ulong[(max / size) + 1];
Во-вторых, оптимизировали фальную проверку: мы проверяем сразу блок на вхождение первых 64 значений: if (sieve[i] != maxInblock) и как только убедились в том, что значение блока не равно бинарным 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111, только тогда ищем уже вхождение на уровне блока: ((sieve[i] & (one << j)) == 0
В итоге алгоритм просеивание нам дает следующие результат:
1) Для случая { 0, 12, 4, 7, 1 }: просеивание: 13 итераций, просеивание с оптимизацией: 13 итераций
2) Для случая { 0, 1, 2, 3, 4 }: В лоб: 15 итераций, просеивание с оптимизацией: 16 итераций
3) Для случая { 11,}: В лоб: 191 итерация, просеивание с оптимизацией: 191 итерации
4) Для случая { 0,,999}: В лоб: 3000 итераций, просеивание с оптимизацией: 2056 итераций

Так, что в итоге в теории по скорости?
O(n*3) мы превратили в O(n*2) + O(n / 64) в целом, чуть увеличили скорость, да еще объём оперативной памяти уменьшили аж в 64 раза. Что хорошо)

3) Сортировка


Как не сложно догадаться, самый простой способ найти отсутствующий элемент во множестве это иметь отсортированное множество.
Самый быстрый алгоритм сортировки это quicksort (быстрая сортировка), которая имеет сложность в O1(n log(n)). И итого мы получим теоретическую сложность для поиска MEX в O1(n log(n)) + O2(n)
static int GetMEX(int[] values, ref int total)        {            total = values.Length * (int)Math.Log(values.Length);            values = values.OrderBy(x => x).ToArray();            for (int i = 0; i < values.Length - 1; i++)            {                total++;                if (values[i] + 1 != values[i + 1])                {                    return values[i] + 1;                }            }            return values.Length;        }

Шикарно. Ничего лишнего)
Проверим количество итераций
1) Для случая { 0, 12, 4, 7, 1 }: просеивание с оптимизацией: 13, сортировка: ~7 итераций
2) Для случая { 0, 1, 2, 3, 4 }: просеивание с оптимизацией: 16 итераций, сортировка: ~9 итераций
3) Для случая { 11,}: просеивание с оптимизацией: 191 итерации, сортировка: ~356 итераций
4) Для случая { 0,,999}: просеивание с оптимизацией: 2056 итераций, сортировка: ~6999 итераций

Здесь указаны средние значения, и они не совсем справедливы. Но в целом: сортировка не требует дополнительной памяти и явно позволяет упростить последний шаг в переборе.
Примечание: values.OrderBy(x => x).ToArray() да я знаю, что тут выделилась память, но если делать по уму, то можно изменить массив, а не копировать его


Вот у меня и возникла идея оптимизировать quicksort для поиска MEX. Данный вариант алгоритма я не находил в интернете, ни с точки зрения математики, и уж тем более с точки зрения программирования. То код будем писать с 0 по дороге придумывая как он будет выглядеть :D

Но, для начала, давайте вспомним как вообще работает quicksort. Я бы ссылку дал, но нормально пояснения quicksort на пальцах фактически нету, создается ощущение, что авторы пособий сами разбираются в алгоритме пока его рассказывают про него
Так вот, что такое quicksort:
У нас есть неупорядоченный массив { 0, 12, 4, 7, 1 }
Нам потребуется случайное число, но лучше взять любое из массива, это называется опорное число (T).
И два указателя: L1 смотрит на первый элемент массива, L2 смотрит на последний элемент массива.
0, 12, 4, 7, 1
L1 = 0, L2 = 1, T = 1 (T взял тупа последние)

Первый этап итерации:
Пока работам только с указателем L1
Сдвигаем его по массиву вправо пока не найдем число больше чем наше опорное.
В нашем случае L1 равен 8

Второй этап итерации:
Теперь сдвигаем указатель L2
Сдвигаем его по массиву влево пока не найдем число меньше либо равное чем наше опорное.
В данном случае L2 равен 1. Т.к. я взял опорное число равным крайнему элементу массива, а туда же смотрел L2.

Третей этап итерации:
Меняем числа в указателях L1 и L2 местами, указатели не двигаем.
И переходим к первому этапу итерации.
Эти этапы мы повторяем до тех пор, пока указатели L1 и L2 не будет равны, не значения по ним, а именно указатели. Т.е. они должны указывать на один элемент.

После того как указатели сойдутся на каком-то элементе, обе части массива будут всё еще не отсортированы, но уже точно, с одной стороны объеденных указателей (L1 и L2) будут элементы, которые меньше T, а со второй больше T. Именно этот факт нам и позволяет разбить массив на две независимые группы, которые мы сортируем можно сортировать в разных потоках в дальнейших итерациях.
Статья на wiki, если и у меня непонятно написанно

Напишем Quicksort
static void Quicksort(int[] values, int l1, int l2, int t, ref int total)        {            var index = QuicksortSub(values, l1, l2, t, ref total);            if (l1 < index)            {                Quicksort(values, l1, index - 1, values[index - 1], ref total);            }            if (index < l2)            {                Quicksort(values, index, l2, values[l2], ref total);            }        }        static int QuicksortSub(int[] values, int l1, int l2, int t, ref int total)        {            for (; l1 < l2; l1++)            {                total++;                if (t < values[l1])                {                    total--;                    for (; l1 <= l2; l2--)                    {                        total++;                        if (l1 == l2)                        {                            return l2;                        }                        if (values[l2] <= t)                        {                            values[l1] = values[l1] ^ values[l2];                            values[l2] = values[l1] ^ values[l2];                            values[l1] = values[l1] ^ values[l2];                            break;                        }                    }                }            }            return l2;        }


Проверим реальное количество итераций:
1) Для случая { 0, 12, 4, 7, 1 }: просеивание с оптимизацией: 13, сортировка: 11 итераций
2) Для случая { 0, 1, 2, 3, 4 }: просеивание с оптимизацией: 16 итераций, сортировка: 14 итераций
3) Для случая { 11,}: просеивание с оптимизацией: 191 итерации, сортировка: 1520 итераций
4) Для случая { 0,,999}: просеивание с оптимизацией: 2056 итераций, сортировка: 500499 итераций

Попробуем поразмышлять вот над чем. В массиве { 0, 4, 1, 2, 3 } нет недостающих элементов, а его длина равно 5. Т.е. получается, массив в котором нет отсутствующих элементов равен длине массива 1. Т.е. m = { 0, 4, 1, 2, 3 }, Length(m) == Max(m) + 1. И самое главное в этом моменте, что это условие справедливо, если значения в массиве переставлены местами. И важно то, что это условие можно распространить на части массива. А именно вот так:
{ 0, 4, 1, 2, 3, 12, 10, 11, 14 } зная, что в левой части массива все числа меньше некого опорного числа, например 5, а в правой всё что больше, то нет смысла искать минимальное число слева.
Т.е. если мы точно знаем, что в одной из частей нет элементов больше определённого значения, то само это отсутствующие число нужно искать во второй части массива. В целом так работает алгоритм бинарного поиска.
В итоге у меня родилась мысль упростить quicksort для поиска MEX объединив его с бинарным поиском. Сразу скажу нам не нужно будет полностью отсортировывать весь массив только те части, в которых мы будем осуществлять поиск.
В итоге получаем код
static int GetMEX(int[] values, ref int total)        {            return QuicksortMEX(values, 0, values.Length - 1, values[values.Length - 1], ref total);        }        static int QuicksortMEX(int[] values, int l1, int l2, int t, ref int total)        {            if (l1 == l2)            {                return l1;            }            int max = -1;            var index = QuicksortMEXSub(values, l1, l2, t, ref max, ref total);            if (index < max + 1)            {                return QuicksortMEX(values, l1, index - 1, values[index - 1], ref total);            }            if (index == values.Length - 1)            {                return index + 1;            }            return QuicksortMEX(values, index, l2, values[l2], ref total);        }        static int QuicksortMEXSub(int[] values, int l1, int l2, int t, ref int max, ref int total)        {            for (; l1 < l2; l1++)            {                total++;                if (values[l1] < t && max < values[l1])                {                    max = values[l1];                }                if (t < values[l1])                {                    total--;                    for (; l1 <= l2; l2--)                    {                        total++;                        if (values[l2] == t && max < values[l2])                        {                            max = values[l2];                        }                        if (l1 == l2)                        {                            return l2;                        }                        if (values[l2] <= t)                        {                            values[l1] = values[l1] ^ values[l2];                            values[l2] = values[l1] ^ values[l2];                            values[l1] = values[l1] ^ values[l2];                            break;                        }                    }                }            }            return l2;        }

Проверим количество итераций
1) Для случая { 0, 12, 4, 7, 1 }: просеивание с оптимизацией: 13, сортировка MEX: 8 итераций
2) Для случая { 0, 1, 2, 3, 4 }: просеивание с оптимизацией: 16 итераций, сортировка MEX: 4 итераций
3) Для случая { 11,}: просеивание с оптимизацией: 191 итерации, сортировка MEX: 1353 итераций
4) Для случая { 0,,999}: просеивание с оптимизацией: 2056 итераций, сортировка MEX: 999 итераций

Итого


Мы получили разны варианты поиска MEX. Какой из них лучше решать вам.
В целом. Мне больше всех нравится просеивание, и вот по каким причинам:
У него очень предсказуемое время выполнения. Более того, этот алгоритм можно легко использовать в многопоточном режиме. Т.е. разделить массив на части и каждую часть пробегать в отдельном потоке:
for (int i = minIndexThread; i < maxIndexThread; i++)sieve[values[i] / size] |= (one << (values[i] % size));

Единственное, нужен lock при записи sieve[values[i] / size]. И еще алгоритм идеален при выгрузки данных из базы данных. Можно грузить пачками по 1000 штук например, в каждом потоке и всё равно он будет работать.
Но если у нас строгая нехватка памяти, то сортировка MEX явно выглядит лучше.

П.с.
Я начал рассказ с конкурса на OZON в котором я пробовал участвовал, сделав предварительный вариант алгоритма просеиванья, приз за него я так и не получил, OZON счел его неудовлетворительным По каким именно причин он так и не сознался До и кода победителя я не видел. Может у кого-то есть идеи как можно решить задачу поиска MEX лучше?
Подробнее..

Регистрация на Microsoft Build 2021 уже началась

19.05.2021 10:20:52 | Автор: admin

Учитесь. Общайтесь. Пишите код.

Конференция Build ключевое событие года для Microsoft. На мероприятии выступают первые лица компании, в том числе, ее глава Сатья Наделла. Это 48 часов погружения в технологические инновации и общения с глобальным технологическим сообществом. Регистрация на конференцию бесплатна.

Что вас ждет

В течение двух дней вас будут ждать:

  • Технические доклады

  • Серии вопросов и ответов с экспертами

  • Обучающие мероприятия Learn Live

  • Общение с участниками из вашего локального сообщества (Local Connections)

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

Бесплатная регистрация необходима для участия в мероприятиях Microsoft Build, даже если вы уже регистрировались на прошлые мероприятия.

Подробности и регистрация.

Загляните в будущее технологий,
присоединяйтесь к Microsoft Build!

Подробнее..

О классах Program и Startup инициализация ASP.NET приложения. Часть I Program иIHostBuilder

30.05.2021 20:12:29 | Автор: admin

Введение. О чем эта статья.

Не так давно на Хабре я увидел статью с многообещающим названием "Что из себя представляет класс Startup и Program.cs в ASP.NET Core" (http://personeltest.ru/aways/habr.com/ru/company/otus/blog/542494/). Меня всегда нтересовало и интересует, что именно происходит под капотом той или иной библиотеки или фреймворка, с которыми мне доводится работать. И к веб-приложениям на ASP.NET Core это относится в полной мере. И я надеялся получить из этой статьи новую информацию о том, как работают упомянутые классы при запуске такого приложения. Та статья, к сожалению, меня разочаровала: в ней всего лишь в очередной раз был пересказан кусок руководства, никакой новой информации я оттуда не получил. И при чтении ее я подумал, что, наверное, есть и другие люди, которым, как и мне, интересно не просто знать, как применять тот или иной фреймворк (ASP.NET Core в данном случае), но и как он работает. А так как я по разным причинам последнее время довольно сильно углубился во внутреннее устройство ASP.NET Core, то я подумал, что теперь мне есть много что рассказать о нем из того, что выходит за рамки руководств. И вот потому я решил для начала написать статью про то, что действительно представляют из себя классы Startup и Program - так, чтобы рассказать не столько о том, как ими пользоваться (это есть в многочисленных руководствах, которые, как мне кажется, нет смысла дублировать), а, в основном, о том, как работают эти классы, причем - в контексте работы всего веб-приложения на ASP.NET Core. Однако поскольку необъятное объять нельзя, то предмет этот статьи ограничен. Прежде всего, она ограничивается рассказом только про веб-приложения, созданные с использованием нового типа шаблона приложения - Generic Host. Во-вторых, статья будет посвящена только тому, как происходит инициализация веб-приложения, потому что основная роль рассматриваемых классов именно такова - инициализация и запуск размещенного приложения. Итак, кому рассматриваемая тема, даже в столь ограниченном объеме, интересна - добро пожаловать под кат.

Введение. Продолжение.

Для начала немного расширю вводную часть статьи - потому что до ката поместилось не все, про что хотелось там написать. Но, прежде всего - краткое содержание статьи (под спойлером):

TL;DR

(сразу предупреждаю: тут - далеко не всё).

Инициализация приложения, сделанного по шаблону Generic Host выглядит следующим образом.

  1. Создается объект построителя размещения (Host), реализующий интерфейс IHostBuilder

  2. Затем выполняется стадия конфигурирования, на которой производится конфигурирование компонентов приложения - либо путем вызовов методов интерфейса IHostBuilder напрямую, либо путем вызова методов расширения для этого интерфейса - статических методов, определенных в других классах. Эти методы расширения, как правило, производят конфигурирование вызовами все тех же методов интерфейса IHostBuilder.

  3. Конфигурирование производится путем передачи в методы интерфейса IHostBuilder процедур-делегатов. Эти делегаты помещаются в очередь соответсвующего этапа, для которого они переданы (этап определяется именем вызываемого для этого метода IHostBuilder). Они будут выполнены впоследствии на соответствующих этапах построения объекта приложения Generic Host (иначе - размещения, Host).

  4. После стадии конфигурирования производится создание объекта размещения (приложения), реализующего интерфейс IHost, оно производится вызовом метода Build интерфейса IHostBuilder построителя.

  5. Создание объекта размещения в реализации по умолчанию производится построителям в несколько этапов. Эти этапы : создание конфигурации построителя, создание объектов окружения, создание конфигурации приложения, создание контейнера сервисов, включая сервисы параметров (options). После создания контейнера сервисов построитель извлекает из него реализацию интерфейса IHost и возвращает как результат вызова метода Build.

  6. На ряде этапов происходит вызов на выполнение делегатов, переданных построителю на этапе конфигурирования и хранящихся в очередях. Список этих этапов: этап создания конфигурации построителя; этап создания конфигурации приложения; подэтап конфигурирования списка регистрации сервисов этапа создания контейнера сервисов; подэтап конфигурирования контейнера-построителя этапа создания контейнера сервисов.

  7. После создания объекта приложения он запускается, что в конечно итоге приводит к запуску метода StartAsync реализации интерфейса IHost. В реализации по умолчанию этот метод выбирает из контейнера сервисов все зарегистрированные компоненты приложения - реализации интерфейса IHostedService - и асинхронно запускает их методы StartAsync. Эти методы могут выполнять инициализацию, зависящую от конкретного компонента приложения.

На этом предмет рассмотрения данной статьи заканчивается.

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

Лирическое отступление: о лирических отступлениях вообще

Когда я читал код ASP.NET Core - написанный в весьма непривычном для меня стиле - я много думал, много разного. Но в этой статье я постарался оставить все эти мысли о коде при себе, и описать в основной части статьи все максимально кратко и по существу того, что происходит в коде, что и как в нем делается - а не что я думаю при прочтении соответствующего фрагмента кода. В конце концов, именно информация о работе программы - это то основное, что, как я полагаю, хотят увидеть читатели этой статьи. А все свои мысли по поводу виденного, которые мне сдержать не удалось, я убрал под спойлер с пометкой "Лирическое отступление", отдельно от описания работы кода. Так что те читатели, кому мое личное мнение не кажется интересным, могут вообще не открывать соответствующие куски: там действительно нет никакой информации о работе фреймворка ASP.NET. И ещё: я прошу не обсуждать это мое личное мнение в комментариях к этой статье - я не считаю его настолько ценным, чтобы тратить на это время тех, кому нужна, прежде всего, информация по рассматриваемой теме (включая мое время, кстати). А обсуждать в комментариях прошу только тему самой статьи: прояснять и уточнять, как происходит инициализация веб-приложения, созданного по шаблону Generic Host, насколько удачна и понята терминология и т.д. Если же мое личное мнение о современно программировании вдруг окажется интересным более-менее заметной доле читателей - я готов изложить его для них в отдельной статье: там можно будет сделать это более систематично и подробно, и там можно обсудить его в комментариях, не отвлекая тех, кто пришел за информацией о продукте, а не за моим мнением.

детали реализации: описание убрано под спойлер

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

Вернемся однако к содержанию статьи. Для начала хочется сказать о коде ASP.NET Core в целом. Не знаю как для других, а для меня стиль написания этого кода оказался весьма непривычен. Уже при первом взгляде на файлы исходного текста, которые нам любезно генерирует мастер создания нового проекта в Visual Studio видно, что программы на ASP.NET Core принято писать в "современном, модном, молодежном" стиле. И более глубокое рассмотрение самих исходных текстов ASP.NET Core это подтверждает.

Лирическое отступление: О современных приемах написания кода

Однако, продолжу: мое внимание не могло не пройти мимо того факта, что в ASP.NET Core широко используются такие новаторские современные (и не очень) приемы написания кода, как объединение вызовов методов в цепочку через точку, методы расширения классов/интерфейсов, определенные в других классах, новые модификации синтаксиса, позволяющие писать код сокращенно ("синтаксический сахар"), вложенные функции, стрелочные (они же - лямбда-)функции (в том числе - и для написания обычных методов, и с фигурными скобками, и с вызовом других лямбда-функций внутри лямбда-функций), передача переменных в эти вложенные методы и лямбда-функции через автоматически генерируемые компилятором замыкания, в том числе - и при создании делегатов, запоминаемых в свойствах других объектов, широкое применение ключевого слова var, позволяющего компилятору (и читающему код - тоже) самому догадываться о типах определенных таким образом переменных, не менее широкое применение делегатов для задания значений полей/свойств объектов вместо простого присвоения этим полям/свойствам - короче целый арсенал приемов, ранее не используемых в C#. Да, я понимаю, что все эти приемы очень ценны, и особенно - в плане повышения продуктивности программиста при написании кода путем экономии количества знаков, необходимых для первоначальной записи программы ;-). Но конкретно мне (почему-то) читать такой код оказалось не всегда просто.

А ещё, разбираясь с исходным кодом ASP.NET, я узнал много новых приемов для написания кода в духе настоящих программистов - приемов ничуть не менее эффективных ;-) , чем освященные временем операторы GOTO и циклы DO на 5 страницах из арсенала настоящих программистов древности. И, вообще-то, я надеюсь опубликовать статью, как можно пользоваться этими приемами для написания действительно сложной для понимания программы, не навлекая на себя при этом обвинений в нарушении принципов чистоты кода и других современных верований о том, как надлежит писать программы, из которой многие, надеюсь, подчерпнут для себя знания этих приемов ;-) (как, надеюсь, все поняли из вышенаписанного, статья планируется несерьезной - ну, или наполовину серьезной). Но вернемся к рассматриваемому вопросу.

Важная особенность кода ASP.NET Core, в целом, и шаблона Generic Host, в частности - широкое использование модного современного подхода (да, я специально его не называю тут по имени, почему - см. лирическое отступление), в котором написанный программистом код работает с некими интерфейсами, получаемые тем или иным методом из специального объекта фреймворка - контейнера сервисов.

Лирическое отступление: что тебе в имени моем

Тем, кого учили теории, придут по поводу этого подхода в голову какие-нибудь умные слова, которым их научили - типа "принцип инверсии зависимостей". И вспомнится буква D в слове "SOLID". А то и другие умные слова вспомнятся: "инверсия управления", "внедрение зависимостей". Но по моему личному мнению без упоминания этих слов вполне можно обойтись - они не дают ничего для понимания работы конкретной программы. И вообще, говорят, что настоящим программистам не требуются абстрактные концепции, чтобы делать конкретную работу:в те древние времена, про которые была написана статья о настоящих программистах, откуда взяты эти слова, им для этого, якобы, требовался компилятор с Фортрана и пиво, а что требуется для этого настоящим программистам сейчас - об этом я не в курсе: я программист ненастоящий. Но про все эти умные слова и про все, якобы, предоставляемые ими преимущества я тут писать не буду - я просто буду описывать все ровно так, как оно есть в коде, без приплетания сюда абстрактных понятий. Надеюсь, настоящим программистам это понравится. Так что вернемся к нашим баранам.

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

Лирическое отступление: попробуй найди

Да, я слышал, что у этого подхода с сокрытием реализации интерфейсов есть многочисленные достоинства. Но и неудобства такой подход тоже создает: потому что зачастую сложно понять, какой код здесь выполняется, а ведь именно код является самой точной спецификацией программы - спецификации из документации не всегда полны/точны/бывают правильно поняты - а потому возоможность посмотреть конкретный код - она IMHO не лишняя. И IDE, которая помогает справляться со многими неясностями, в этом случае мало может подсказать, что стоит за этими интерфейсами: потому что путем статического анализа невозможно определить, какой именно код, в каком именно классе, в каком именно файле исходного текста находящийся, будет реализовывать эти интерфейсы, их методы и свойства. Это в немалой степени затрудняет, на мой взгляд, использование исходного текста фреймворка для выяснения точного поведения этого кода (что иногда бывает реально надо, особенно - при отладке).

И потому одной из задач данной статьи я считаю как раз прояснение вопроса, какие именно классы стоят за теми или иными интерфейсами в процессе выполнения приложения ASP.NET, и как именно они эти интерфейсы реализуют.

Конечно, реальная жизнь есть реальная жизнь, и разработчикам ASP.NET в некоторых местах пришлось-таки написать код так, чтобы он учитывал, какой каким именно классом реализуется интерфейс, который используется данным кодом, и менять на основе этого знания путь выполнения программы Но сделано это было весьма изящно и тоже в рамках того же подхода "используй интерфейсы вместо классов": код в таких местах проверяет, реализует ли объект, который стоит за используемым интерфейсом, некий другой интерфейс, и в зависимости от этого использует разные пути выполнения. Конкретно про это будет рассказано при рассмотрении соответствующих частей кода во второй части статьи.

Далее стоит упомянуть, что исторически для ASP.NET, кроме Generic Host существует другой, более старый тип шаблона веб-приложения: Web Host. Хотя производителем (Microsoft) он в рассматриваемой в статье версии ASP.NET Core (статья писалась изначально по версии 3.1.8) объявлен нежелательным для использования в новых проектах, но он все ещё поддерживается. Эти шаблоны приложения внешне весьма сходны, но в их реализации, тем не менее, есть весьма существенные отличия, рассмотрение которых заметно бы увеличило объем и так неслабо разросшейся статьи. Поэтому решено было рассмотреть в статье только реализацию шаблона Generic Host. Далее, ни работа веб-приложения, ни компоненты (Middleware), которые могут использоваться в его работе, в этой статье рассматриваться не будут по той же самой причине: необъятное объять нельзя.
Также очень кратко, только в объеме, необходимом для понимания процесса инициализации размещения (объекта, реализующего интерфейс IHost), будут рассмотрены инициализация и работа общесистемных компонентов .NET Core, таких как упомянутый выше контейнер сервисов (он же - контейнер внедрения зависимостей, DI Container), конфигурация (Configuration), параметры (Options).

И последнее, что нужно сказать во введении - про терминологию. Везде, где возможно, в статье использована русскоязычная терминология. Причем, по возможности - сделанная без помощи транслитерации, потому как шедевры транслитерации такие, как "континуация таски" (выкопано в одной прошлогодней статье на Хабре) весьма неприятно царапают мое эстетическое чувство. Однако, поскольку для немалого числа понятий в рассматриваемой области русскоязычная терминология, как минимум, не устоялась (а то и вовсе отсутствует), я, во-первых, допускаю, что использованный мной вариант может быть неудачным (или не самым удачным), а потому готов прислушиваться к комментариям об удачности терминов и о возможных альтернативах, а во-вторых, по той же причине отсутствия общепринятой русскоязычной терминологии, и чтобы в любом случае сохранить однозначность понимания, русскоязычные термины в этой статье будут дополняться их англоязычными эквивалентами - или терминами, или названиями классов - везде, где это уместно, по крайней мере - при первом их использовании. Ну, и некоторые понятия, вроде Middleware, для которых мне не удалось найти адекватный (в контексте их использования в ASP.NET) перевод, так и оставлены на языке оригинала.

На первый взгляд...

Итак, начнем с начала - с начала выполнения программы. Как известно, наверное, уже всем, выполнение программы ASP.NET начинается с определенного в файле program.cs класса Program, с его метода Main. И самый первый взгляд на этот файл, сгенерированный мастером создания нового проекта в Visual Studio, подсказывает нам, что приложение выполняется в две стадии. Сначала следует стадия настройки, производимая в шаблоне автоматически генерируемым отдельным методом CreateHostBuilder: создается вызовом статического метода CreateDefaultBuilder класса Microsoft.Extension.Hosting.Host экземпляр класса, реализующего интерфейс IHostBuilder (с добавленными к нему настройками по умолчанию), а затем с помощью методов IHostBuilder и многочисленных методов расширения IHostBuilder (специфических для различных компонентов приложения), указывается, какие компоненты, и с какими настройками будут использоваться. Отдельный метод CreateHostBuilder с этим именем нужен, как сказано в документации, чтобы средства разработки для ORM Entity Framework Core могли найти контекст подключения к БД (DbContext), с которым должно работать разрабатываемое приложение (в данной статье про это ничего не будет). Затем, уже в методе Main, вызовом метода IHostBuilder.Build создается объект, реализующий интерфейс IHost. И, наконец, приложение запускается на выполнение одним из методов интерфейса IHost или методов его расширения: в сгенерированном мастером файле используется метод расширения Run, запускающий приложение на выполнение и ожидающий его завершения, но есть и другие, альтернативные методы. Так все выглядит на первый взгляд - легко и просто.

Лирическое отступление: а как же теория?

Знатоки теории могут попытаться натянуть на описанную архитектуру какой-нибудь шаблон проектирования. Я, например, встречал мнения, что ASP.NET Core реализует шаблон "Построитель" (Builder pattern). Но, IMHO лучше забыть теорию, а рассматривать все так, как оно реализовано в натуре (именно об этом будет рассказано дальше, и в подробностях). А в теоретические рассуждения - типа, что Builder pattern плохо совместим с Dependency Injection (тот самй упомянутый выше модный современный подход), которое очень глубоко внедрено в код ASP.NET Core - я вдаваться не собираюсь. Но вернемся к нашим баранам.

Однако при более пристальном расмотрении все оказывается не так просто. Дело в том, что для конфигурирования веб-приложения в данном фреймворке нельзя просто взять и указать список использумых компонентов и их параметров и задать их настройки любым декларативным способом (константами/переменными/параметрами конфигурации),
а в приложение добавитьтолько тот код, который будет делать нечто специфичное именно для приложения. То тут, то там мы видим использование для конфигурирования компонентов фреймворка каких-то дополнительных блоков кода , передаваемых в виде делегатов (обычно - оформленных в виде лямбда-выражений) в вызовы каких-то методов.
Первый такой делегат встречает нас сразу же в сгенерированном мастером шаблоне простейшего веб-приложения: он используется в методе конфигурирования ConfigureWebHostDefaults для указания типа нашего класса инициализации веб-приложения, условно называемого Startup (этот класс, вообще-то, может иметь произвольное имя, поэтому дальше, чтобы не забывать этот факт, я буду называть его Startup-классом).
Т.е. в метод конфигурирования в качестве аргумента передается почему-то не просто тип Startup-класса, а некий блок кода, делегат, указывающий тип Startup-класса с помощью какого-то неочевидного соглашения: в виде лямбда-выражения, вызывающего для передаваемого в него параметра некий его обобщенный метод, специализированный типом нашего Startup-класса. А как это соглашение работает, и почему нельзя было передать в метод конфигурирования просто тип Startup-класса в качестве аргумента - это остается загадкой.

Лирическое отступление: о типах параметров лямбда-выражений.

Параметр ConfigureWebHost, при этом, как и положено правильно написанного в целях повышения сложности кода параметру лямбда-выражения, имеет неведомый нам тип. Впрочем, IDE(сразу) или документация (если ее все же прочесть) тут слегка помогут: они покажут, что тип этого параметра (он является интерфейсом) называется IWebHostBuilder. Но вот какой класс за этим интерфейсом прячется - все равно не покажут. Но я про то, что это за класс, дальше, во второй части статьи, обязательно расскажу - это очень интересный и нетривиальный класс.

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

В документации причины использование именно такого способа передачи конфигурационной информации не объяснены никак. То, что этот делегат где-то в процессе построения приложения вызывается и выполняется - это очевидно

На самом деле, нет

С появлением (ещё в незапамятные времена - в .NET Framework 3.5) поддержки деревьев выражений (Expression Trees) делегат может быть преобразован в такое дерево и использован внутри приложения каким-нибудь другим образом, кроме выполнения его кода. Но в коде конфигурирования и построения веб-приложений, который тут рассматривается, это, насколько я заметил, нигде не используется. Так что в этой статье можно смело писать "очевидно" ;-) .

Но где, в какой момент, и в каких условиях это происходит - документация нам про это не рассказывает. В основном, документация по этапу конфигурирования и построения приложения ASP.NET Core является сборником рецептов "как сделать", а не описанием "как это работает".

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

В результате типичное описание инициализации фреймворка превращается в некую форму декларативного программирования: для получения нужного приложения записывается набор пожеланий о том, что программист хочет получить на выходе. Правда, назвать это чисто декларативным программированием сложно: описание желаемого результата записывается на весьма необычном языке, включающем в себя вполне императивные конструкции внутри лямбда-выражений и даже полноценные методы (в Startup-классе, например). Но, в целом, описание по своей сути вполне декларативно: все эти императивно выглядящие конструкции типовой программист обычно тщательно переписывает из документации (или со StackOverflow и т.п.), изменяя в них, разве что, имена переменных. Получаются своего рода такие усложненные декларации. Я такой подход называю "заклинательным программированием": подобно магу, программист создает программу-заклинание, используя таинственные слова, точный смысл которых ему неведом, лишь вплетая в заклинание небольшие свои кусочки, чтобы добиться желаемого. И, подобно магу, программист не знает, как и почему заклинание будет выполнено - но уверен, что если не допущено ошибок, то оно обязательно сработает - и оно действительно ведь срабатывает! Подход этот древний (но, конечно, менее древний, чем магия): я видел людей, его использующих, ещё студентом, работая ещё на электронно-вычислительной машине (ЭВМ), а не на компьютере. У одного из таких людей (кстати, вполне неплохого прикладного программиста) была по этому поводу любимая присказка: "что бы такого ей (то есть ЭВМ) сказать". Причем, это явно были не первые люди, кто такой подход использовал. Лично я такой подход не люблю: мне всегда хочется не то, чтобы докопаться до первооснов, но, по крайней мере, иметь в голове модель того, что происходит. И это, кстати, послужило одной из причин, почему я стал разбираться в том материале, который вошел в эту статью.

Раскрываем тайны: этапы большого пути

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

Общая схема процесса инициализации схематически изображена на рисунке ниже:

Рис. 1. Схема процесса инициализации Generic HostРис. 1. Схема процесса инициализации Generic HostУсловные обозначения на рисунках

Рисунки-cхемы - эта, единственная в первой части, и ещё несколько во второй части - не являются формализованными диаграммами, а служат только для иллюстрации. Поэтому объекты на них изображены с некоторой степнью вольности, без строгой формализации. Но, тем не менее, специфичческий смысл у разных форм графических элементов присутствует. Прямоугольные элементы со сплошниыми границами обозначают блоки кода - как реализации методов классов, так и делегаты. Прямоугольныики со скругленными границами обозначают объекты. Элементы с штриховыми границами - элементы данных: свойства объектов и данные, передаваемые методам. Маленькие кружки на выносных линиях обозначают методы и свойства интерфейсов и классов. Сплошные линии со стрелками - направления, в которых идет выполнение кода. Штриховые линии со стрелками - передача данных в методы/свойства (а делегаты, передаваемые в методы в качестве данных, обозначены прямоугольниками в штриховых кругах). Штрих-пунктирные линии с пустотелыми стрелками на конце обозначают создание экземпляров объектов.

В изображенном на рисунке типичном процессе инициализации статический метод Host.CreateHostBuilder сначала создает объект построителя, реализующий интерфейс IHostBuilder. Затем для этого интерфейса в процессе конфигурирования вызываются (условные) присоединенные методы этого интерфейса для конфигурирования компонетов AddFeature1..AddFeatureN. В процессе конфигурирования присоединенные методы вызывают методы интерфейса IHostBuilder для регистрации делегатов, которые будут фактически выполнять конфигурирование при вызове метода Build. После окончания конфигурирования программа вызывает метод Build созданного объекта построителя, который выполняет конфигурирование. В результате этого вызова программа получает ссылку на интерфейс IHost объекта приложения (оно же - размещение, Host) и вызывает метод StartAsync этого интерфейса для запуска приложения.

Реализацией интерфейса IHostBuilder, которую создает метод CreateDefaultBuilder (статический, определен в классе Microsoft.Extensions.Hosting.Host), является класс Microsoft.Extensions.Hosting.HostBuilder(далее я буду называть его построителем, а чтобы избегать путаницы с переводом - параллельно использовать английское название интерфейса IHostBuilder). После создания построителя метод CreateDefaultBuilder добавляет в него ряд делегатов, создающих настройки по умолчанию (подробности см. в документации). Если же вам по какой-то причине эти настройки по умолчанию не нужны - вы имеете полное право создать объект построителя самостоятельно, с помощью оператора new, и конфигурировать его как угодно, что называется, "с чистого листа".

О значениях по умолчанию, фиксированных константах и т.п.

Все такие вещи довольно подробно описаны и в документации, и в многочисленных руководствах, копирующих куски из документации с небольшими изменениями. И, так как объем статьи ограничен, я не вижу смысла тратить его ещё и на эти, многократно описанные вещи, которые часто вполне можно опустить без ущерба для связности изложения. Поэтому по их поводу везде далее будет написано кратко: "см. документацию". Потому что моя цель - сообщить оригинальную информацию. Но вот там, где без этих сведений из документации обойтись нельзя - страдает либо понимание, либо связность изложения - там я, конечно, могу и сдублировать такие сведения.

Теперь рассмотрим метод Build. Просмотр исходного кода класса построителя показывает, что создание объекта класса, реализующего интерфейс IHost (далее я буду называть его словом "размещение") происходит в этом методе в несколько четко определенных этапов.

Отступление - о терминологии

В англоязычной документации этот объект называется Host, но это слово сложно адекватно перевести на русский язык. Используемый самой Microsoft перевод "узел" и смысл не раскрывает, и к путанице может привести - слово "node" тоже переводится как "узел", и его смысл такой перевод передает куда точнее. По-русски точнее всего по смыслу было бы перевести Host в контексте этой статьи примерно как "разместитель", но это слово явно "не звучит". Другой более-менее точный перевод - "размещение" - не передает тот факт, что этот объект играет активную роль, а не просто является вместилищем чего-то. Но за неимением лучшего буду использовать его.

Для использования на некоторых из этапов конфигурирования действий, задаваемых пользователем фреймворка - тех самых "загадочных" лямбда-выражений - в этом классе определены (и создаются в конструкторе) поля, содержащие списки действий, специфичных для этапа. Эти поля представляют собой экземпляры обобщенного класса List<>, специализированные нужным типом делегата,

детали реализации: списки делегатов

или (для одного из этапов) - интерфейса, реализованного вспомогательным классом, создаваемым на базе делегата (причина, почему так сделано, будет объяснена при описании этапа).

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

детали реализации: другие методы IHostBuilder

Для полноты описания методов IHostBuilder, но забегая немного вперед. Еще один метод интерфейса IHostBuilder, UseServiceProviderFactory (имеющий две перегруженных формы) используется для указания объекта-фабрики, создающего один из ключевых компонентов - контейнер сервисов приложения, реализующий интерфейс IServiceProvider. Об использовании объекта фабрики для его создания будет рассказано ниже при описании стадии создания этого компонента. Ну и, кроме того, в интерфейсе IHostBuilder определено (а в классе построителя, соответственно, реализовано) свойство-словарь Properties(типа IDictionary<object,object>), которое можно использовать для передачи произвольных значений между несколькими делегатами, конфигурирующими один и тот же компонент (в том числе - и делегатами, выполняющимися на разных этапах). Более того, содержимое этого свойства будет доступно и на этапе выполнения, где интерфейс IHostBuilder уже недоступен. А доступно оно будет через контейнер сервисов (например, путем внедрения зависимостей): ссылка на этот словарь будет помещена в объект HostBuilderContext, который будет зарегистрирован в контейнере сервисов как реализация сервиса для своего собственного типа - естественно (иначе не получится), с постоянным (Singleton) временем жизни.

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

Ключевые компоненты Generic Host

Теперь, прежде чем двигаться дальше, нужно вкратце рассмотреть те ключевые компоненты, которые создаются в процессе построения приложения на базе шаблона Generic Host

Первый рассматриваемый ключевой компонент ASP.NET Core (и .NET Core в целом) - это конфигурация, представляемая интерфейсом IConfiguration Конфигурация в .NET Core делает для приложения доступным набор строк-ключей, которым сопоставлены строки-значения, получаемых от различных поставщиков.

детали реализации: перечисление того, что умеет конфигурация

В качестве поставщиков конфигурации (все они представлены объектами, реализующими интерфейс IConfigurationProvider) .NET Core может использовать довольно разнообразные объекты: переменные среды (environment), параметры командной строки, файлы различных форматов... Имеется возможность использовать дополнительные поставщики конфигурации, создавая собственные реализации интерфейса IConfigurationProvider. Можно изменять значения ключей и добавлять новые ключи - однако эти изменения остаются только в пределах выполняющейся программы: существующие поставщики конфигурации на основе постоянных объектов (таких, как файлы) эти изменения в представляемых ими постоянных объектах не фиксируют. Конфигурация поддерживают возможность получения оповещений об изменениях в объектах, доступных через поставщики конфигурации (например, после редактирования файла конфигурации), повторной загрузки изменившейся части конфигурации и оповещения об изменении объектов, которые используют эту конфигурацию. Ключи конфигурации образуют иерархическое пространство имен - т.е. могут быть составными: состоять из нескольких компонентов разделенных знаком двоеточия-имен разделов, после которых находится имя значения. Есть возможность выделять ключи, принадлежащие одному разделу, в отдельные объекты разделов конфигурации, реализующие свою, ограниченную разделом, IConfiguration. Из объекта конфигурации (обычно - из одного из разделов) можно получать значения для экземпляров объектов: свойства объектов при этом заполняются на основе одноименных значений из конфигурации, преобразованных к нужному типу: это называется привязкой (Bind) конфигурации.

Предметом же данной статьи является процесс создания конфигурации. Он происходит следующим образом: сначала в специальный объект-построитель с интерфейсом IConfigurationBuilder добавляются его методами источники конфигурации - объекты с интерфейсом IConfigurationSource. А после указания всех нужных объектов-источников конфигурации методом Build построителя конфигурации производится создание конфигурации - объекта с интерфейсом IConfiguration. Процессы добавления источников конфигурации в объект-построитель конфигурации и создания из него конфигурации детально описаны ниже, при описании соответствующих этапов инициализации приложения.

Следующий по порядку создания, но, наверное, первый по важности ключевой компонент ASP.NET Core и .NET Core - это контейнер сервисов: объект реализующий интерфейс IServiceProvider. Именно этот объект в большинстве случаев предоставляет реализации тех самых интерфейсов, который обычно используются и в коде фреймворка, и в модулях, реализующих функциональность конкретного приложения. Сервисы, которые должен предоставлять контейнер сервисов - определяются типами интерфейсов или, реже, классов, которые требуются коду. Контейнер сервисов при обращении к нему за определенным сервисом (типом интерфейса или класса) путем вызова обобщенного метода GetService с указанием нужного параметра-типа, возвращает ссылку на объект запрошенного типа - реализующий этот интерфейс или являющийся объектом этого класса.

Исходным для создания контейнера сервисов является список регистраций сервисов с интерфейсом IServiceCollection, в который методами этого интерфейса добавляются объекты - описатели сервисов. Описатели сервисов содержат такую информацию, как тип сервиса (возвращаемого класса или интерфейса), способ реализации и время жизни сервиса. Процессы добавления описателей в список регистраций сервисов и создания из него контейнера сервисов детально описаны ниже, при описании соответствующих этапов инициализации приложения.

детали реализации: дополнительные полезные сведения о контейнере сервисов
  1. Список регистрации сервисов хранит описатели сервисов в объектах типа ServiceDescriptor. Возможно добавление в список регистраций сервисов заранее созданных объектов описателей сервисов, иногда это может быть полезным.

  2. Контейнер сервисов позволяет регистрировать в нем несколько вариантов одного и того же сервиса - с одним и тем же типом, но с разными способами реализации. Как именно в таких случаях будет вести себя контейнер сервисов - зависит от типа обращения к нему. Если запрашивается одиночный объект какого-либо сервиса, то контейнер сервисов создает и возвращает объект, реализующий сервис, только для последней регистрации в нем сервиса этого типа. Если запрашивается список сервисов некоторого типа - сервис типа IEnumerable с этим параметром-типом, то контейнер сервисов создает объекты для всех зарегистрированных реализаций сервиса того типа, который был указан в качестве параметра-типа IEnumerable и возвращает ссылку на IEnumerable, который перечисляет все эти объекты в том порядке, в котором эти реализации были зарегистрированы. Порядок регистрации сервисов в контейнере сервисов совпадает с тем порядком, в котором они были добавлены в список регистраций сервисов IServiceCollection

  3. При регистрации сервиса указывается его время жизни. По времени жизни сервисы разделяются на постоянные, реализации которых создаются в одном экземпляре (Singleton) и существующие в течение всего времени жизни приложения (пока существует контейнер сервисов), временные - реализации которых создаются каждый раз в момент обращения и существуют пока объект, реализующий сервис, используется в запросившем его коде (Transient), и сервисы со временем жизни ограниченной области (Scoped), которые должны запрашиваться не из основного (корневого) контейнера сервисов, а из производного от него контейнера сервисов ограниченной области, доступного через свойство ServiceProvider интерфейса IServiceScope создаваемого с помощью метода расширения CreateScope для интерфейса контейнера сервисов IServiceProvider. Объекты, реализующие сервисы со временем жизни ограниченной области существуют, пока существует соответствующая ограниченная область, а в рамках этой области существуют в единственном экземпляре.

  4. В качестве реализации сервиса может быть указан:

    а) класс: в этом случае контейнер сервисов для создания реализующего сервис объекта находит конструктор указанного класса, создает реализации для всех параметров этого конструктора, вызывает конструктор и возвращает ссылку на созданный таким образом объект;

    б) делегат-фабрика: в этом случае в этом случае контейнер сервисов для создания реализующего сервис объекта вызывает этот делегат, передавая в него в качестве аргумента ссылку на свой интерфейс IServiceProvider; делегат сам отвечает за создание нужного объекта и всех необходимых для этого объектов (для этого может использоваться переданная в качестве параметра ссылка на контейнер сервисов); контейнер сервисов возвращает ссылку на созданный делегатом-фабрикой объект;

    в) экземпляр класса нужного типа (только для сервисов с постоянным временем жизни): в этом случае контейнер сервисов возвращает ссылку на этот объект.

Третий ключевой компонент ASP.NET Core и .NET Core инициализуемый в процессе создания приложения (а также - используемый внутри самого процесса инициализации) - это механизм параметров (Options). Параметры, передаваемые данным механизмом, представляют собой сервисы, позволяющие получать в приложении значения заранее определенного типа, причем на стадии инициализации (конфигурирования) приложения задается не само значение, а способ его получения, его источник. То есть, во-первых, параметры передаются в программу на стадии выполнения не как объекты данных, а как сервисы, реализованные в виде интерфейсов .NET, и реализация механизма параметров опирается на использование контейнера сервисов: сервисы для работы с параметрами, так же как и любые другие сервисы, регистрируются в списке регистраций сервисов, а затем, после создания контейнера сервисов на основе этого списка регистраций, становятся доступными в приложении. Для получения самих же передаваемых значений необходимо вызвать соответствующие свойства или методы этих сервисов (для доступа к параметрам есть три разных типа сервисов, отличающихся способами их использования - IOptions<>, IOptionsSnapshot<>, и IOptionsMonitor<>, их описание я здесь приводить не буду). Во-вторых, значения параметров являются строго статически типизированными, их типы задаются на стадии написания программы, и указываются как параметры-типы для обобщенных типов сервисов получения значений параметров(options). В-третьих, на стадии конфигурирования задаются не сами значения параметров, а способы получения их значений. Эти способы задаются путем добавления в контейнер сервисов специальных сервисов конфигурирования значений. Регистрация сервисов, реализующих механизм параметров (options), в принципе, может быть произведена обычными методами регистрации сервисов. Но для удобства конфигурирования этого механизма созданы специальные методы расширения для интерфейса списка регистрации сервисов IServiceCollection, и чаще всего используются именно они. Этими методами можно указать, что либо источником значения сервиса служит объект конфигурации, реализующий IConfiguration (обычно - раздел конфигурации), либо произвольный код, записываемый в виде одной или нескольких процедур-делегатов, устанавливающих это значение. Более подробное рассмотрение механизма параметров заслуживает отдельной статьи, поэтому здесь его не будет.

Рассмотрим теперь в подробностях процесс создания приложения, точнее - его объекта размещения (или хоста), реализующего интерфейс IHost Как уже было сказано, это производится методом Build интерфейса IHostBuilder. А поскольку статья рассматривает работу стандартной реализации шаблона приложения Generic Host построителя Microsoft.Extensions.Hosting.HostBuilder (или просто HostBuilder) здесь рассматривается работа метода Build этого конкретного класса.

Создание конфигурации приложения

Вот теперь можно вернуться к дальнешему изучению метода Build. Сначала в методе Build создается конфигурация приложения.

детали реализации: метод Build

Но в самом что ни на есть начале метод HostBuilder.Build проверяет, что он не был запущен повторно. В объекте класса HostBuilder для этого есть специальное булево поле _hostBuilt, которое после создания объекта имеет значение по умолчанию false. Код в начале метода проверяет, установлено ли это поле в true, и если так - выбрасывает исключение InvalidOperationException. А сразу после этой проверки поле _hostBuilt устанавливается в true.

Для создания конфигурации приложения используются два отдельных этапа, с некоторым количеством промежуточных этапов между ними, на которых создаются другие структуры данных. Это связано с тем, что для нахождения части источников конфигурации приложения - таких, как файлы конфигурации - нужно знать некую дополнительную информацию - такую, как местонахождение корневого каталога приложения - которая обычно задается по умолчанию, но может, вообще говоря, и переопределяться в конфигурации, только в другой ее части. Поэтому сначала выполняется стадия, на которой создается конфигурация размещения (Host Configuration, чаще я буду назвать ее конфигурации построителя - по месту ее использования). Это - та часть конфигурации, которая не зависит от контекста построения (о нем немного ниже), в который входит, в частности, упомянутый путь к корневому каталогу приложения. Что именно входит в конфигурацию построителя по умолчанию - см. документацию. Очередь делегатов, выполняемых на этой стадии, создается методом ConfigureHostConfiguration интерфейса IHostBuilder и хранится во внутреннем поле _configureHostConfigActions. На рис.1 она обозначена "корзиной" под номером 1.

детали реализации: конфигурация построителя

Метод ConfigureHostConfiguration принимает единственный параметр-делегат, который тоже имеет единственный параметр - ссылку на объект построителя конфигурации IConfigurationBuilder.

Создание конфигурации построителя производится внутренним методом BuildHostConfiguration().

детали реализации: создание конфигурации построителя

Этот метод создает объект-построитель конфигурации класса ConfigurationBuider(реализующий IConfigurationBuilder) - локальную переменную configBuilder(на рис.1 она представлена штриховым прямоугольником) - и добавляет в него источник конфигурации, создающий провайдер хранилища конфигурации в памяти (изначально пустого) - чтобы делать возможным установку параметров конфигурации (через свойство по умолчанию интерфейса IConfiguration) даже в случае, если для конфигурации не будет больше определен никакой другой источник. После этого вызываются все делегаты из очереди построения конфигурации размещения, которые добавляют в созданный объект с интерфейсом IConfigurationBuilder свои источники конфигурации (но могут, конечно, делать и другие действия). И, наконец, из этих источников создается (методом IConfigurationBuilder.Build()) конфигурация размещения (она же - конфигурация построителя).

Созданная конфигурация построителя запоминается в поле _hostConfiguration построителя.

После этого наступают этапы создания структур данных - объектов, содержащих информацию о контексте, в котором производится построение приложения. Первый такой объект - это объект окружения размещения (хоста), реализующий интерфейс IHostEnvironment. Его создание производится внутренним методом CreateHostingEnvironment(). Данный метод создает объект внутреннего для сборки типа HostingEnvironment, реализующий интерфейсы IHostEnvironment и IHostingEnvironment (устаревший аналог IHostEnvironment). При этом при создании объекта его свойства устанавливаются на основе значений ключей конфигурации размещения с фиксированными названиями (подробности - см. документацию). В число этих свойств входят имя приложения (ApplicationName), название среды выполнения(Environment) и путь к корневому каталогу приложения (ContentRootPath).
Если нужных ключей в конфигурации размещения нет - эти свойства устанавливаются в значения по умолчанию (опять-таки, см. документацию). И, наконец, в свойство ContentRootFileProvider созданного объекта окружения записывается вновь созданный экземпляр класса PhysicalFileProvider для пути ContentRootPath - то есть, в качестве средства доступа к файлам приложения используется (изначально и по умолчанию) обычная файловая система. Созданный объект окружения размещения запоминается в поле _hostingEnvironment объекта построителя.

Другой объект, содержащий информацию о контексте - это объект контекста построения (экземпляр класса HostBuilderContext) Он создается на следующем этапе внутренним методом CreateHostBuilderContext. В его свойствах запоминаются ссылки на другие объекты, связанные с построителем: в Properties - ссылка на одноименное свойство построителя - словарь построителя (его описание см. выше под спойлером "детали реализации: другие методы IHostBuilder"), в Environment - на только что созданный объект окружения размещения (IHostEnvironment), в Configuration - (временно) конфигурация размещения (потом она будет заменена на конфигурацию приложения). Ссылка на объект контекста построения запоминается в поле _hostBuilderContext. И вот теперь все готово для окончательного создания полной конфигурации приложения, включающей в себя все источники, и это становится следующей стадией.

Стадия окончательного создания конфигурации приложения производится внутренним методом BuildAppConfiguration. Очередь делегатов, выполняемых на этой стадии, создается методом ConfigureAppConfiguration интерфейса IHostBuilder и хранится во внутреннем поле _configureAppConfigActions. На рис.1 она обозначена "корзиной" под номером 2.

детали реализации: конфигурация приложения

Метод ConfigureAppConfiguration принимает единственный параметр-делегат, который имеет уже два параметра - ссылку на контекст построения HostBuilderContext и ссылку на объект построителя конфигурации IHostBuilder. Данный метод сначала создает построитель конфигурации - объект класса ConfigurationBuider(реализующий IConfigurationBuilder) и устанавливает для него базовым каталогом корневой каталог приложения IHostEnvironment.ContentRootPath . установка базового каталога построителя конфигурации IConfigurationBuilder производится его методом расширения SetBasePath. Этот метод записывает в словарь построителя конфигурации под ключом "FileProvider" ссылку на свежесозданный объект PhysicalPathProvider с указанным базовым каталогом. Этот провайдер затем будет использоваться в качестве файлового провайдера IFileProvider по умолчанию во всех классах-источниках конфигурации файловых провайдеров (классов-наследников FileConfigurationSource) Непонятно, однако, зачем нужно было создавать два одинаковых по смыслу объекта провайдера - здесь и для окружения размещения - работающих с одним и тем же каталогом, причем - создавать их немного разным образом.

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

детали реализации: добавление конфигурации построителя к конфигурации приложения

Делается это добавлением в список источников класса ChainedConfigurationSource, содержащий ссылку на объект конфигурации построителя, причем в конструкторе ChainedConfigurationSource указывается флаг, что за освобождение (вызов метода Dispose) этого объекта конфигурации должен будет отвечать созданный из этого источника экземпляр класс провайдера ChainedConfigurationProvider, который также будет содержать ссылку на этот объект конфигурации.

Затем построение конфигурации приложения завершается аналогично построению конфигурации построителя.

детали реализации: завершение построения конфигурации приложения

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

Полученная конфигурация (интерфейс IConfiguration) сохраняется в поле _appConfiguration объекта построителя.

Последнее действие, которое производит метод IHostBuilder.Build построителя - это создание контейнера сервисов. Выполняет его внутренний метод CreateServiceProvider. Это действие достаточно сложное, оно включает в себя несколько стадий, на двух из которых применяются две разных очереди делегатов конфигурирования.

Создание контейнера сервисов

Для начала - немного о том, как выглядит процесс создания контейнера сервисов. Выполняется этот вызовами двумя последовательными вызовами методов интерфейса фабрики контейнера сервисов IServiceProviderFactory. Этот интерфейс, как мы видим, является обобщенным, с параметром-типом, который является типом промежуточного объекта, создаваемого первым методом, и принимаемого вторым. Точный смысл этого параметра-типа я, честно говоря, так до конца и не понял, т.к. ни в документации, ни в изученных мной исходных текстах примеров его нетривиального использования не встречается: фабрика контейнера сервисов по умолчанию (о ней см. ниже) использует тривиальный вариант, в котором этот тип совпадает с типом списка регистраций сервисов IServiceCollection и никакого дополнительного конфигурирования с использованием этого типа не производится. В документации и в коде его в разных местах называют по-разному - то ContainerBuilder(как вышеприведенный параметр-тип, только без префикса T), то просто Container. В тексте я буду называть его "контейнер-построитель". Судя по всему, он используется где-то для конфигурирования процесса подключения контейнера сервисов стороннего типа: в .NET Core есть возможность использовать другие контейнеры сервисов, кроме реализации по умолчанию, и несколько из них официально поддерживаются, однако я со сторонними контейнерами дела не имел, а потому достоверно утверждать об этом не могу. В самой же ASP.NET Core используется только упомянутый уже тривиальный вариант по умолчанию.

Вернемся, однако, к интерфейсу фабрики контейнера сервисов IServiceProviderFactory. Первый метод, CreateBuilder, принимает в качестве аргумента список регистраций сервисов IServiceCollection services) и возвращает объект контейнера-построителя, который (я тут забегаю вперед) может быть подвергнут дополнительному конфигурированию. После этого второй метод интерфейса фабрики CreateServiceProvider принимает аргумент - контейнер-построитель и возвращает созданный на его основе контейнер сервисов (интерфейс IServiceProvider).

Для установки используемой при построения контейнера сервисов фабрики в процессе построения размещения (IHost) в интерфейсе построителя IHostBuilder существует метод UseServiceProviderFactory (его параметр тип - это тип контейнера-построителя), имеющий две перегруженных формы: первая принимает в качестве параметра ссылку на интерфейс фабрики заранее созданного объекта, вторая - делегат, принимающий в качестве параметра объект контекста построения HostBuilderContext и возвращающий ссылку на интерфейс фабрики контейнера сервисов, реализуемый объектом, который выбирается или создается на основе содержимого контекста построения на этапе создания контейнера сервисов. Ссылка на объект фабрики контейнера сервисов, которая будет использована, сохраняется во внутреннем поле построителя (на рис.1 оно обозначено штриховым кругом справа).

детали реализации: хранение фабрики контейнера сервисов

В объекте построителя, однако, для хранения используется поле _serviceProviderFactory, имеющее тип адаптера фабрики контейнера сервисов - внутреннего интерфейса IConfigureContainerAdapter. Этот интерфейс является необобщенным - по причине того, что класс HostBuilder, реализующий построитель, также является необобщенным и не имеет параметра-типа, соответствующего типу контейнера-построителя - а потому этот тип нельзя использовать в качестве типа поля этого класса. Методы интерфейса адаптера фабрики IConfigureContainerAdapter в целом аналогичны методам самой фабрики IServiceProviderFactory, но с некоторыми различиями. Первое различие - в том, что метод CreateBuilder имеет дополнительный параметр - контекст построителя HostBuilderContext: он нужен для использования фабрики контейнера сервисов, получаемой от делегата, переданного через вторую форму метода UseServiceProviderFactory. Второе различие - в том, что методы IConfigureContainerAdapter вместо недоступного им типа контейнера-построителя используют универсальный тип Object. Реализуется этот интерфейс, однако, обобщенным внутренним классом ConfigureContainerAdapter, поэтому некоторый статический контроль (или вывод) типов все-таки производится - за счет конструктора этого класса в методе UseServiceProviderFactory, а потому объект, передаваемый во второй метод, CreateServiceProvider имеет правильный тип. Но вот соответствие типа контейнера-построителя, указанного при вызове метода UseServiceProviderFactory сигнатурам делегатов, помещенных в очередь конфигурирования контейнера-построителя (см. ниже) контролируется только динамически при их приведении к нужному для делегата типу (Из-за чего возможно возникновение исключения).

По умолчанию построитель использует реализацию фабрики контейнера сервисов на основе класса DefaultServiceProviderFactory. Этот класс в качестве типа контейнера-построителя использует тип списка регистраций сервисов IServiceCollection, т.е. реализует интерфейс IServiceProviderFactory. Некоторые свойства создаваемого контейнера сервисов можно задать с помощью параметра его конструктора - и это используется в одном из методов инициализации построителя веб-приложения (об этом - позже, во второй части).

детали реализации: о параметре конструктора

Конструктор может принимать параметр класса ServiceProviderOptions - параметры (options) контейнера сервисов. Этот класс имеет два публичных свойства-флага: ValidateScopes - проверять, не производится ли разрешение сервиса со временем жизни ограниченной области (Scoped) из корневого контейнера сервисов (используемого вне ограниченных областей) и ValidateOnBuild - при создании контейнера сервисов выполнить проверку, что можно создать все зарегистрированные в нем сервисы. По умолчанию параметр конструктора установлен в значение ServiceProviderOptions.Default, в котором оба эти флага сброшены. Параметр, переданный в конструктор, сохраняется во внутреннем поле.

Метод CreateBuilder класса DefaultServiceProviderFactory, реализующего фабрику контейнера сервисов по умолчанию, просто возвращает переданный в него список регистраций сервисов. Метод CreateServiceProvider этого класса использует метод расширения BuildServiceProvider для интерфейса IServiceCollection.

детали реализации: метод CreateServiceProvider

Он возвращает значение, получаемое от BuildServiceProvider, вызываемого для ссылки на интерфейс IServiceCollection, переданной в CreateServiceProvider как параметр - "контейнер-построитель". При вызове в качестве аргумента используется сохраненное значение в конструкторе значение ServiceProviderOptions - параметры построения контейнера сервисов.

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

Вернемся к процессу создания контейнера сервисов внутренним методом CreateServiceProvider построителя, и рассмотрим подробнее стадии этого процесса. На первой стадии создается объект списка регистраций сервисов с интерфейсом IServiceCollection - он записывается в локальную переменную services(на рис.1 она представлена штриховым прямоугольником). А потом в этот список добавляются (регистрируются) описатели сервисов, реализующих базовые сервисы фреймворка.

детали реализации: список регистраций сервисов

Реальный класс объекта, реализующего список регистраций сервисов - внутренний класс ServiceCollection. Устроен этот класс весьма прямолинейно: в нем есть внутреннее поле типа List (содержащийся в нем объект создается в конструкторе пустым), и все методы интерфейса IServiceCollection напрямую отображаются на соответствующие методы этого объекта. Базовые сервисы фреймворка - это следующие сервисы (все они регистрируются как сервисы с постоянным (Singleton)временем жизни). Во-первых - сервисы, реализацией которых являются экземпляры объектов, ранее созданных в процессе работы метода Build: IHostEnvironment и его устаревший аналог IHostingEnvironment (реализация - экземпляр объекта типа HostingEnvironment из поля _hostingEnvironment) и HostBuilderContext (регистрируется в качестве сервиса не интерфейс, а именно класс) (реализация - объект этого класса из поля _hostBuilderContext. Во-вторых, сервисы, реализацией являются классы, реализующие соответствующий интерфейс. Это - сервисы, которые связаны с запуском, остановом, и отслеживанием этапов работы приложения (в данной статье они подробно не рассматриваются): IHostLifetime (реализация - класс ConsoleLifetime), IHostApplicationLifetime (реализация - класс ApplicationLifetime) и его устаревший аналог IApplicationLifetime (последний реализуется с помощью фабрики, получающей реализацию IHostApplicationLifetime - ссылку на объект класса ApplicationLifetime - и возвращающий этот результат, преобразованный к типу IApplicationLifetime) Сервис для интерфейса IConfiguration регистрируется в виде фабрики - лямбда-функции, возвращающей конфигурацию приложения - значение поля _appConfiguration класса построителя (это значение сохраняется в замыкании этой лямбда-функции). Мотив сделать именно так - вызвать метод Dispose для конфигурации приложения при уничтожении контейнера сервисов. Но для этого требуется ещё одно дополнительное действие - фиктивное получение ссылки на этот сервис (см. спойлер в конце описания работы внутреннего метода CreateServiceProvider построителя). Кроме того, методами расширения для интерфейса IServiceCollection регистрируются группы сервисов, реализующие компоненты параметров (options) - методом AddOptions, и регистрации (logging) - методом AddLogging

Кроме этих интерфейсов регистрируется (с постоянным (Singleton) временем жизни основной интерфейс приложения, построенного на шаблоне Generic Host - IHost: он реализуется внутренним классом Internal.Host, его мы затронем немного позднее.

На второй стадии создания контейнера сервисов - стадии конфигурирования списка регистраций сервисов - к созданному списку регистраций сервисов IServiceCollection применяются делегаты из очереди конфигурирования списка регистраций сервисов _configureServicesActions. На рис.1 она обозначена "корзиной" под номером 3. Для добавления делегатов в эту очередь служит метод ConfigureServices. Делегат, добавляемый в эту очередь должен принимать два параметра: контекст построения HostBuilderContext и ссылку на конфигурируемый список регистраций сервисов IServiceCollection.

На третьей стадии создания контейнера сервисов с помощью фабрики сервисов создается промежуточный объект контейнера-построителя - он записывается в локальную переменную containerBuilder (на рис.1 она представлена штриховым прямоугольником).

детали реализации: создание контейнера-построителя

Это делается вызовом метода CreateBuilder адаптера фабрики контейнера сервисов IConfigureContainerAdapter, ссылка на который хранится в поле _serviceProviderFactory Ссылка на созданный контейнер-построитель запоминается в локальной переменной containerBuilder

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

На четвертой стадии создания контейнера сервисов - стадии конфигурирования контейнера-построителя - к контейнеру-построителю применяются элементы очереди конфигурирования контейнера-построителя _configureContainerActions. На рис.1 она обозначена "корзиной" под номером 4. Для добавления элементов в эту очередь служит метод ConfigureContainer. В рассматриваемом нами случае "чистой" (без сторонних контейнеров сервисов) ASP.NET Core этот этап, похоже, не используется. Об этом, например, говорит тот факт, что в документации даже не отражена возможность использования Startup-класса на этом этапе конфигурирования, хотя поддержка метода, производящего такое конфигурирование, в коде реализована (об этом будет рассказано подробно при рассмотрении реализации метода расширения UseStartup интерфейса IWebHostBuilder).

Поэтому сведения о реализации этого этапа целиком убраны под спойлер (и там - детали реализации и лирическое отступление)

ConfigureContainer - это обобщенный метод, имеющий один параметр-тип, совпадающий с типом контейнера-построителя. И этот параметр-тип входит в сигнатуру параметров-делегатов, которые в него передаются: делегаты должны принимать два параметра - контекст построения типа HostBuilderContext и ссылку на контейнер-построитель, тип которой - это тип контейнера-построителя, то есть - значение параметра-типа метода ConfigureContainer. А так, как значение типа контейнера-построителя на уровне всего класса построителя на этапе компиляции не известно, то элементами очереди являются не сами делегаты (потому что их реальный тип не известен и не может быть использован для специализации экземпляра класса List<>, используемого в качестве очереди), а ссылки на необобщенный внутренний интерфейс IConfigureContainerAdapter, реализуемый внутренним объектом обобщенного типа ConfigureContainerAdapter, специализированным типом контейнера-построителя, использованным указанным в сигнатуре делегата (то есть - типом второго параметра делегата). Этот делегат передается в качестве параметра в конструктор этого объекта и запоминается в его внутреннем поле. Метод ConfigureContainer этого интерфейса по сигнатуре аналогичен инкапсулированному делегату, но принимает в качестве типа контейнера-построителя универсальный тип Object. А реализация этого метода в классе ConfigureContainerAdapter состоит в приведении полученной ссылки на контейнер-построитель к типу второго параметра запомненного делегата и вызове этого делегата. Такое решение: палка о двух концах: с одной стороны, это позволяет довольно просто поместить все делегаты конфигурирования контейнера-построителя в одну очередь, но с другой - препятствует статической проверке соответствия типов параметров делегатов типу контейнера-построителя. А это несоответствие может привести к возникновению исключения недопустимого преобразования типов. Причем, поскольку в реализации метода ConfigureContainer в типе ConfigureContainerAdapter аргумент ссылки на контейнер-построитель просто безо всяких проверок приводится к типу, принимаемому делегатом, то исключение возникнет в месте, не контролируемом разработчиком, и приводит к прерыванию выполнения метода Build построителя. Разработчик может бороться с этим, разве что, используя делегаты с универсальным типом Object в качестве типа контейнера-построителя и проверяя тип контейнера-построителя внутри делегата. Но IMHO это - так себе решение.

На следующей, пятой стадии из контейнера-построителя создается объект контейнера сервисов.

детали реализации: создание контейнера сервисов из контейнера-построителя

Точнее - корневого контейнера сервисов: попытка получения из него сервиса со временем жизни ограниченной области (Scoped) считается ошибкой, потому что полученный сервис реально будет иметь постоянное время жизни, эквивалентное Singleton - а это, вероятно, не то, что ожидал разработчик, указывая время жизни ограниченной области (Scoped). В некоторых режимах - например, в режиме разработки (окружении Development) - такие действия реально проверяются и, в случае их обнаружения выбрасывается исключение InvalidOperationException. За производство такой проверки отвечает флаг ValidateScopes параметра типа ServiceProviderOptions, передаваемого при использовании фабрики контейнера сервисов по умолчанию DefaultServiceProviderFactory, в метод расширения BuildServiceProvider для интерфейса IServiceCollection, который производит создание контейнера сервисов. Само создание делается вызовом метода CreateBuilder адаптера фабрики контейнера сервисов IConfigureContainerAdapter, ссылка на который хранится в поле _serviceProviderFactory, а ссылка на созданный контейнер-построитель запоминается в локальной переменной containerBuilder

На этом процесс создания контейнера сервисов внутренним методом CreateServiceProvider построителя заканчивается.

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

Точнее - почти заканчивается: дополнительно производится еще фиктивный запрос сервиса IConfiguration - чтобы сервис был помечен как запрошенный, это нужно чтобы реализующий его объект _appConfiguration был вовремя освобожден (вызовом Dispose()) контейнером сервисов (подробности в этой статье не рассматриваются - они явно выходят за пределы рассматриваемой темы)

И последнее, что делает метод Build класса построителя HostBuilder - это получает из контейнера сервисов реализацию интерфейса IHost (объект класса Internal.Host), которую возвращает в качестве результата.

Лирическое отступление: О торжестве подхода внедрения зависимостей

На поверхности все выглядит так, что для получения реализации интерфейса IHost метод Build просто запрашивает его у контейнера сервисов. А уж контейнер сервисов сам находит нужный класс Internal.Host, отыскивает его конструктор, который имеет немало параметров-зависимостей, разрешает внутри себя эти зависимости, создает экземпляр этого класса, указав все нужные параметры и, наконец, возвращает ссылку на запрошенный интерфейс, реализованный этим экземпляром. Это выглядит, вроде бы, как существенное упрощение, показывающее преимущества внедрения зависимостей. Но это - только видимость: объекты почти для всех этих параметров-интерфейсов были созданы в том же самом методе Build, так что передать их в конструктор класса более традиционным путем не составило бы никакого труда. Единственный параметр Internal.Host, который не создается кодом внутри HostBuilder.Build таким путем - сервис, реализующий параметр (option) для типа HostOptions: его значение задается одним из методов конфигурирования, применяемым разработчиком конкретного приложения. Но фактически единственный параметр, который передается таким образом - таймаут завершения - мог бы быть куда проще передан традиционным образом - через параметр типа Timespan. Так что процесс получения IHost в HostBuilder.Build сам по себе демонстрирует лишь саму технику использования внедрения зависимостей, но никак не ее преимущества.

Что происходит потом

Последний этап инициализации приложения, компоненты которого размещены в полученном размещении IHost - этап, специфичный для каждого из компонентов. Он происходит в процессе запуска приложения, который производится методом IHost.StartAsync - прямо или косвенно, через методы расширения этого интерфейса, которые внутри себя вызывают StartAsync. Метод StartAsync стандартной реализации IHost - класса Internal.Host - запускает (уже асинхронно) каждый из размещенных компонентов приложения методом IHostedService.StartAsync: все компоненты реализуют интерфейс IHostedService. С помощью этого метода компоненты выполняют специфичную для них инициализацию. Об инициализации, выполняемым компонентом веб-приложения будет подробно рассказано во второй части статьи. Кроме того, метод StartAsync стандартной реализации IHost производит еще ряд действий, которые в данной статье не рассматриваются.

И на этом рассказ об инициализации приложения, построенного по шаблону Generic Host можно считать законченным - теперь приложение запущено и выполняется. Не правда ли, все на самом деле очень просто? Не торопитесь, в продолжении этой статьи последует рассказ об особенностях инициализации веб-приложения - и ощущение простоты, если таковое и было, обязательно развеется.

Продолжение: будет скоро опубликовано. Оно уже написано (и даже выложенно в мой блог, но в не до конца причесанном виде).

Подробнее..
Категории: C , Net , Asp.net core , Internals

Как уменьшить размер приложения на C, которое независимо от среды?

05.06.2021 00:21:43 | Автор: admin

В этой статье поделюсь опытом, как уменьшить размер приложения, написанное на C# и независящее от сборки, в 2 4 раза.

Внимание: Сжатие содержимого программы доступно только для self-contained публикаций. А также все действия происходят в Visual Studio Preview 2019.

Если вы здесь за быстрым решение, то вот что вам нужно сделать

В .csproject добавьте следующие строки:

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

<PublishSingleFile>true</PublishSingleFile><SelfContained>true</SelfContained><RuntimeIdentifier>win-x64</RuntimeIdentifier><PublishTrimmed>true</PublishTrimmed><TrimMode>Link</TrimMode>

Более безопасный режим: удаляет только неиспользуемые сборки.

<PublishSingleFile>true</PublishSingleFile><SelfContained>true</SelfContained><RuntimeIdentifier>win-x64</RuntimeIdentifier><PublishTrimmed>true</PublishTrimmed><TrimMode>CopyUsed</TrimMode>

Затем нажмите ПКМ по проекту Publish Folder Финиш Show All Settings. Выставите следующие настройки:

  • Deployment Mode: Self-Contained

  • Target Runtime: win-x64 или свою версию. (Должна совпадать со строчкой RuntimeIdentifier)

Разверните File publish options и поставьте галочки под: Produce single file и Trim unused asseblies.

Нажмите кнопку Publish.


Всё то же самое, только командой

Опасный режим:

dotnet publish -c Release -r win10-x64 -p:PublishTrimmed=True -p:TrimMode=Link -p:PublishSingleFile=true --self-contained true

Более безопасный режим:

dotnet publish -c Release -r win10-x64 -p:PublishTrimmed=True -p:TrimMode=CopyUsed -p:PublishSingleFile=true --self-contained true

Более подробно о том, что происходит за настройками выше

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

Команда PublishTrimmed активирует обрезку сборки.

Команда TrimMode выбирает способ обрезки сборки. Здесь и происходит вся магия по сокращению размера итогового файла.

Всего имеется 2 режима: CopyUsed (Assembly-level trimming) и Link (Member-Level Trimming).

Assembly-level trimming Просто удаляет неиспользуемые сборки. То есть алгоритм просто проходится по всем файлам программы, составляет список сборок, а затем удаляет из итоговой сборки все файлы, которые не используется. Этот метод мне помог сократить размер программы с 300 МБ до 96 МБ. При ZIP архивации этот файл стал 30МБ.

Member-Level Trimming Экспериментальный режим. Алгоритм анализирует ваш код и удаляет все ненужные классы, методы и т.д. Из-за того, что алгоритм влезает в код, существует большой риск, что приложение перестанет работать корректно, поэтому требует после публикации обширных тестов всех функций приложения. В моём случае, этот режим сократил размер программ с 300МБ до 86МБ, но при этом приложение перестало запускать и подавать какие-либо признаки жизни. Отладки тоже не поддалось, к сожалению.

Более подробно можете почитать в этой статье

Подробнее..

Как я сделал Discord бота для игровой гильдии с помощью .NET Core

05.06.2021 18:10:05 | Автор: admin
Батрак предупреждает о том что к гильдии присоединился игрокБатрак предупреждает о том что к гильдии присоединился игрок

Вступление

Всем привет! Недавно я написал Discord бота для World of Warcraft гильдии. Он регулярно забирает данные об игроках с серверов игры и пишет сообщения в Discord о том что к гильдии присоединился новый игрок или о том что гильдию покинул старый игрок. Между собой мы прозвали этого бота Батрак.

В этой статье я решил поделиться опытом и рассказать как сделать такой проект. По сути мы будем реализовывать микросервис на .NET Core: напишем логику, проведем интеграцию с api сторонних сервисов, покроем тестами, упакуем в Docker и разместим в Heroku. Кроме этого я покажу как реализовать continuous integration с помощью Github Actions.

От вас не потребуется никаких знаний об игре. Я написал материал так чтобы можно было абстрагироваться от игры и сделал заглушку для данных об игроках. Но если у вас есть учетная запись в Battle.net, то вы сможете получать реальные данные.

Для понимания материала, от вас ожидается хотя бы минимальный опыт создания веб сервисов с помощью фреймворка ASP.NET и небольшой опыт работы с Docker.

План

На каждом шаге будем постепенно наращивать функционал.

  1. Создадим новый web api проект с одним контроллером /check. При обращении к этому адресу будем отправлять строку Hello! в Discord чат.

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

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

  4. Напишем Dockerfile для нашего проекта и разместим проект на хостинге Heroku.

  5. Посмотрим на несколько способов сделать периодическое выполнение кода.

  6. Реализуем автоматическую сборку, запуск тестов и публикацию проекта после каждого коммита в master

Шаг 1. Отправляем сообщение в Discord

Нам потребуется создать новый ASP.NET Core Web API проект.

Создание нового проекта - это одна из фундаментальных вещей которые я не буду подробно расписывать. При работе над проектом используйте сервис Github для хранения кода. В дальнейшем мы воспользуемся несколькими возможностями Github.

Добавим к проекту новый контроллер

[ApiController]public class GuildController : ControllerBase{    [HttpGet("/check")]    public async Task<IActionResult> Check(CancellationToken ct)    {        return Ok();    }}

Затем нам понадобится webhook от вашего Discord сервера. Webhook - это механизм отправки событий. В данном случае, то это адрес к которому можно слать простые http запросы с сообщениями внутри.

Получить его можно в пункте integrations в настройках любого текстового канала вашего Discord сервера.

Создание webhookСоздание webhook

Добавим webhook в appsettings.json нашего проекта. Позже мы унесем его в переменные окружения Heroku. Если вы не знакомы с тем как работать с конфигурацией в ASP Core проектах предварительно изучите эту тему.

{"DiscordWebhook":"https://discord.com/api/webhooks/****/***"}

Теперь создадим новый сервис DiscordBroker, который умеет отправлять сообщения в Discord. Создайте папку Services и поместите туда новый класс, эта папка нам еще пригодится.

По сути этот новый сервис делает post запрос по адресу из webhook и содержит сообщение в теле запроса.

public class DiscordBroker : IDiscordBroker{    private readonly string _webhook;    private readonly HttpClient _client;    public DiscordBroker(IHttpClientFactory clientFactory, IConfiguration configuration)    {        _client = clientFactory.CreateClient();        _webhook = configuration["DiscordWebhook"];    }    public async Task SendMessage(string message, CancellationToken ct)    {        var request = new HttpRequestMessage        {            Method = HttpMethod.Post,            RequestUri = new Uri(_webhook),            Content = new FormUrlEncodedContent(new[] {new KeyValuePair<string, string>("content", message)})        };        await _client.SendAsync(request, ct);    }}

Как видите, мы используем внедрение зависимостей. IConfiguration позволит нам достать webhook из конфигов, а IHttpClientFactory создать новый HttpClient.

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

Не забудьте что новый класс нужно будет зарегистрировать в Startup.

services.AddScoped<IDiscordBroker, DiscordBroker>();

А также нужно будет зарегистрировать HttpClient, для работы IHttpClientFactory.

services.AddHttpClient();

Теперь можно воспользоваться новым классом в контроллере.

private readonly IDiscordBroker _discordBroker;public GuildController(IDiscordBroker discordBroker){  _discordBroker = discordBroker;}[HttpGet("/check")]public async Task<IActionResult> Check(CancellationToken ct){  await _discordBroker.SendMessage("Hello", ct);  return Ok();}

Запустите проект, зайдите по адресу /check в браузере и убедитесь что в Discord пришло новое сообщение.

Шаг 2. Получаем данные из Battle.net

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

Получаем реальные данные

Вам понадобится зайти на https://develop.battle.net/ и получить там две персональных строки BattleNetId и BattleNetSecret. Они будут нужны нам чтобы авторизоваться в api перед отправкой запросов. Поместите их в appsettings.

Подключим к проекту библиотеку ArgentPonyWarcraftClient.

Создадим новый класс BattleNetApiClient в папке Services.

public class BattleNetApiClient{   private readonly string _guildName;   private readonly string _realmName;   private readonly IWarcraftClient _warcraftClient;   public BattleNetApiClient(IHttpClientFactory clientFactory, IConfiguration configuration)   {       _warcraftClient = new WarcraftClient(           configuration["BattleNetId"],           configuration["BattleNetSecret"],           Region.Europe,           Locale.ru_RU,           clientFactory.CreateClient()       );       _realmName = configuration["RealmName"];       _guildName = configuration["GuildName"];   }}

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

Кроме этого, нужно создать в appsettings проекта две новых записи RealmName и GuildName. RealmName это название игрового мира, а GuildName это название гильдии. Их будем использовать как параметры при запросе.

Сделаем метод GetGuildMembers чтобы получать состав гильдии и создадим модель WowCharacterToken которая будет представлять собой информацию об игроке.

public async Task<WowCharacterToken[]> GetGuildMembers(){   var roster = await _warcraftClient.GetGuildRosterAsync(_realmName, _guildName, "profile-eu");   if (!roster.Success) throw new ApplicationException("get roster failed");   return roster.Value.Members.Select(x => new WowCharacterToken   {       WowId = x.Character.Id,       Name = x.Character.Name   }).ToArray();}
public class WowCharacterToken{  public int WowId { get; set; }  public string Name { get; set; }}

Класс WowCharacterToken следует поместить в папку Models.

Не забудьте подключить BattleNetApiClient в Startup.

services.AddScoped<IBattleNetApiClient, BattleNetApiClient>();

Берем данные из заглушки

Для начала создадим модель WowCharacterToken и поместим ее в папку Models. Она представляет собой информацию об игроке.

public class WowCharacterToken{  public int WowId { get; set; }  public string Name { get; set; }}

Дальше сделаем вот такой класс

public class BattleNetApiClient{    private bool _firstTime = true;    public Task<WowCharacterToken[]> GetGuildMembers()    {        if (_firstTime)        {            _firstTime = false;            return Task.FromResult(new[]            {                new WowCharacterToken                {                    WowId = 1,                    Name = "Артас"                },                new WowCharacterToken                {                    WowId = 2,                    Name = "Сильвана"                }            });        }        return Task.FromResult(new[]        {            new WowCharacterToken            {                WowId = 1,                Name = "Артас"            },            new WowCharacterToken            {                WowId = 3,                Name = "Непобедимый"            }        });    }}

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

Сделайте интерфейс и подключите все что мы создали в Startup.

services.AddScoped<IBattleNetApiClient, BattleNetApiClient>();

Выведем результаты в Discord

После того как мы сделали BattleNetApiClient, им можно воспользоваться в контроллере чтобы вывести кол-во игроков в Discord.

[ApiController]public class GuildController : ControllerBase{  private readonly IDiscordBroker _discordBroker;  private readonly IBattleNetApiClient _battleNetApiClient;  public GuildController(IDiscordBroker discordBroker, IBattleNetApiClient battleNetApiClient)  {     _discordBroker = discordBroker;     _battleNetApiClient = battleNetApiClient;  }  [HttpGet("/check")]  public async Task<IActionResult> Check(CancellationToken ct)  {     var members = await _battleNetApiClient.GetGuildMembers();     await _discordBroker.SendMessage($"Members count: {members.Length}", ct);     return Ok();  }}

Шаг 3. Находим новых и ушедших игроков

Нужно научиться определять какие игроки появились или пропали из списка при последующих запросах к api. Для этого мы можем закэшировать список в InMemory кэше (в оперативной памяти) или во внешнем хранилище.

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

А пока что подключим InMemory кэш в Startup.

services.AddMemoryCache(); 

Теперь в нашем распоряжении есть IDistributedCache, который можно подключить через конструктор. Я предпочел не использовать его напрямую , а написать для него обертку. Создайте класс GuildRepository и поместите его в новую папку Repositories.

public class GuildRepository : IGuildRepository{    private readonly IDistributedCache _cache;    private const string Key = "wowcharacters";    public GuildRepository(IDistributedCache cache)    {        _cache = cache;    }    public async Task<WowCharacterToken[]> GetCharacters(CancellationToken ct)    {        var value = await _cache.GetAsync(Key, ct);        if (value == null) return Array.Empty<WowCharacterToken>();        return await Deserialize(value);    }    public async Task SaveCharacters(WowCharacterToken[] characters, CancellationToken ct)    {        var value = await Serialize(characters);        await _cache.SetAsync(Key, value, ct);    }        private static async Task<byte[]> Serialize(WowCharacterToken[] tokens)    {        var binaryFormatter = new BinaryFormatter();        await using var memoryStream = new MemoryStream();        binaryFormatter.Serialize(memoryStream, tokens);        return memoryStream.ToArray();    }    private static async Task<WowCharacterToken[]> Deserialize(byte[] bytes)    {        await using var memoryStream = new MemoryStream();        var binaryFormatter = new BinaryFormatter();        memoryStream.Write(bytes, 0, bytes.Length);        memoryStream.Seek(0, SeekOrigin.Begin);        return (WowCharacterToken[]) binaryFormatter.Deserialize(memoryStream);    }}

Теперь можно написать сервис который будет сравнивать новый список игроков с сохраненным.

public class GuildService{    private readonly IBattleNetApiClient _battleNetApiClient;    private readonly IGuildRepository _repository;    public GuildService(IBattleNetApiClient battleNetApiClient, IGuildRepository repository)    {        _battleNetApiClient = battleNetApiClient;        _repository = repository;    }    public async Task<Report> Check(CancellationToken ct)    {        var newCharacters = await _battleNetApiClient.GetGuildMembers();        var savedCharacters = await _repository.GetCharacters(ct);        await _repository.SaveCharacters(newCharacters, ct);        if (!savedCharacters.Any())            return new Report            {                JoinedMembers = Array.Empty<WowCharacterToken>(),                DepartedMembers = Array.Empty<WowCharacterToken>(),                TotalCount = newCharacters.Length            };        var joined = newCharacters.Where(x => savedCharacters.All(y => y.WowId != x.WowId)).ToArray();        var departed = savedCharacters.Where(x => newCharacters.All(y => y.Name != x.Name)).ToArray();        return new Report        {            JoinedMembers = joined,            DepartedMembers = departed,            TotalCount = newCharacters.Length        };    }}

В качестве возвращаемого результата используется модель Report. Ее нужно создать и поместить в папку Models.

public class Report{   public WowCharacterToken[] JoinedMembers { get; set; }   public WowCharacterToken[] DepartedMembers { get; set; }   public int TotalCount { get; set; }}

Применим GuildService в контроллере.

[HttpGet("/check")]public async Task<IActionResult> Check(CancellationToken ct){   var report = await _guildService.Check(ct);   return new JsonResult(report, new JsonSerializerOptions   {      Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.Cyrillic)   });}

Теперь отправим в Discord какие игроки присоединились или покинули гильдию.

if (joined.Any() || departed.Any()){   foreach (var c in joined)      await _discordBroker.SendMessage(         $":smile: **{c.Name}** присоединился к гильдии",         ct);   foreach (var c in departed)      await _discordBroker.SendMessage(         $":smile: **{c.Name}** покинул гильдию",         ct);}

Эту логику я добавил в GuildService в конец метода Check. Писать бизнес логику в контроллере не стоит, у него другое назначение. В самом начале мы делали там отправку сообщения в Discord потому что еще не существовало GuildService.

На первом скриншоте в статье вы видели что я вывел больше информации об игроке. Ее можно получить если воспользоваться библиотекой ArgentPonyWarcraftClient

await _warcraftClient.GetCharacterProfileSummaryAsync(_realmName, name.ToLower(), Namespace);

Я решил не добавлять в статью больше кода в BattleNetApiClient, чтобы статья не разрослась до безумных размеров.

Unit тесты

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

Создайте новый проект для Unit тестов. Заведите в нем папку Fakes и сделайте три фейка.

public class DiscordBrokerFake : IDiscordBroker{   public List<string> SentMessages { get; } = new();   public Task SendMessage(string message, CancellationToken ct)   {      SentMessages.Add(message);      return Task.CompletedTask;   }}
public class GuildRepositoryFake : IGuildRepository{    public List<WowCharacterToken> Characters { get; } = new();    public Task<WowCharacterToken[]> GetCharacters(CancellationToken ct)    {        return Task.FromResult(Characters.ToArray());    }    public Task SaveCharacters(WowCharacterToken[] characters, CancellationToken ct)    {        Characters.Clear();        Characters.AddRange(characters);        return Task.CompletedTask;    }}
public class BattleNetApiClientFake : IBattleNetApiClient{   public List<WowCharacterToken> GuildMembers { get; } = new();   public List<WowCharacter> Characters { get; } = new();   public Task<WowCharacterToken[]> GetGuildMembers()   {      return Task.FromResult(GuildMembers.ToArray());   }}

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

Первый тест на GuildService будет выглядеть так:

[Test]public async Task SaveNewMembers_WhenCacheIsEmpty(){   var wowCharacterToken = new WowCharacterToken   {      WowId = 100,      Name = "Sam"   };      var battleNetApiClient = new BattleNetApiApiClientFake();   battleNetApiClient.GuildMembers.Add(wowCharacterToken);   var guildRepositoryFake = new GuildRepositoryFake();   var guildService = new GuildService(battleNetApiClient, null, guildRepositoryFake);   var changes = await guildService.Check(CancellationToken.None);   changes.JoinedMembers.Length.Should().Be(0);   changes.DepartedMembers.Length.Should().Be(0);   changes.TotalCount.Should().Be(1);   guildRepositoryFake.Characters.Should().BeEquivalentTo(wowCharacterToken);}

Как видно из названия, тест позволяет проверить что мы сохраним список игроков, если кэш пуст. Заметьте, в конце теста используется специальный набор методов Should, Be... Это методы из библиотеки FluentAssertions, которые помогают нам сделать Assertion более читабельным.

Теперь у нас есть база для написания тестов. Я показал вам основную идею, дальнейшее написание тестов оставляю вам.

Главный функционал проекта готов. Теперь можно подумать о его публикации.

Шаг 4. Привет Docker и Heroku!

Мы будем размещать проект на платформе Heroku. Heroku не позволяет запускать .NET проекты из коробки, но она позволяет запускать Docker образы.

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

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS builderWORKDIR /sourcesCOPY *.sln .COPY ./src/peon.csproj ./src/COPY ./tests/tests.csproj ./tests/RUN dotnet restoreCOPY . .RUN dotnet publish --output /app/ --configuration ReleaseFROM mcr.microsoft.com/dotnet/core/aspnet:3.1WORKDIR /appCOPY --from=builder /app .CMD ["dotnet", "peon.dll"]

peon.dll это название моего Solution. Peon переводится как батрак.

О том как работать с Docker и Heroku можно прочитать здесь. Но я все же опишу последовательность действий.

Вам понадобится создать аккаунт в Heroku, установить Heroku CLI.

Создайте новый проект в heroku и свяжите его с вашим репозиторием.

heroku git:remote -a project_name

Теперь нам необходимо создать файл heroku.yml в папке с проектом. У него будет такое содержимое:

build:  docker:    web: Dockerfile

Дальше выполним небольшую череду команд:

# Залогинимся в heroku registryheroku container:login# Соберем и запушим образ в registryheroku container:push web# Зарелизим приложение из образаheroku container:release web

Можете открыть приложение в браузере с помощью команды:

heroku open

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

Установите для нашего Heroku приложения бесплатный аддон RedisCloud.

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

Нам нужно получить эту переменную в коде приложения.

Установите библиотеку Microsoft.Extensions.Caching.StackExchangeRedis.

С помощью нее можно зарегистрировать Redis реализацию для IDistributedCache в Startup.

services.AddStackExchangeRedisCache(o =>{   o.InstanceName = "PeonCache";   var redisCloudUrl = Environment.GetEnvironmentVariable("REDISCLOUD_URL");   if (string.IsNullOrEmpty(redisCloudUrl))   {      throw new ApplicationException("redis connection string was not found");   }   var (endpoint, password) = RedisUtils.ParseConnectionString(redisCloudUrl);   o.ConfigurationOptions = new ConfigurationOptions   {      EndPoints = {endpoint},      Password = password   };});

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

public static class RedisUtils{   public static (string endpoint, string password) ParseConnectionString(string connectionString)   {      var bodyPart = connectionString.Split("://")[1];      var authPart = bodyPart.Split("@")[0];      var password = authPart.Split(":")[1];      var endpoint = bodyPart.Split("@")[1];      return (endpoint, password);   }}

На этот класс можно сделать простой Unit тест.

[Test]public void ParseConnectionString(){   const string example = "redis://user:password@url:port";   var (endpoint, password) = RedisUtils.ParseConnectionString(example);   endpoint.Should().Be("url:port");   password.Should().Be("password");}

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

Опубликуйте новую версию приложения.

Шаг 5. Реализуем циклическое выполнение

Нам нужно сделать так чтобы проверка состава гильдии происходила регулярно, например каждые 15 минут.

Есть несколько способов это реализовать:

Самый простой способ - это сделать задание на сайте https://cron-job.org. Этот сервис будет слать get запрос на /check вашего приложения каждые N минут.

Второй способ - это использовать Hosted Services. В этой статье подробно описано как создать повторяющееся задание в ASP.NET Core проекте. Учтите, бесплатный тариф в Heroku подразумевает что ваше приложение будет засыпать после того как к нему некоторое время не делали запросов. Hosted Service перестанет работать после того как приложение заснет. В этом варианте вам следует перейти на платный тариф. Кстати, так сейчас работает мой бот.

Третий способ - это подключить к проекту специальные Cron аддоны. Например Heroku Scheduler. Можете пойти этим путем и разобраться как создать cron job в Heroku.

Шаг 6. Автоматическая сборка, прогон тестов и публикация

Во-первых, зайдите в настройки приложения в Heroku.

Там есть пункт Deploy. Подключите там свой Github аккаунт и включите Automatic deploys после каждого коммита в master.

Поставьте галочку у пункта Wait for CI to pass before deploy. Нам нужно чтобы Heroku дожидался сборки и прогонки тестов. Если тесты покраснеют, то публикация не случится.

Сделаем сборку и прогонку тестов в Github Actions.

Зайдите в репозиторий и перейдите в пункт Actions. Теперь создайте новый workflow на основе шаблона .NET

В репозитории появится новый файл dotnet.yml. Он описывает процесс сборки.

Как видите по его содержимому, задание build будет запускаться после пуша в ветку master.

on:  push:    branches: [ master ]  pull_request:    branches: [ master ]

Содержимое самого задания нас полностью устраивает. Если вы вчитаетесь в то что там происходит, то увидите что там происходит запуск команд dotnet build и dotnet test.

    steps:    - uses: actions/checkout@v2    - name: Setup .NET      uses: actions/setup-dotnet@v1      with:        dotnet-version: 5.0.x    - name: Restore dependencies      run: dotnet restore    - name: Build      run: dotnet build --no-restore    - name: Test      run: dotnet test --no-build --verbosity normal

Менять в этом файле ничего не нужно, все уже будет работать из коробки.

Запуште что-нибудь в master и посмотрите что задание запускается. Кстати, оно уже должно было запуститься после создания нового workflow.

Отлично! Вот мы и сделали микросервис на .NET Core который собирается и публикуется в Heroku. У проекта есть множество точек для развития: можно было бы добавить логирование, прокачать тесты, повесить метрики и. т. д.

Надеюсь данная статья подкинула вам пару новых идей и тем для изучения. Спасибо за внимание. Удачи вам в ваших проектах!

Подробнее..
Категории: C , Net , Api , Docker , Dotnet , Discord , Bot , Бот , Heroku , Микросервис , Wow

Linked Server MSSQL. Оптимизация производительности в 30 раз

13.06.2021 20:13:49 | Автор: admin

Исходные данные:

  1. Два SQL Server'а, которые находятся в прямой доступности между собой, на одном из которых настроен Linked Server.

  2. SQL запрос вида:

insert into LocalDatabaseName.dbo.TableName (column1, column2, ..., columnN)select column1, column2, ..., columnNfrom LinkedServerName.RemoteDatabaseName.dbo.TableName

Задача: максимально быстро скопировать записи с одного сервера на другой

Столкнулся с тем, что подобный запрос выполняется на 40k (40000) записей больше минуты. С ростом количества подобных запросов или количества записей, производительность сильно падает и оптимизировать запрос средствами SQL никак нельзя. С использованием приложения ImportExportDataSql мне удалось ускорить этот запрос до 2 секунд, не используя Linked Server.

Приложение ImportExportDataSql создавал для себя и постоянно его дорабатывал на протяжении нескольких лет. Основные требования при создании приложения - портативность, работа под всеми версиями Windows без установки сторонних библиотек (кроме NET Framework 3.5), простой интерфейс и высокая производительность.

ImportExportDataSql - универсальный конвертер данных, как альтернатива "bcp"

Главная форма ImportExportDataSqlГлавная форма ImportExportDataSql

При работе с данными очень часто требуется загружать файлы из разных файлов в БД (чаще всего CSV и Excel) и обратно (из БД в CSV). До этого пользовался утилитой bcp, но всегда не хватало графического интерфейса. Кроме этого у "bcp", есть недостатки, описанные в моей предыдущей статье.

В ImportExportDataSql кроме графического интерфейса, реализована возможность работы через командную строку. Пример командной строки:

Пример работы ImportExportDataSql из командной строки:
ImportExportDataSql.exe -ConnectionName="Имя соединения с БД" -TaskName="Имя Задачи 1" -TaskName="Имя задачи 2" [-Log="C:\FolderName\LogFileName.log"]

Параметры командной строки:

-ConnectionName - Имя соединения с БД, которое должно быть сохранено на форме "Соединение с БД" по кнопке "Сохранить настройку соединения с БД"

Сохранить настройку соединения с БДСохранить настройку соединения с БД

-TaskName - Имя задачи из пользовательского списка задач

-Log - имя лог файла. Необязательный параметр. По-умолчанию, используется лог файл в папке Logs\UserName\ImportExportDataSql.log

Список решаемых задач в ImportExportDataSql

  1. Сохранить из БД в файл - если файлы хранятся в БД и их нужно сохранить на диск

  2. Сохранить из БД в файл (утилитой bcp) - если файлы хранятся в БД и их нужно сохранить на диск с помощью утилиты bcp (создается bat файл)

  3. Сохранить из файла в БД - если нужно загрузить файлы с диска в таблицу БД с полем типа varbinary

  4. Сохранить из БД в скрипт SQL - сохраняет результат SELECT запроса в SQL файл

  5. Из БД в скрипт SQL (только INSERT)

  6. Из БД в скрипт SQL (только UPDATE)

  7. Статический скрипт SQL

  8. Сохранить из Excel в скрипт SQL

  9. Сохранить из БД в CSV

  10. Сохранить из CSV в SQL

  11. Сохранить из CSV в БД

  12. Сохранить конфигурацию БД в SQL - выгружает структуру БД в SQL файл

  13. Сохранить из БД в БД - сохраняет результат SELECT запроса на другой или текущий сервер

Все типы обработки, которые заканчиваются словом SQL могут объединяться в один файл, если имя файла одинаковое в нескольких задачах. Это очень удобно для копирования данных из одной БД в другую (например, при переносе данных с прода на тест или наоборот).

Сохранить из БД в БД

Сохранить из БД в БДСохранить из БД в БД

Использование данного способа позволило оптимизировать запрос (приведенный в начале статьи) копирования данных через Linked Server, сократив время выполнения с 1 минуты до 2 секунд. Алгоритм копирования данных из одной БД в другую выполнен стандартными классами языка C# из пространства имен System.Data.SqlClient: SqlConnection, SqlDataReader, SqlCommand и SqlBulkCopy.

Чтобы не возникало ошибки нехватки памяти OutOfMemoryException, чтение и запись данных выполняется блоками (частями). Блок ограничивается максимальным количеством записей, который определяется пользователем. Параметры, которые задает пользователь:

  1. SQL запрос - выполняется на БД источнике, с которой нужно копировать информацию

  2. Настройки выгрузки в БД назначения:

    Имя соединения - выбирается из списка соединений, которые пользователь сохраняет на форме "Соединение с БД", отображаемая при запуске приложения. Точка (.) в параметре "Имя соединения" означает, что используется текущее соединение с БД.

    Имя таблицы - в которую нужно копировать записи. Таблицу можно выбирать из списка, либо указать вручную (может содержать не только имя схемы и имя таблицы, но и имя БД)

    Номер последней обрабатываемой строки - служит для ограничения количества копируемых строк, и применяется для отладки. Например, если запрос возвращает 100 записей, а "Номер последней обрабатываемой строки" = 10, то будет скопировано только 10 первых строк из результата запроса.

    Количество строк в блоке - количество строк сохраняемых одной транзакцией

Способом "Сохранить из БД в БД" я также пользуюсь, когда необходимо скопировать результат запроса с большим количеством записей. Ограничение количества записей, в этом случае, дает преимущество, перед обычным запросом копирования записей (insert into ... select ...), так как снижается нагрузка на диск, не сильно растет журнал транзакций и не используется база tempdb (если "Количество строк в блоке" оптимальное).

Преимущества и применение ImportExportDataSql

Приложение ImportExportDataSql постоянно помогает мне в работе. С помощью него удобно переносить данные из одной БД в другую.

В коде встроено множество проверок, чтобы достаточно быстро можно было понимать на какой строке возникла ошибка при импорте CSV файла или Excel.

Можно загружать большие CSV файлы (больше 1Гб) и добавлять свои поля, которых нет в CSV. Отсекать ненужные поля из CSV, не загружая их в БД.

Скрипты при выгрузке в SQL формат дополнены различными проверками, чтобы при выполнении скрипта на другой базе все ошибки отображались в одной таблице, а не списком ошибок на панеле "Messages" в SQL Server Management Studio.

С помощью типа обработки "Сохранить конфигурацию БД в SQL" и командной строки я автоматизировал создание резервных копий джобов (jobs), репликаций и других объектов БД, чего нельзя сделать стандартными способами.

Заключение

Используя язык C# и класс SqlBulkCopy можно существенно сократить время выполнения запроса, в котором используется Linked Server.

Ссылки

Скачать ImportExportDataSql

Статья с подробным описанием ImportExportDataSql

Статья "Быстрое чтение CSV в C#", в которой рассказывается о недостатках "bcp"

Сообщество VK, для желающих пообщаться с автором

Подробнее..

Оптимизация .NET приложения как простые правки позволили ускорить PVS-Studio и уменьшить потребление памяти на 70

15.06.2021 18:13:51 | Автор: admin

Проблемы с производительностью, такие как аномально низкая скорость работы и высокое потребление памяти, могут быть обнаружены самыми разными способами. Такие недостатки приложения выявляются тестами, самими разработчиками или тестировщиками, а при менее удачном раскладе пользователями. Увы, но обнаружение аномалий лишь первый шаг. Далее проблему необходимо локализовать, ведь в противном случае решить её не получится. Тут возникает вопрос как найти в большом проекте причины, приводящие к излишнему потреблению памяти и замедлению работы? Есть ли они вообще? Быть может, дело и не в приложении вовсе? Эта статья посвящена истории о том, как разработчики C#-анализатора PVS-Studio столкнулись с подобной проблемой и смогли решить её.

Бесконечный анализ

Анализ крупных C#-проектов всегда занимает некоторое время. Это ожидаемо PVS-Studio погружается в исследование исходников достаточно глубоко и использует при этом различные технологии, такие как межпроцедурный анализ, анализ потока данных и т.д. Тем не менее анализ многих крупных проектов, найденных нами на github, производится не дольше нескольких часов.

Возьмём, к примеру, Roslyn. Его solution включает более 200 проектных файлов, и почти все из них проекты на C#. Нетрудно догадаться, что в каждом из проектов далеко не по одному файлу, а сами файлы состоят далеко не из пары строчек кода. PVS-Studio проводит полный анализ Roslyn примерно за 1,5-2 часа. Конечно, некоторые проекты наших пользователей требуют гораздо больше времени на анализ, но ситуации, когда анализ не проходит даже за сутки, исключительны.

Именно в такой ситуации и оказался один из наших клиентов. Он написал в поддержку, что анализ его проекта не проходит... Даже за 3 дня! Тут явно было что-то не так. Очевидно, мы не могли оставить подобную проблему без внимания.

Стоп, а как же тестирование?!

Наверняка у читателя возникает логичный вопрос почему же проблема не была выявлена на этапе тестирования? Как же так вышло, что она была обнаружена именно клиентом? Неужели C#-анализатор PVS-Studio не тестируется?

Тестируется и тщательно! Для нас тестирование является неотъемлемой частью процесса разработки. Корректность работы анализатора постоянно проверяется, ровно как проверяется и корректность работы отдельных его частей. Без преувеличения можно сказать, что unit-тесты диагностических правил и внутренних механизмов составляют примерно половину от общего объёма исходного кода проекта C#-анализатора. Кроме того, каждую ночь на сервере производится анализ большого набора проектов и проверка корректности формируемых анализатором отчётов. При этом автоматически определяется как скорость работы анализатора, так и объём потребляемой памяти. Более-менее существенные отклонения от нормы мгновенно обнаруживаются и исследуются.

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

Поиск причин

Дамп

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

Эти детали нам мог дать дамп памяти процесса анализатора. Что это такое? Если вкратце, то дамп это срез данных из оперативной памяти. С его помощью мы решили выяснить, какие данные загружены в рабочую память процесса PVS-Studio. В первую очередь нас интересовали какие-нибудь аномалии, которые могли стать причиной столь сильного замедления работы.

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

От файла с дампом мало толку, если нет возможности его открыть. К счастью, пользователю этим заниматься уже не нужно :). Ну а мы решили изучить данные дампа при помощи Visual Studio. Делается это достаточно просто:

  1. Открываем проект с исходниками приложения в Visual Studio.

  2. В верхнем меню нажимаем File->Open->File (или Ctrl+O).

  3. Находим файл с дампом и открываем.

В результате появится окошко с кучей различной информации о процессе:

Нас в первую очередь интересовала возможность перехода в своеобразный режим отладки дампа. Для этого нужно нажать кнопку Debug With Managed Only.

Примечание. Если вас интересует более подробная информация по теме открытия дампов через Visual Studio для отладки, то отличным источником информации будет официальная документация.

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

  • отсутствует какая-либо возможность возобновления работы процесса, пошагового выполнения кода и т.п.;

  • в окне Quick Watch и Immediate Window невозможно использовать некоторые функции. К примеру, попытка вызова метода File.WriteAllText приводила к возникновению ошибки "Caracteres no vlidos en la ruta de acceso!". Дело в том, что дамп связан с окружением, на котором он был снят.

Отладка дампа позволила нам получить достаточно большое количество различных данных. Ниже представлена небольшая часть информации о состоянии процесса анализа в момент снятия дампа:

  • вычисленное количество файлов в проекте: 1 500;

  • приблизительное время анализа: 24 часа;

  • количество одновременно анализируемых в текущий момент файлов: 12;

  • количество уже проверенных файлов: 1060.

Работа с дампом позволила нам сделать некоторые выводы. Было ясно, что анализ большей части файлов проекта к моменту снятия дампа уже прошёл. Из этого следовало, что замедление стало проявляться ближе к концу анализа. У нас появилось предположение, что факторы, приводящие к замедлению, могли накапливаться.

Тем не менее, понять причины замедления нам не удалось. Мы не обнаружили каких-либо аномалий или ошибок, а количество файлов в проекте не казалось чем-то из ряда вон выходящим. Анализ проекта похожего объёма обычно занимает у нас около 2 часов.

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

Наконец-то, воспроизведение проблемы

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

Мы решили пойти дальше и создать собственный тестовый проект с большим количеством сложных конструкций. Было крайне важно воспроизвести проблему локально это позволило бы сильно упростить дальнейший поиск её решения.

Мы создали свой тестовый проект, стараясь повторить следующие характеристики проекта пользователя:

  • количество файлов;

  • средний размер файлов;

  • максимальный уровень вложенности и сложность используемых конструкций.

Скрестив пальцы, мы запустили его анализ и...

Никаких замедлений. После стольких приложенных усилий мы так и не смогли воспроизвести проблему. Анализ сформированного проекта проходил за вполне адекватное время и успешно завершался. Ни зависаний, ни ошибок, ни каких-то аномалий. В такие моменты можно всерьёз задуматься а не решил ли пользователь над нами пошутить?

Казалось, что мы уже всё перепробовали. Казалось, что докопаться до правды не выйдет. А ведь мы бы и рады были заняться исследованием проблемы с замедлением! Мы бы и рады были одолеть её, наконец, порадовать клиента, порадоваться самим. Как ни крути, анализ проекта нашего пользователя не должен был зависать!

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

Неожиданно обнаружилось, что на компьютере одного из сотрудников анализ тестового проекта также никак не мог пройти. Он использовал ту же самую версию анализатора и проверял тот же самый проект. В чём же тогда было отличие?

А отличие было в железе. Точнее говоря, в ОЗУ.

Казалось бы, при чём тут ОЗУ?

Наши автоматизированные тесты проводятся на сервере с 32 Гб доступной оперативной памяти. На компьютерах сотрудников её объём различается, но везде есть по крайней мере 16 гигабайт, а у большинства 32 и более. Воспроизвести же баг удалось на ноутбуке, объём оперативной памяти которого составлял 8 Гб.

Возникает логичный вопрос к чему это всё? Мы же решали проблему замедления работы, а не высокого потребления памяти!

Дело в том, что высокое потребление памяти действительно может приводить к замедлению работы приложения. Это происходит в тех случаях, когда процессу не хватает памяти, установленной на устройстве. В таких случаях активируется особый механизм memory paging (другое название "swapping"). При его работе часть данных из оперативной памяти переносится во вторичное хранилище (диск). При необходимости система загружает данные с диска. Благодаря данному механизму приложения могут использовать оперативную память в большем объёме, чем доступно в системе. Увы, но у этого чуда есть своя цена.

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

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

Решаем проблему

dotMemory и диаграмма доминаторов

Мы использовали приложение dotMemory, разработанное компанией JetBrains. Это профилировщик памяти для .NET, который можно запускать как прямо из Visual Studio, так и в качестве отдельного инструмента. Среди всех возможностей dotMemory более всего нас интересовало профилирование процесса анализа.

Ниже представлено окно присоединения к процессу:

Сначала нужно запустить соответствующий процесс, затем выбрать его и начать профилирование с помощью кнопки "Run". Откроется новое окно:

В любой момент времени можно получить снимок состояния памяти. За время работы процесса можно сделать несколько таких снимков все они появятся на панели "Memory Snapshots":

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

Более полную информацию о работе с dotMemory, включая подробное описание представленных здесь данных, можно найти в официальной документации. Нам же была особенно интересна sunburst диаграмма, показывающая иерархию доминаторов объектов, эксклюзивно удерживающих другие объекты в памяти. Для перехода к ней необходимо открыть вкладку "Dominators".

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

Объекты высокого уровня были неособенно интересны, ведь сами по себе они не занимали много места. Куда важнее было узнать, что именно содержится "внутри". Какие же объекты размножились настолько, что начали занимать так много места?

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

Анализ потока данных (Data-Flow Analysis) заключается в вычислении возможных значений переменных в различных точках компьютерной программы. Например, если ссылка разыменовывается и при этом известно, что в текущий момент она может быть равна null, то это потенциальная ошибка, и статический анализатор сообщит о ней. Подробнее об этой и других технологиях, использующихся в PVS-Studio, можно прочитать в статье.

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

Что же тогда делать? Неужели опять тупик?

А не такие уж они и разные

Итак, вычисленные значения переменных кешируются, и их очень много. Настолько много, что проект не проверяется даже за 3 дня. Отказаться от кеширования этих значений мы не можем. Но что, если как-то оптимизировать способ их хранения?

Мы решили повнимательнее взглянуть на значения в кеше. Оказалось, что PVS-Studio хранил большое количество абсолютно идентичных объектов. К примеру, для многих переменных анализатор не может вычислить значение, так как оно может быть любым (в пределах ограничений своего типа):

void MyFunction(int a, int b, int c ....){  // a = ?  // b = ?  // c = ?  ....}

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

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

А вот и нет! На самом деле, нужно совсем немного:

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

  • механизмы доступа к хранилищу добавление новых и получение существующих элементов;

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

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

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

Кроме того, можно вспомнить и такое понятие, как интернирование строк. По сути то же самое: если строки одинаковы по значению, то фактически они будут представлены одним и тем же объектом. В C# строковые литералы интернируются автоматически. Для прочих строк можно использовать методы string.Intern и string.IsInterned. Однако не всё так просто. Даже этим механизмом нужно пользоваться с умом. Если вам интересна данная тема, то предлагаю к прочтению статью "Подводные камни в бассейне строк, или ещё один повод подумать перед интернированием экземпляров класса String в C#".

Выигранная память

Мы внесли несколько мелких правок, реализовав паттерн Flyweight. Каковы были результаты?

Они были невероятны! Пиковое потребление оперативной памяти при проверке тестового проекта уменьшилось с 14,55 до 4,73 гигабайт. Столь простое и быстрое решение позволило уменьшить расход памяти примерно на 68%! Мы были шокированы и очень довольны результатом. Доволен был и клиент теперь ОЗУ его компьютера хватало, а значит, и анализ начал проходить за адекватное время.

Достигнутый результат действительно радовал, но...

Нужно больше оптимизаций!

Да, мы смогли уменьшить потребление памяти. Однако изначально мы же хотели ускорить анализ! Конечно, он действительно ускорился у клиента, как и на других машинах, где не хватало ОЗУ. Но ускорения на мощных компьютерах мы не добились только сократили потребление памяти. А раз уж мы столь глубоко погрузились в эту тему... Почему бы не продолжить?

dotTrace

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

Ответы на наши вопросы мог дать dotTrace хороший профилировщик производительности для .NET приложений, предоставляющий ряд интересных возможностей. Интерфейс этого приложения довольно сильно напоминает dotMemory:

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

Итак, используя dotTrace, мы запустили анализ одного большого проекта. Ниже показан пример окна, отображающего в реальном времени графики использования процессом памяти и CPU:

Чтобы начать "запись" данных о работе приложения, нужно нажать Start (по умолчанию процесс сбора данных начинается сразу). Подождав некоторое время, нажимаем "Get Snapshot And Wait". Перед нами отображается окно с собранными данными. Например, для простого консольного приложения это окно выглядит так:

Здесь нам доступно большое количество различной информации. В первую очередь интересно время работы отдельных методов. Также может быть полезно узнать время работы потоков. Доступна и возможность рассмотрения общего отчёта для этого нужно кликнуть в верхнем меню View->Snapshot Overview или использовать комбинацию Ctrl+Shift+O.

Уставший сборщик мусора

Что же мы смогли выяснить благодаря dotTrace? Ну, во-первых, мы в очередной раз убедились, что C#-анализатор не использует процессорные мощности даже наполовину. PVS-Studio C# многопоточное приложение, и, по идее, нагрузка на процессор должна быть ощутимой. Несмотря на это, при анализе загрузка процессора часто падала до 1315% общей мощности CPU. Очевидно, работаем неэффективно, но почему?

dotTrace показал нам, что большую часть времени анализа работает даже не само приложение, а сборщик мусора! Возникает логичный вопрос как же так?

Дело в том, что запуск сборки блокировал потоки анализатора. Сборка завершалась, анализатор немного поработал и снова запускается сборка мусора, а PVS-Studio "отдыхает".

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

Мы не виноваты, это всё их DisplayPart!

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

Возможно, мы могли бы вообще отказаться от использования этих объектов, если бы не один нюанс. В исходниках нашего C#-анализатора DisplayPart даже не упоминается! Как оказалось, этот тип играет определённую роль в используемом нами Roslyn API.

Roslyn (или .NET Compiler Platform) является основой C#-анализатора PVS-Studio. Он предоставляет нам готовые решения для ряда задач:

  • преобразование файла с исходным кодом в синтаксическое дерево;

  • удобный способ обхода синтаксического дерева;

  • получение различной (в том числе семантической) информации о конкретном узле дерева;

  • и т.д.

Roslyn платформа с открытым исходным кодом. Это позволило без проблем понять, что такое *DisplayPart *и зачем этот тип вообще нужен.

Оказалось, что объекты DisplayPart активно используются при создании строковых представлений так называемых символов. Если не погружаться в детали, то символ это объект, содержащий семантическую информацию о некоторой сущности в исходном коде. К примеру, символ метода позволяет получить данные о параметрах данного метода, классе-родителе, возвращаемом типе и т.д. Более подробно данная тема освещена в статье "Введение в Roslyn. Использование для разработки инструментов статического анализа". Очень рекомендую к прочтению всем, кто интересуется статическим анализом (вне зависимости от предпочитаемого языка программирования).

Строковые представления некоторых символов нам действительно приходилось получать, и делали мы это с помощью вызова метода ToString. Как оказалось, внутри отрабатывал сложный алгоритм, активно создающий объекты типа DisplayPart. Проблема состояла в том, что алгоритм отрабатывал каждый раз, когда нам было необходимо получить строковое представление (то есть довольно часто).

Как это обычно и бывает, локализация проблемы = 90% её решения. Раз уж вызовы ToString у символов создают столько проблем, то, может, и не стоит производить их?

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

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

Описанная правка, вопреки ожиданиям, практически не увеличила загрузку процессора (изменение составляло буквально несколько процентов). Тем не менее, PVS-Studio стал работать значительно быстрее: один из наших тестовых проектов ранее анализировался 2,5 часа, а после правок анализ проходил всего за 2. Ускорение работы на 20% действительно радовало.

Упакованный Enumerator

На втором месте по количеству выделяемой памяти были объекты типа List<T>.Enumerator, используемые при обходе соответствующих коллекций. Итератор списка является структурой, а значит, создаётся на стеке. Тем не менее, трассировка показывала, что такие объекты в больших количествах попадали в кучу! С этим нужно было разобраться.

Объект значимого типа может попасть в кучу в результате упаковки (boxing). Она выполняется при приведении объекта значимого типа к object или реализуемому интерфейсу. Итератор списка реализует интерфейс IEnumerator, и именно приведение к этому интерфейсу вело к попаданию итератора в кучу.

Для получения объекта Enumerator используется метод GetEnumerator. Общеизвестно, что это метод, определённый в интерфейсе IEnumerable. Взглянув на его сигнатуру, можно заметить, что возвращаемый тип данного метода IEnumerator. Получается, что вызов GetEnumerator у списка всегда приводит к упаковке?

А вот и нет! Метод GetEnumerator, определённый в классе List, возвращает структуру:

Так всё-таки будет упаковка производиться или нет? Ответ на этот вопрос зависит от типа ссылки, у которой вызывается GetEnumerator:

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

Конечно, разница невелика, если такой *Enumerator *создаётся лишь пару сотен раз за время работы программы. Однако при анализе более-менее объёмного проекта в нашем C#-анализаторе эти объекты создаются миллионы или даже десятки миллионов раз. В таких случаях разница становится весьма ощутимой.

Примечание. Как правило, мы не вызываем GetEnumerator напрямую. Зато достаточно часто приходится использовать цикл foreach. Именно он "под капотом" получает итератор. Если в foreach передана ссылка типа List, то и итератор, используемый в foreach, будет лежать на стеке. Если же с помощью foreach производится обход абстрактного IEnumerable, то итератор будет сохранён в куче, а foreach будет работать со ссылкой типа IEnumerator. Описанное поведение актуально и для других коллекций, в которых присутствует GetEnumerator, возвращающий итератор значимого типа.

Конечно, нельзя полностью отказаться от использования IEnumerable. Однако в коде анализатора мы обнаружили множество мест, где метод принимал в качестве аргумента абстрактный IEnumerable, а при его вызове всегда передавался вполне конкретный List.

Конечно, обобщение это хорошо, ведь метод, принимающий IEnumerable, сможет работать с любой коллекцией, а не с какой-то конкретной. Однако иногда у этого подхода есть серьёзные недостатки, а вот какие-либо реальные преимущества отсутствуют.

И ты, LINQ?!

Методы расширения, определённые в пространстве имён System.Linq, используются для работы с коллекциями повсеместно. Достаточно часто они действительно позволяют упростить код. Наверное, ни один более-менее серьёзный проект не обходится без использования всеми любимых методов Where, Select и т. д. C#-анализатор PVS-Studio не исключение.

Что ж, красота и удобство LINQ-методов дорого нам обошлись. Так дорого, что во многих местах мы отказались от их использования в пользу простого foreach. Как же так вышло?

Основная проблема снова состояла в создании огромного количества объектов, реализующих интерфейс IEnumerator. Такие объекты создаются на каждый вызов LINQ-метода. Взгляните на следующий код:

List<int> sourceList = ....var enumeration = sourceList.Where(item => item > 0)                            .Select(item => someArray[item])                            .Where(item => item > 0)                            .Take(5);

Сколько итераторов будет создано при его выполнении? Давайте посчитаем! Чтобы понять, как всё это работает, откроем исходники System.Linq. Они доступны на github по ссылке.

При вызове Where будет создан объект класса WhereListIterator особая версия Where-итератора, оптимизированная для работы с List (похожая оптимизация есть и для массивов). Данный итератор хранит внутри ссылку на список. При переборе коллекции WhereListIterator сохранит в себе итератор списка, после чего будет использовать его при работе. Так как WhereListIterator рассчитан именно на список, то приведение итератора к типу IEnumerator не производится. Однако сам *WhereListIterator *является классом, а значит, его экземпляры попадут в кучу. Следовательно, исходный итератор в любом случае будет храниться не на стеке.

Вызов Select приведёт к созданию объекта класса WhereSelectListIterator. Очевидно, и он будет храниться в куче.

Последующие вызовы Where и *Take *также приведут к созданию итераторов и выделению памяти под них.

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

Теперь взглянем на фрагмент, написанный с использованием foreach:

List<int> sourceList = ....List<int> result = new List<int>();foreach (var item in sourceList){  if (item > 0)  {    var arrayItem = someArray[item];    if (arrayItem > 0)    {      result.Add(arrayItem);      if (result.Count == 5)        break;    }  }}

Давайте попробуем проанализировать сравнить подходы с foreach и LINQ.

  • Преимущества варианта с LINQ-вызовами:

    • короче, приятнее выглядит и в целом лучше читается;

    • не требует создания коллекции для хранения результата;

    • вычисление значений будет произведено только при обращении к элементам;

    • в большинстве случаев объект, полученный в результате запроса, хранит только один элемент последовательности.

  • Недостатки варианта с LINQ-вызовами:

    • память в куче выделяется гораздо чаще: в первом примере туда попадает 5 объектов, а во втором только 1 (список result);

    • повторные обходы последовательности приводят к повторному выполнению обхода с выполнением всех указанных функций. Случаи, когда такое поведение действительно полезно, довольно редки. Конечно, можно использовать методы типа ToList, однако это сводит преимущества варианта с LINQ-вызовами на нет (кроме первого).

В целом недостатки не очень весомы, если LINQ-запрос выполняется относительно нечасто. Однако мы оказались в ситуации, когда это происходило сотни тысяч и даже миллионы раз. Кроме того, важно понимать, что эти запросы были вовсе не так просты, как в приведённом примере.

При всём этом мы заметили, что в подавляющем большинстве случаев у нас совершенно не было интереса в отложенном выполнении. Либо для результата LINQ-операций сразу вызывался какой-нибудь ToList, либо код запросов выполнялся по несколько раз при повторных обходах коллекции (что не есть хорошо).

*Замечание. *На самом деле существует простой способ реализовать отложенное выполнение и не плодить при этом лишние итераторы. Возможно, вы догадались, что я говорю о ключевом слове yield. С его помощью можно реализовывать генерацию последовательности элементов, задавать любые правила и условия добавления элементов в последовательность. Подробнее о возможностях yield в C# (а также о том, как эта штука работает внутри) можно найти в статье "Что такое yield и как он работает в C#?".

Изучив внимательно код анализатора, мы обнаружили множество мест, в которых оптимальнее использовать foreach вместо LINQ-методов. Это позволило существенно сократить количество необходимых операций выделения памяти в куче и сборки мусора.

Что же в итоге?

Успех!

Оптимизация работы PVS-Studio прошла успешно! Мы добились успехов в уменьшении потребляемой памяти, а также серьёзно увеличили скорость анализа (на некоторых проектах скорость работы увеличилась более чем на 20%, а пиковое потребление памяти сократилось практически на 70%!). А ведь всё начиналось с непонятной истории клиента о том, как он три дня не мог проверить свой проект! Тем не менее, на этом оптимизация работы анализатора не заканчивается, и мы продолжаем находить новые способы совершенствования PVS-Studio.

Изучение проблем заняло у нас куда больше времени, чем их решение. Но рассказанная история произошла очень давно. Сейчас, как правило, подобные вопросы решаются командой PVS-Studio куда быстрее. Главными помощниками в исследовании проблем выступают различные инструменты, такие как трассировщик и профилировщик. В этой статье я рассказывал о нашем опыте работы с dotMemory и dotPeek, однако это вовсе не означает, что эти приложения единственные в своём роде. Пожалуйста, напишите в комментариях, какими инструментами в таких случаях пользуетесь вы.

Это ещё не конец

Да, мы действительно решили проблему клиента и даже ускорили анализатор в целом, однако... Очевидно, он работает далеко не так быстро, как мог бы. PVS-Studio всё ещё недостаточно активно использует процессорные мощности. Проблема состоит не совсем в алгоритмах анализа проверка каждого файла в отдельном потоке позволяет обеспечивать достаточно высокий уровень параллелизма. Основной бедой производительности C#-анализатора является сборщик мусора, который очень часто блокирует работу всех потоков, что сильно замедляет PVS-Studio. Даже если анализатор будет использовать сотни ядер, скорость работы будет снижаться из-за частой блокировки потоков сборщиком, который, в свою очередь, не может использовать все доступные мощности при выполнении своих задач в силу некоторых алгоритмических ограничений.

Однако и это не тупик, а лишь ещё одно препятствие, которое мы должны преодолеть. Некоторое время назад до меня дошла "секретная информация" о планах реализации процесса анализа... в нескольких процессах! Это позволит обойти существующие ограничения, ведь сборка мусора в одном из процессов не будет влиять на анализ, выполняющийся в других. Подобный подход позволит эффективно использовать большое количество ядер в том числе и с использованием Incredibuild. Кстати, похожим образом уже работает C++ анализатор, для которого давным-давно доступна возможность распределённого анализа.

Откуда ещё берутся проблемы с производительностью

Кстати, стоит сказать, что проблемы с производительностью часто бывают связаны не с чрезмерным использованием LINQ-запросов или чем-то подобным, а с самыми обыкновенными ошибками в коде. Какие-нибудь "always true"-условия, заставляющие метод работать гораздо дольше, чем необходимо, опечатки и прочее всё это может негативно сказаться как на производительности, так и на корректности работы приложения в целом.

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

PVS-Studio один из таких анализаторов. Он использует серьёзные технологии вроде межпроцедурного анализа или анализа потока данных, что позволяет значительно повысить надёжность кода любого приложения. Кроме того, одним из наиболее приоритетных направлений работы компании является поддержка пользователей, решение их вопросов и возникающих проблем, а в некоторых случаях мы даже добавляем по просьбе клиента новый функционал :). Смело пишите нам по всем возникающим вопросам! А чтобы попробовать анализатор в деле вы можете перейти по ссылке. Удачного использования!

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikita Lipilin. .NET Application Optimization: Simple Edits Speeded Up PVS-Studio and Reduced Memory Consumption by 70%.

Подробнее..

Перевод Предупреждение для разработчиков о грядущих критических изменениях в движке

16.06.2021 14:10:53 | Автор: admin

Поддержка движка отстает, а исправление положения - задача не из легких

Разработчик программного обеспечения Unity Джош Питерсон рассказал нам о будущем поддержки .NET в широко используемом движке для разработки игр.

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

Обработчик сценариев C# использует Mono, но разработчики также могут использовать .NET Framework при работе в Windows. Mono - это старая реализация .NET с открытым исходным кодом, созданная до того, как Microsoft выпустила .NET Core. Microsoft получила контроль над Mono вместе с Xamarin в 2016 году, и Mono теперь имеет много общего кода с .NET Core, но он все равно остается отдельным продуктом, в котором по-прежнему в некоторых сценариях используется рантайм.

Unity поддерживает собственный форк Mono, который, по словам Питерсона, примерно на два года отстает от upstream кода. Сейчас команда обновляет его до последней версии кода из upstream репозитория Mono - изменение, в котором он уверен на 95%, что оно попадет в следующий релиз, Unity 2021.2. Он добавил, что эта работа улучшит производительность и исправит ошибки, но сама по себе не привнесет никаких новых фич .NET - хотя она закладывает фундамент для фич, которые будут добавлены в будущем.

Тем не менее, Питерсон ожидает, что в Unity 2021.2 будет добавлена поддержка .NET Standard 2.1, но на этот раз с 75% уверенностью. Версии .NET Standard определяют набор API, которые должна поддерживать реализация .NET. Сложный аспект .NET Standard 2.1 заключается в том, что .NET Framework навсегда застрял на .NET Standard 2.0. Питерсон говорит: Хотя .NET Framework не поддерживает .NET Standard 2.1, библиотеки классов Mono его поддерживают, поэтому мы должны быть в состоянии выстроить хороший мост к экосистеме на основе .NET Core.

Это обновление не может произойти быстро, поэтому некоторые разработчики разочарованы таким медленным прогрессом. Предпринимаются ли какие-либо подвижки в направлении отказа от Mono в пользу полной интеграции .NET? Особенно сейчас, когда .NET становится настолько кроссплатформенным, - спросил пользователь в августе прошлого года. К числу востребованных фич относятся Span<T>, представленный в C# 7.2, и оператор диапазона, представленный в C# 8.0. Microsoft выпустила C# 8.0 в сентябре 2019 года, и внедрение полного набора фич в Unity заняло много времени. Пользователи также обеспокоены отставанием в производительности .NET в Unity.

Питерсон говорит, что поддержка C# 8.0 в 2021 году по-прежнему будет реализована на основе Mono. Он также выразил надежду, что C# 9.0, выпущенный Microsoft в ноябре 2020 года, также будет поддерживаться, но это зависит от добавления фич в Mono и IL2CPP (который преобразует код .NET в C++ для компиляции), в чем его уверенность снизилась до 50%, сказал он.

Что касается перехода на .NET Core, это вряд ли будет скоро. Питерсон сказал, что Unity, вероятно, откажется от .NET 5 в пользу .NET 6, который является предстоящим релизом с долгосрочной поддержкой. Даже тут он заметил, что похоже, что JIT рантайм здесь будет Mono, но он не уверен в этом и добавил, что нам может потребоваться перейти непосредственно к CoreCLR в целях поддержки .NET 6.

Одна из проблем заключается в том, что функция редактора Unity, называемая перезагрузкой домена (domain reloading), которая сбрасывает состояние сценария, зависит от функции (AppDomains), которой нет в .NET Core. Питерсон говорит, что это может быть реализовано другим способом, но это будет критическое изменение. Для разработчиков игр .NET 6 в любом случае станет критическим изменением, поскольку любые сборки, скомпилированные с использованием mscorlib.dll из экосистемы .NET Framework, не будут работать и должны быть перекомпилированы.

Сложность, связанная с .NET Standard, .NET Framework, .NET Core и Mono, является проблемой для разработчиков Unity и показывает, что унификация .NET, которую затеяла Microsoft, на самом деле является длительным процессом, а не тем, что может произойти в одночасье с выпуском .NET 5.0 в прошлом году.

Единственное, что меня волнует, это поддержка .NET 6. Самая большая проблема, с которой я столкнулся, заключалась в низкой производительности редактора и длительном времени итерации по мере увеличения размера проекта. В настоящее время я отказался от Unity, потому что его было слишком неудобно использовать, и перешел на Unreal, - сказал другой пользователь, добавив, что Mono скоро станет историей, и у него нет будущего.

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


В преддверии старта курса "Unity Game Developer. Professional" приглашаем всех желающих посетить бесплатный двухдневный интенсив в рамках которого мы разработаем все необходимые инструменты и архитектуру для диалоговой системы (чтобы наш персонаж мог общаться с неигровыми персонажами), реализуем инвентарь, добавим в игру торговцев и создадим систему квестов. Всего два занятия и практически готовая RPG у вас в кармане.

Подробнее..

Как WCF сам себе в ногу стреляет посредством TraceSource

21.06.2021 14:12:41 | Автор: admin

Не так часто удается написать что-то интересное про проблемы, связанные с параллельным программированием. В этот же раз "повезло". Из-за особенностей реализации стандартного метода TraceEvent произошла ошибка с блокировкой нескольких потоков. Хочется предупредить о существующем нюансе и рассказать об интересном случае из поддержки наших пользователей. Причем тут поддержка? Это вы узнаете из статьи. Приятного чтения.

Предыстория

В дистрибутиве PVS-Studio есть одна утилита под названием CLMonitor.exe, или система мониторинга компиляции. Она предназначена для "бесшовной" интеграции статического анализа PVS-Studio для языков C и C++ в любую сборочную систему. Сборочная система должна использовать для сборки файлов один из компиляторов, поддерживаемых анализатором PVS-Studio. Например: gcc, clang, cl, и т.п.

Стандартный сценарий работы данной Windows утилиты очень простой, всего 3 шага:

  1. Выполняем 'CLMonitor.exe monitor';

  2. Выполняем сборку проекта;

  3. Выполняем 'CLMonitor.exe analyze'.

Первый шаг запускает 'сервер', который начинает отслеживать все процессы компиляторов в системе до тех пор, пока его не остановят. Как только мы запустили сервер выполняем сборку проекта, который мы хотим проанализировать. Если сборка прошла, то нужно запустить анализ. Для этого мы исполняем третий шаг. 'CLMonitor.exe analyze' запускает 'клиент', который говорит серверу: "Всё, хватит, выключайся и давай сюда результаты мониторинга за процессами". В этот момент сервер должен завершить свою работу, а клиент запустить анализ. Подробнее о том, как внутри работает система мониторинга и зачем сервер вообще собирает процессы, мы поговорим чуть позже.

И вот в один прекрасный момент описанный сценарий не заработал, анализ просто-напросто не запустился. Ну и чтобы было поинтереснее, возникла эта проблема не у нас, а у пользователя, который обратился к нам в поддержку. У него стабильно после запуска анализа происходило десятиминутное ожидание ответа от сервера с дальнейшим выходом из программы по timeout'у. В чём причина непонятно. Проблема не воспроизводится. Да... беда. Пришлось запросить дампфайл для процесса нашей утилиты, чтобы посмотреть, что там происходит внутри.

Примечание. Проблема у пользователя возникла при использовании Windows утилиты CLMonitor.exe. Поэтому все дальнейшие примеры будут актуальны именно для Windows.

Как работает CLMonitor.exe

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

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

Зачем мы вообще отлавливаем процессы

Как вы поняли, история начинается с того, что нужно запустить сервер, который будет отлавливать все процессы. Делаем мы это не просто так. Вообще, более удобный способ проанализировать C++ проект это прямой запуск анализатора через утилиту командной строки PVS-Studio_Cmd. У неё, однако, есть существенное ограничение она может проверять только проекты для Visual Studio. Дело в том, что для анализа требуется вызывать компилятор, чтобы он препроцессировал проверяемые исходные файлы, ведь анализатор работает именно с препроцессированными файлами. А чтобы вызвать препроцессор, нужно знать:

  • какой конкретно компилятор вызывать;

  • какой файл препроцессировать;

  • параметры препроцессирования.

Утилита PVS-Studio_Cmd узнает все необходимое из проектного файла (*.vcxproj). Однако это работает только для "обычных" MSBuild проектов Visual Studio. Даже для тех же NMake проектов мы не можем получить необходимую анализатору информацию, потому что она не хранится в самом проектном файле. И это несмотря на то, что NMake также является .vcxproj. Сам проект при этом является как бы обёрткой для другой сборочной системы. Тут в игру и вступают всяческие ухищрения. Например, для анализа Unreal Engine проектов мы используем прямую интеграцию с *Unreal Build Tool * сборочной системой, используемой "под капотом". Подробнее здесь.

Поэтому, для того чтобы можно было использовать PVS-Studio независимо сборочной системы, даже самой экзотической, у нас и появилась утилита CLMonitor.exe. Она отслеживает все процессы во время сборки проекта и отлавливает вызовы компиляторов. А уже из вызовов компиляторов мы получаем всю необходимую информацию для дальнейшего препроцессирования и анализа. Теперь вы знаете, зачем нам нужно мониторить процессы.

Как клиент запускает анализ

Для обмена данными между сервером и клиентом мы используем программный фреймворк WCF (Windows Communication Foundation). Давайте далее кратко опишем, как мы с ним работаем.

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

static ErrorLevels PerformMonitoring(....) {  using (ServiceHost host = new ServiceHost(                       typeof(CLMonitoringContract),                          new Uri[]{new Uri(PipeCredentials.PipeRoot)}))   {    ....    host.AddServiceEndpoint(typeof(ICLMonitoringContract),                             pipe,                             PipeCredentials.PipeName);    host.Open();         ....  }}

Обратите тут внимание на две вещи: *CLMonitoringContract *и ICLMonitoringContract.

*ICLMonitoringContract * это сервисный контракт. *CLMonitoringContract * реализация сервисного контракта. Выглядит это так:

[ServiceContract(SessionMode = SessionMode.Required,                  CallbackContract = typeof(ICLMonitoringContractCallback))]interface ICLMonitoringContract{  [OperationContract]  void StopMonitoring(string dumpPath = null);} [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]class CLMonitoringContract : ICLMonitoringContract{  public void StopMonitoring(string dumpPath = null)  {    ....    CLMonitoringServer.CompilerMonitor.StopMonitoring(dumpPath);  } }

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

public void FinishMonitor(){  CLMonitoringContractCallback сallback = new CLMonitoringContractCallback();  var pipeFactory = new DuplexChannelFactory<ICLMonitoringContract>(           сallback,            pipe,            new EndpointAddress(....));  ICLMonitoringContract pipeProxy = pipeFactory.CreateChannel();  ((IContextChannel)pipeProxy).OperationTimeout = new TimeSpan(24, 0, 0);  ((IContextChannel)pipeProxy).Faulted += CLMonitoringServer_Faulted;  pipeProxy.StopMonitoring(dumpPath);}

Когда клиент выполняет метод StopMonitoring, он на самом деле выполняется у сервера и вызывает его остановку. А клиент получает данные для запуска анализа.

Всё, теперь вы, хоть немного, имеете представление о внутренней работе утилиты CLMonitor.exe.

Просмотр дамп файла и осознание проблемы

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

**Интересный факт. **Откуда вообще взялись эти 10 минут? Дело в том, что мы задаем время ожидания ответа от сервера намного больше, а именно - 24 часа, как видно в примере кода, приведённом выше. Однако для некоторых операций фреймворк сам решает, что это слишком много и он успеет быстрее. Поэтому берет только часть от изначального значения.

Мы попросили у пользователя снять дамп с двух процессов (клиент и сервер) минуток через 5 после запуска клиента, чтобы посмотреть, что там происходит.

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

Дамп 'клиента'

Так вот, когда мы открыли дамп файл клиента, перед глазами предстал следующий список потоков:

Нас интересует главный поток. Он висит на методе, отвечающем за запрос остановки сервера:

public void FinishMonitor(){  ....  ICLMonitoringContract pipeProxy = pipeFactory.CreateChannel();  ((IContextChannel)pipeProxy).OperationTimeout = new TimeSpan(24, 0, 0);  ((IContextChannel)pipeProxy).Faulted += CLMonitoringServer_Faulted;  pipeProxy.StopMonitoring(dumpPath);            // <=  ....}

Получается, что клиент попросил сервер завершить работу, а ответа не последовало. И это странное поведение, так как обычно данная операция занимает доли секунды. А я напомню, что в данном случае дамп был снят спустя 5 минут от начала работы клиента. Что ж, давайте смотреть, что там на сервере происходит.

Дамп 'сервера'

Открываем его и видим следующий список потоков:

Воу-воу, откуда так много TraceEvent'ов? Кстати, на скриншоте не уместилось, но всего их более 50. Ну давайте подумаем. Данный метод у нас используется, чтобы логировать различную информацию. Например, если отловленный процесс является компилятором, который не поддерживается, произошла ошибка считывания какого-либо параметра процесса и т.д. Посмотрев стеки данных потоков, мы выяснили, что все они ведут в один и тот же метод в нашем коде. А метод этот смотрит, является ли отловленный нашей утилитой процесс компилятором или это нечто иное и неинтересное, и, если мы отловили такой неинтересный процесс, мы это логируем.

Получается, что у пользователя запускается очень много процессов, которые, конкретно для нас, являются 'мусором'. Ну допустим, что это так. Однако данная картина все равно выглядит очень подозрительно. Почему же таких потоков так много? Ведь, по идее, логирование должно происходить быстро. Очень похоже на то, что все эти потоки висят на какой-то точке синхронизации или критической секции и чего-то ждут. Давайте зайдем на ReferenceSource и посмотрим исходный код метода TraceEvent.

Открываем исходники и действительно видим в методе TraceEvent оператор lock:

Мы предположили, что из-за постоянной синхронизации и логирования накапливается большое количество вызовов методов TraceEvent, ждущих освобождения TraceInternal.critSec. Хм, ну допустим. Однако это пока не объясняет, почему сервер не может ответить клиенту. Посмотрим еще раз в дамп файл сервера и заметим один одинокий поток, который висит на методе DiagnosticsConfiguration.Initialize:

В данный метод мы попадаем из метода NegotiateStream.AuthenticateAsServer, выполняющего проверку подлинности со стороны сервера в соединении клиент-сервер:

В нашем случае клиент-серверное взаимодействие происходит с помощью WCF. Плюс напоминаю, что клиент ждет ответ от сервера. По этому стеку очень похоже, что метод DiagnosticsConfiguration.Initialize был вызван при запросе от клиента и теперь висит и ждет. Хм... а давайте-ка зайдем в его исходный код:

И тут мы замечаем, что в данном методе имеется критическая секция, да еще и на ту же самую переменную. Посмотрев, что вообще такое этот critSec, увидим следующее:

Собственно, у нас уже есть достаточно информации, чтобы подвести итоги.

Интересный факт. Изучая просторы интернета в поисках информации про данную проблему с TraceEvent была обнаружена интересная тема на GitHub. Она немного о другом, но есть один занимательный комментарий от сотрудника компании Microsoft:

"Also one of the locks, TraceInternal.critSec, is only present if the TraceListener asks for it. Generally speaking such 'global' locks are not a good idea for a high performance logging system (indeed we don't recommend TraceSource for high performance logging at all, it is really there only for compatibility reasons)".

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

Итоги изучения дампов

Итак, что мы имеем:

  1. Клиент общается с сервером с помощью фреймворка WCF.

  2. Клиент не может получить ответа от сервера. После 10 минут ожидания он падает по тайм-ауту.

  3. На сервере висит множество потоков на методе TraceEvent и всего один - на Initialize.

  4. Оба метода зависят от одной и той же переменной в критической секции, притом это статическое поле.

  5. Потоки, в которых выполняется метод TraceEvent, бесконечно появляются и из-за lock не могут быстро сделать свое дело и исчезнуть. Тем самым они долго не отпускают объект в lock.

  6. Метод Initialize возникает при попытке клиента завершить работу сервера и висит бесконечно на lock.

Из этого можно сделать вывод, что сервер получил команду завершения от клиента. Чтобы начать выполнять метод остановки работы сервера, необходимо установить соединение и выполнить метод Initialize. Данный метод не может выполниться из-за того, что объект в критической секции держат методы TraceEvent, которые в этот момент выполняются на сервере. Появление новых TraceEvent'ов не прекратится, потому что сервер продолжает работать и отлавливать новые 'мусорные' процессы. Получается, что клиент никогда не получит ответа от сервера, потому что сервер бесконечно логирует отловленные процессы с помощью TraceEvent. Проблема найдена!

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

Теперь остается только воспроизвести и починить проблему.

Воспроизведение проблемы

Само воспроизведение получается крайне простое. Все, что нам нужно, это сделать так, чтобы сервер постоянно что-то логировал. Потому создаем метод с говорящим названием CrazyLogging, который и будет это делать:

private void CrazyLogging(){  for (var i = 0; i < 30; i++)  {    var j = i;    new Thread(new ThreadStart(() =>    {      while (!Program.isStopMonitor)        Logger.TraceEvent(TraceEventType.Error, 0, j.ToString());    })).Start();  }}

За работу сервера у нас отвечает метод Trace, поэтому добавляем наше логирование в него. Например, вот сюда:

public void Trace(){  ListenersInitialization();  CrazyLogging();  ....}

Готово. Запускаем сервер (я буду это делать с помощью Visual Studio 2019), приостанавливаем секунд через 5 процесс и смотрим что у нас там с потоками:

Отлично! Теперь запускаем клиент (TestTraceSource.exe analyze), который должен установить связь с сервером и остановить его работу.

Запустив, мы увидим, что анализ не начинается. Поэтому опять останавливаем потоки в Visual Studio и видим ту же самую картину из дамп файла сервера. А именно появился поток, который висит на методе DiagnosticsConfiguration.Initialize. Проблема воспроизведена.

Как же её чинить? Ну для начала стоит сказать, что TraceSource это класс, который предоставляет набор методов и свойств, позволяющих приложениям делать трассировку выполнения кода и связывать сообщения трассировки с их источником. Используем мы его потому, что сервер может быть запущен не приаттаченным к консоли, и консольное логирование будет бессмысленно. В этом случае мы логировали всё в Event'ы операционной системы с помощью метода TraceSource.TraceEvent.

Проблему мы "решили" следующим образом. По умолчанию теперь вся информация логируется в консоль, используя метод Console.WriteLine. В большинстве случаев, даже если эта логируемая информация и будет потеряна из-за не приаттаченной консоли, она всё равно не требуется для решения задач в работе утилиты, а проблема исчезла. Плюс изменения заняли какие-то минуты. Однако мы оставили возможность старого способа логирования с помощью специального флага enableLogger.

Код, воспроизводящий проблему

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

Чтобы запустить имитирование работы сервера, запустите .exe с флагом trace. Чтобы запустить клиент, воспользуйтесь флагом analyze.

**Примечание: **количество потоков в методе CrazyLogging следует подбирать индивидуально. Если проблема у вас не воспроизводится, то попробуйте поиграться с этим значением. Также можете запустить данный проект в Visual Studio в режиме отладки.

Точка входа в программу:

using System.Linq;namespace TestTraceSource{  class Program  {    public static bool isStopMonitor = false;    static void Main(string[] args)    {      if (!args.Any())        return;      if (args[0] == "trace")      {        Server server = new Server();        server.Trace();      }      if (args[0] == "analyze")      {        Client client = new Client();        client.FinishMonitor();      }    }    }}

Сервер:

using System;using System.Diagnostics;using System.ServiceModel;using System.Threading;namespace TestTraceSource{  class Server  {    private static TraceSource Logger;    public void Trace()    {      ListenersInitialization();      CrazyLogging();      using (ServiceHost host = new ServiceHost(                          typeof(TestTraceContract),                           new Uri[]{new Uri(PipeCredentials.PipeRoot)}))      {        host.AddServiceEndpoint(typeof(IContract),                                 new NetNamedPipeBinding(),                                 PipeCredentials.PipeName);        host.Open();        while (!Program.isStopMonitor)        {          // We catch all processes, process them, and so on        }        host.Close();      }      Console.WriteLine("Complited.");    }    private void ListenersInitialization()    {      Logger = new TraceSource("PVS-Studio CLMonitoring");      Logger.Switch.Level = SourceLevels.Verbose;      Logger.Listeners.Add(new ConsoleTraceListener());      String EventSourceName = "PVS-Studio CL Monitoring";      EventLog log = new EventLog();      log.Source = EventSourceName;      Logger.Listeners.Add(new EventLogTraceListener(log));    }    private void CrazyLogging()    {      for (var i = 0; i < 30; i++)      {        var j = i;        new Thread(new ThreadStart(() =>        {          var start = DateTime.Now;          while (!Program.isStopMonitor)            Logger.TraceEvent(TraceEventType.Error, 0, j.ToString());        })).Start();      }    }   }}

Клиент:

using System;using System.ServiceModel;namespace TestTraceSource{  class Client  {    public void FinishMonitor()    {      TestTraceContractCallback сallback = new TestTraceContractCallback();      var pipeFactory = new DuplexChannelFactory<IContract>(                                сallback,                                new NetNamedPipeBinding(),                                new EndpointAddress(PipeCredentials.PipeRoot                                                   + PipeCredentials.PipeName));      IContract pipeProxy = pipeFactory.CreateChannel();      pipeProxy.StopServer();      Console.WriteLine("Complited.");        }  }}

Прокси:

using System;using System.ServiceModel;namespace TestTraceSource{  class PipeCredentials  {    public const String PipeName = "PipeCLMonitoring";    public const String PipeRoot = "net.pipe://localhost/";    public const long MaxMessageSize = 500 * 1024 * 1024; //bytes  }  class TestTraceContractCallback : IContractCallback  {    public void JobComplete()    {      Console.WriteLine("Job Completed.");    }  }  [ServiceContract(SessionMode = SessionMode.Required,                    CallbackContract = typeof(IContractCallback))]  interface IContract  {    [OperationContract]    void StopServer();  }  interface IContractCallback  {    [OperationContract(IsOneWay = true)]    void JobComplete();  }  [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]  class TestTraceContract : IContract  {    public void StopServer()    {      Program.isStopMonitor = true;    }  }}

Вывод

Будьте осторожны со стандартным методом TraceSource.TraceEvent. Если у вас теоретически возможно частое использование данного метода в программе, то вы можете столкнуться с подобной проблемой. Особенно если у вас высоконагруженная система. Сами разработчики в таком случае не рекомендуют использовать всё, что связано с классом TraceSource. Если вы уже сталкивались с чем-то подобным, то не стесняйтесь рассказать об этом в комментариях.

Спасибо за просмотр. Незаметно рекламирую свой Twitter.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikolay Mironov. How WCF Shoots Itself in the Foot With TraceSource.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2023, personeltest.ru