11 заметок с тегом

c#

Асинхронное программирование в C#

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

Что такое асинхронность?

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

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

TAP

С приходом .NET Framework 4.0 появился новый способ для работы с асинхронностью — TAP. По сути, это набор классов, которые предоставляют более удобный интерфейс для работы с асинхронным кодом. Но мы всё ещё можем использовать старые подходы: APM и EAP. Но Майкрософт советует использовать TAP.

Центральным классов в TAP является Task. Он описывает отдельную задачу, выполнение которой может завершиться в какой-то момент. 

Детальнее про подходы работы с многопоточностью.

Как создать задачу?

Фреймворк предоставляет множество способов для создания и запуска задач, а также оборачивания старого кода (APM и EAP).

Фабрики запущенных задач

Task.Run(Action/Func)
Task.Factory.StartNew(Action/Func)

Фабрики завершенных задач

Task.FromResult(Result)
Task.FromCanceled(Result)
Task.FromException(Result)
Task.CompletedTask

Конструктор

var task = new Task(Action/Func)
task.Start()

Фабрики для оборачивания в таски

Task.Factory.FromAsync(APM pattern)
TaskCompletionSource(EAP, APM, etc)

Класс TaskScheduler

Класс, который содержит стратегию запуска и планирования задач Task. Существуют две стандартные реализации: ThreadPoolTaskScheduler и SynchronizationContextTaskScheduler. Первая запускает задачи с помощью пула потоков, а вторая — использует текущий контекст синхронизации.

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

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

Контекст синхронизации при работе с Task и Thread

Комбинирование задач

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

Task.WaitAll();
Task.WaitAny();
Task.WhenAll();
Task.WhenAny();

Но нужно быть внимательным, так как WaitAll и WaitAny являются блокирующими операциями. Вместо них желательно использовать WhenAll и WhenAny.

Как получить результат?

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

Блокирующее получение результата:

.Result
.GetAwaiter().GetResult() - //лучше не использовать так как этот метод предназначен для компилятора, а не для нас.
.Wait()

Асинхронное:

await

Что такое sync over async deadlock?

Продолжение задач

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

Пример:

var task = Task.Run(() => Console.WriteLine("Async task"));
var nextTask = task.ContinueWith((prevTask) => {
	Console.WriteLine("Continuation");
});
nextTask.ContinueWith((prevTask) => {
	Console.WriteLine("Last Continuation");
});

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

Официальная документация

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

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

Обработка исключения при использовании await:

try
{
	await Task.Run(() => { throw new Exception(); });
}
catch (Exception e)
{
	// 
}

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

Task.Run(() => { throw new Exception(); })
	.ContinueWith((prev) => {
		if (prev.IsFaulted)
		{
			//
		}
	});

Task.Run(() => { throw new Exception(); })
	.ContinueWith((prev) => {
		
	}, TaskCreationOptions.OnlyOnfaulted);

Стоит избегать асинхронных методов, которые ничего не возвращают (async void). Так как при возникновении исключения внутри метода, мы просто не сможем его обработать.

Комбинаторы задач также по-разному предоставляют исключения:

  • Task.WaitAll() — вернёт AggregateException;
  • Task.WhenAll() — вернёт только первое исключение;

Отмена задач

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

async/await

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

Что делает async?

  • Создаёт машину состояний, которая обрабатывает все продолжения и синхронизации.
  • Разрешает использовать await.
  • Позволяет передавать вверх по стеку результат и исключения, используя Task.

Что делает await?

  • Позволяет не блокирующее ожидать результат.
  • Запуск продолжения в нужном потоке.
  • Возвращает результат или исключение.

Заблуждения по поводу await

  • Запускает операцию асинхронно.
  • Являться синтаксическим сахаром над Task.ContinueWtih.
  • Обязательно запускает продолжение в новый поток.
  • Всегда работает асинхронно.

Советы

  • Не используйте комбинацию async void — вы не сможете отловить исключение, а также ожидать выполнение метода.
  • Методы помеченные как async должны внутри себя содержать await.
  • Старайтесь вежде использовать не блокирующее ожидание await вместо блокирующего.
  • Для выполнения продолжительных операций используйте Task.Factory.StartNew вместе с TaskCreationOptions.LognRunning.
  • Используйте await t.ConfigureAwait(false) для библиотечного кода.
  • Не используйте совместно await и ContinueWith, так как они ничего не знают друг о друге.

Что осталось за кадром?

  • Асинхронные потоки
  • ValueTask
  • Progress

Ссылки

 Нет комментариев    52   23 дн   .net   c#

Лучшее объяснения работы семафора

A semaphore is like a nightclub: it has a certain capacity, enforced by a bouncer. Once it’s full, no more people can enter, and a queue builds up outside. Then, for each person that leaves, one person enters from the head of the queue. The constructor requires a minimum of two arguments: the number of places currently available in the nightclub and the club’s total capacity.

Алгоритм поиска Ли

Алгоритм поиска пути в планарном графе. Зачастую используется в схемотехнике и в играх (стратегиях) для поиска кратчайшего пути.

Алгоритм состоит из 3 шагов:

  1. Инициализация
  2. Распространение волны
  3. Восстановление пути

Также есть 2 способа поиска пути: ортогональный и ортогонально-диагональный. Ниже на скриншотах можно увидеть работу этих двух способов.

Ортогональный поиск.
Ортогонально-диагональный поиск.

Псевдокод

Взято из Википедии.

Инициализация

Пометить стартовую ячейку 
d := 0

Распространение волны

ЦИКЛ
  ДЛЯ каждой ячейки loc, помеченной числом d
    пометить все соседние свободные непомеченные ячейки числом d + 1
  КЦ
  d := d + 1
ПОКА (финишная ячейка не помечена) И (есть возможность распространения волны)

Восстановление пути

ЕСЛИ финишная ячейка помечена
ТО
  перейти в финишную ячейку
  ЦИКЛ
    выбрать среди соседних ячейку, помеченную числом на 1 меньше числа в текущей ячейке
    перейти в выбранную ячейку и добавить её к пути
  ПОКА текущая ячейка — не стартовая
  ВОЗВРАТ путь найден
ИНАЧЕ
  ВОЗВРАТ путь не найден

Пример кода, который реализует алгоритм и выводит на экран путь (C#).

Правила использования AutoMapper в .NET

Оригинал статьи: https://bool.dev/blog/detail/pravila-ispolzovaniya-automapper-v-net

Конфигурация
✔ Используйте инициализировать AutoMapper один раз в Mapper.Initialize в AppDomain startup в устаревшем ASP.NET
✔ Используйте использовать пакет AutoMapper.Extensions.Microsoft.DependencyInjection в ASP.NET Core с services.AddAutoMapper(assembly[])
✔ Используйте конфигурацию маппингов в профайле
✔ Рассмотрите организацию классов профайлов близко к типам назначения, которые они настраивают (иными словами в UserProfile должны быть маппинги классов связанных с юзером)
✔ Рассмотрите использование параметров конфигурации поддерживаемых LINQ, по сравнению с параметрами, не поддерживаемыми LINQ
Х Избегайте before/after map конфигурацию
Х Не размещайте логику которая строго не относится к поведению при маппинге данных
Х Не используйте MapFrom, когда элемент и так может быть автоматически смаплен
Х Не используйте AutoMapper, за исключением случаев, когда тип назначения представляет собой уплощенное подмножество свойств типа источника
Х Не используйте AutoMapper для поддержки сложной многоуровневой архитектуры
Х Избегайте использования AutoMapper, когда у вас значительный процент конфигурации прописан в форме Ignore или MapFrom
Х Не используйте доступ к статичному Mapper классу внутри профайла
Х Не используйте DI контейнер для регистрации всех профилей
Х Не используйте инджект зависимостей в профайлы
Х Не используйте CreateMap на каждый запрос
Х Не используйте inline maps

Моделирование
✔ Используйте не большие DTO
✔ Необходимо создавать внутренние типы в DTO для типов элементов, которые не могут быть сведены
✔ Следует помещать общие простые «computed property» в source модель
✔ Следует помещать «computed property» которые специфичны для dest модели в dest модель
Х Избегайте совмесное использование DTO между маппингами
Х Не создавайте DTO с «circular» ассоциациями
Х Избегайте изменения имен членов DTO для управления сереализацией

Выполнение
✔ Рассмотрите возможность использования query projection поверх in-memory mapping’a​
✔ Необходимо использовать mapping опции для runtime-resolved значений в проекциях.
✔ Используйте опции mappinga для разрешения контекстуализированных сервисов в in-memory mapping’e
Х Не абстрагируйте и не инкапсулируйте mapping за интерфейсом

Коллекции в .NET

Сегодня мы рассмотрим следующие интерфейсы:

  • IEnumerable
  • IQueryable
  • ICollection
  • IList
  • ISet
  • IDictionary
  • Queue и Stack

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

IEnumerable

Данный интерфейс является базовым для всех коллекций. В нем объявлен только один метод, который позволяет обходить элементы коллекции в цикле. Нельзя изменить данные из IEnumerable.

IQueryable

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

Разница между IQueryable и IEnumerable

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

Когда что использовать?

IEnumerable IQueryable
Может двигаться только вперед по коллекции, он не может идти назад может двигаться только вперед по коллекции, он не может идти назад
Хорошо подходит для работы с данными в памяти (списки, массивы) Лучше работает с запросами к базе данных (вне памяти)
Подходит для LINQ to Object и LINQ to XML Подходит для LINQ to SQL
Поддерживает отложенное выполнение (Lazy Evaluation) Поддерживает отложенное выполнение (Lazy Evaluation)
Не поддерживает произвольные запросы Поддерживает произвольные запросы (используя CreateQuery и метод Execute)
Не поддерживает ленивую загрузку (lazy loading) Поддерживает ленивую загрузку (lazy loading)
Методы расширения, работающие с IEnumerable принимают функциональные объекты Методы расширения, работающие с IQueryable принимают объекты выражения (expression tree)

ICollection

Этот интерфейс содержит методы, которые позволяют манипулировать элементами коллекции. Также содержит свойство Count.

IList

Расширяет коллекции методами поиска по индексу и индексатором.

ISet

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

ISet<int> set = new HashSet<int> { 1, 2, 3, 4, 5 };
 
set.UnionWith(new[] { 5, 6 });              // set = { 1, 2, 3, 4, 5, 6 }
set.IntersectWith(new[] { 3, 4, 5, 6, 7 }); // set = { 3, 4, 5, 6 }
set.ExceptWith(new[] { 6, 7 });             // set = { 3, 4, 5 }
set.SymmetricExceptWith(new[] { 4, 5, 6 }); // set = { 3, 6 }

Остальные только выполняют проверки над коллекцией и возвращают логическое значение (bool):

ISet<int> set = new HashSet<int> { 1, 2, 3, 4, 5 };
 
var isSubset = set.IsSubsetOf(new[] { 1, 2, 3, 4, 5 }); // = true
var isProperSubset = set.IsProperSubsetOf(new[] { 1, 2, 3, 4, 5 }); // = false
var isSuperset = set.IsSupersetOf(new[] { 1, 2, 3, 4, 5 }); // = true
var isProperSuperset = set.IsProperSupersetOf(new[] { 1, 2, 3, 4, 5 }); // = false
var equals = set.SetEquals(new[] { 1, 2, 3, 4, 5 }); // = true
var overlaps = set.Overlaps(new[] { 5, 6 }); // = true

IDictionary

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

Реализуют его следующие классы:

  1. Dictionary — обычный словарь.
  2. SortedDictionary — отсортированный список, реализован как словарь. Быстрее вставляет и удаляет элементы.
  3. SortedList — отсортированный список, который основан на массиве. Использует меньше памяти, чем SortedDictionary.

Queue и Stack

Класс Queue реализует принцип FIFO (first in, first out) — первым пришел, первым вышел.
Класс Stack похож на класс Queue, но он реализует принцип LIFO (последний пришел, первый вышел). Единственный элемент, который непосредственно доступен в этой коллекции, — это тот, который был добавлен совсем недавно.

Потокобезопасность

Обычные generic классы в Base Class Library имеют один очень важный недостаток, если вам нужно использовать их в многопоточных приложениях: они не являются поточно-ориентированными или по крайней мере, не полностью потокобезопасные.

Base Class Library содержит класс ReaderWriterLockSlim, который можно использовать для реализации этой конкретной функции относительно простым способом. Содержит 2 метода:

  1. EnterReadLock и ExitReadLock
  2. EnterWriteLock и ExitWriteLock

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

Immutable Collections

Immutable (неизменяемые) коллекции не включены в библиотеку базовых классов. Чтобы использовать их в проекте должен быть установлен пакет NuGet System.Collections.Immutable.

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

Ссылки

  1. https://bool.dev/blog/detail/sravnenie-kollektsiy-v-net
  2. https://bool.dev/blog/detail/kak-pravilno-vybrat-net-kollektsiyu

Первый проект

Нашел в старой переписке скриншоты игры, которую я делал когда только изучал программирование на C#. Игру делал на Unity в сеттинге сталкера. Дальше одной локации и простенького управления не зашло. Жаль что видео не сохранились.

Это был 2014 год.

Также пробовал моделировать в 3ds Max.

Разработка на C# с помощью Visual Studio Code

Пару недель назад решил перейти на VS Code в качестве основной IDE для .NET приложений. Составил список плагинов, которые использую каждый день.

  1. C#
  2. vscode-solution-explorer — позволяет работать с файлами .sln. Добавляет для этого дополнительное меню.
  3. Visual Studio IntelliCode
  4. PowerShell — отличное расширение, которое полностью заменяет PowerShell ISE.
  5. GitLens — Git supercharged
  6. Code Spell Checker
  7. C# Extension
  8. .NET Core Test Explorer
  9. Bookmarks

CancellationToken в C#

В .NET есть механизм, который позволяет отменять асинхронные операции. Состоит он из 3 элементов:

  1. CancellationTokenSource — создает маркеры отмены, которые можно получить из свойства Token и обрабатывает запросы на отмену.
  2. CancellationToken — сам маркер, позволяет отслеживать состояние отмены.
  3. OperationCanceledException — исключение, которое должно генерироваться в случае отмены асинхронной операции.

Как это работает?

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

Пример

public static void Main() 
{
    CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
    CancellationToken token = cancelTokenSource.Token;
    
    var task = Task.Run(() => Task(cancelTokenSource.Token), cancelTokenSource.Token);

    cancelTokenSource.Cancel();
}

public static void Task(CancellationToken cancellationToken) 
{
    while (true) 
    {
        // делает полезную работу.

        cancellationToken.ThrowIfCancellationRequested();
    }
    
}

Как работать в токеном?

Токен содержит набор полезных методов и полей:

  1. Register() — позволяет зарегистрировать callback функцию.
  2. IsCancellationRequested — свойство, возвращает true, если операция отменена.
  3. ThrowIfCancellationRequested() — метод, бросает исключения, в случае отмены задачи.
  4. CanBeCanceled — если свойство возвращает false, то запроса на отмену гарантированно не будет. Такой токен можно получить с помощью статического свойства CancellationTokenSource.None или вызвав конструктор с параметром false.

UPD: Также мы можем использовать CancellationToken для отмены задачи спустя какое-то время. Для этого достаточно вызвать CancelAfter передав ему количество миллисекунд спустя которые надо отменить задачу.

Источники:

  1. Задачи и отмена в .Net — tips & tricks
  2. Отмена задач и параллельных операций. CancellationToken
 Нет комментариев    95   10 мес   .net   c#

Кратко о WebSocket

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

Основной библиотекой для упрощения работы с сокетами от Microsoft является SignalR. Она берет на себя всю работу с данным протоколом и дает разработчику удобный интерфейс для работы.

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

SignalR предоставляет разработчикам две модели: постоянные подключения (Persistent Connection) и хабы (Hubs).
Постоянные подключения (Persistent Connection API) представляют разработчикам прямой доступ к низкоуровневому протоколу коммуникации. Подключения в этой модели представляют конечную точку, к которой подключаются клиенты, наподобие модели подключений в WCF.
Хабы же предоставляют протокол взаимодействия более высокого уровня. Они представляют верхний слой над Persistent Connection API и позволяют клиенту и серверу напрямую вызывать методы друг друга.

SignalR предоставляет следующие типы технологий для взаимодействия сервера и клиента:

  • WebSockets
  • Server-sent events
  • Forever Frames
  • Long polling

Картинка, которая иллюстрирует процесс установления связи по протоколу WebSocket

Следующий код был взят из websocket.org, но он демонстрирует базовое взаимодействие со стороны клиента с данным протоколом:

var wsUri = "ws://echo.websocket.org/";
var output;

function init()
{
  output = document.getElementById("output");
  testWebSocket();
}

function testWebSocket()
{
  websocket = new WebSocket(wsUri);
  websocket.onopen = function(evt) { onOpen(evt) };
  websocket.onclose = function(evt) { onClose(evt) };
  websocket.onmessage = function(evt) { onMessage(evt) };
  websocket.onerror = function(evt) { onError(evt) };
}

function onOpen(evt)
{
  writeToScreen("CONNECTED");
  doSend("WebSocket rocks");
}

function onClose(evt)
{
  writeToScreen("DISCONNECTED");
}

function onMessage(evt)
{
  writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
  websocket.close();
}

function onError(evt)
{
  writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
}

function doSend(message)
{
  writeToScreen("SENT: " + message);
  websocket.send(message);
}

function writeToScreen(message)
{
  var pre = document.createElement("p");
  pre.style.wordWrap = "break-word";
  pre.innerHTML = message;
  output.appendChild(pre);
}

window.addEventListener("load", init, false);

Ну и заключение, которое было к одной из статей и которое я не мог не вставить:

По мнению многих, WebSockets — едва ли не самое полезное изобретение после горячей воды. Когда вы ухватите суть WebSockets, вы будете удивляться, как это мир программного обеспечения мог обходиться без этих сокетов. WebSockets здорово пригодится в ряде приложений, но отнюдь не во всех. В любом приложении, где мгновенный обмен сообщениями является ключевым требованием, — имеет смысл серьезно подумать о создании WebSocket-сервера и клиентов (веб-приложения, мобильные и даже настольные программы). Еще одна группа приложений, которые получат выигрыш от внедрения WebSocket Protocol, — игры и каналы реального времени. Да, WebSockets — определенно лучшее, что было изобретено после горячей воды!

Ссылки:

  1. Отличный пример реализации клиента и сервера без использования сторонних библиотек
  2. SignalR Core. Первое приложение
  3. Офф. сайт
Ранее Ctrl + ↓