C# Flashcards
LINQ
LINQ (Language-Integrated Query) - язык запросов к источнику данных (это может быть объект IEnumerable, DataSet, DbSet (EF Core), документ XML) независимо от типа источника.
Виды LINQ:
- LINQ to Objects: применяется для работы с массивами и коллекциями
- LINQ to Entities: используется при обращении к базам данных через технологию Entity Framework
- LINQ to XML: применяется при работе с файлами XML
- LINQ to DataSet: применяется при работе с объектом DataSet
- Parallel LINQ (PLINQ): используется для выполнения параллельных запросов
Два способа работы с коллекциями:
1) Операторы запросов LINQ:
var selectedPeople = from p in people
where p.ToUpper().StartsWith(“T”) //фильтрация по критерию
orderby p // упорядочиваем по возрастанию
select p;
2) Методы расширений LINQ:
var selectedPeople = people.Where(p => p.ToUpper().StartsWith(“T”)).OrderBy(p => p);
Отложенное и немедленное выполнение LINQ
LINQ-выражение не выполняется, пока не будет произведена итерация или перебор по выборке, например, в цикле foreach.
Операции с отложенным выполнением:
- все, кроме тех, что возвращают атомарное значение или один элемент или данные типов Array, List, Dictionary
PLINQ
PLINQ (Parallel LINQ) - позволяет выполнять обращения к коллекции в параллельном режиме, т.е. источник данных разделяется на сегменты, и каждый сегмент обрабатывается в отдельном потоке (при доступе к общему разделяемому состоянию в параллельных операциях будет неявно использоваться синхронизация, чтобы избежать взаимоблокировки доступа к этим общим ресурсам):
- работают с IEnumerable
- имеют отложенное выполнение
Методы PLINQ:
- AsParallel (позволяет распараллелить запрос к источнику данных)
- ForAll (позволяет обработать результаты в том же потоке, в котором они обрабатываются)
- AsOrdered (упорядочивание результата в соответствие с исходной последовательностью)
- AsUnordered (отключение включенного ранее упорядочивания)
- WithDegreeOfParallelism (Указывает максимальное количество процессоров)
- WithCancellation (Прерывание параллельной операции, в качестве параметра передается токенCancellationToken)
Оптимизация работы PLINQ запросов
PLINQ лучше всего себя показывает когда обработка каждого элемента в исходной коллекции не зависит от других элементов и отдельные делегаты не используют общее состояние.
Факторы, влияющие на производительность запросов PLINQ:
- Для небольших коллекций с не затратными функциями PLINQ не стоит использовать
- параллельные запросы работают быстрее на компьютерах, которые имеют больше ядер
- работает лучше всего, если есть возможность обрабатывать элементы исходной коллекции в любом порядке (без упорядочивания)
- В PLINQ можно включить буферизацию, чтобы возвращать результаты блоками или целиком после завершения работы - это уменьшает общее время выполнения
В каких случаях PLINQ выбирает последовательный режим:
PLINQ ищет операторы запросов или сочетания операторов, которые обычно вызывают выполнение запроса более медленно в параллельном режиме - и в таком случае переходит на последовательный режим:
- запросы с Take, TakeWhile, Skip, SkipWhile (в которых индексы исходной последовательности не сохраняют исходный порядок)
- Запросы с Concat, Reverse
TPL Parallel
Parallel - класс библиотеки TPL, предназначенный для упрощения параллельного выполнения кода, с методами:
- Parallel.Invoke (параллельно выполняет задачи):
Parallel.Invoke(
Print,
() =>
{
Console.WriteLine($”Выполняется задача {Task.CurrentId}”);
Thread.Sleep(3000);
},
() => Square(5)
);
- Parallel.For (позволяет выполнять итерации цикла параллельно)
- Parallel.ForEach (осуществляет параллельный перебор коллекции):
ParallelLoopResult result = Parallel.ForEach<int>(
new List<int>() { 1, 3, 5, 8 },
Square
);</int></int>
Когда следует использовать Parallel, а когда PLINQ
1) Если важен порядок элементов, тогда PLINQ (т.к. есть встроенный метод AsOrdered)
2) Если работа со стримами (получаем элементы коллекции на каждой итерации foreach), тогда PLINQ:
- нет избыточности хранения данных в памяти
- есть внутренние буферы и можно настраивать их размер
3) Если нужно работать с локальными переменными потока, тогда Parallel
Многопоточность
Поток - часть программы (процесса) которая может выполняться одновременно с остальными, поток завершается, когда завершится функция потока.
Пул потоков - склад запасных потоков (создаются заранее), когда нужен поток берем из пула, когда поток завершается, он возвращается в пул, можно настроить количество потоков в пуле.
Синхронизация потоков
Синхронизация потоков - механизм предотвращения состояния гонки потоков и конфликтов при доступе к общему состоянию.
Объекты синхронизации потоков:
- Mutex (Обеспечивает поочерёдный доступ к объекту)
- Semaphore (Обеспечивает поочерёдный доступ к объекту для нескольких потоков сразу)
- События:
- ManualResetEvent
- AutoResetEvent
- CountdownEvent
- Monitor (Критическая секция - область кода, которую может выполнять лишь 1 поток одновременно)
- Interlocked (Примитивные атомарные операции):
Interlocked.Increment(ref num);
Mutex
Mutex - системный объект, который может принадлежать только одному потоку единовременно, может быть рекурсивным (т.е. поток, владеющим мьютексом, может брать его несколько раз - и столько же раз освободить его), освободить мьютекс может только поток, владеющий им:
- mutex.WaitOne( ) //Получить контроль над мьютексом. Подождать при необходимости
- mutex.ReleaseOne( ) //Освободить мьютекс - он переходит в сигнальное состояние
Semaphore
Semaphore - системный объект синхронизации, позволяющий ограничить количество потоков, которые имеют доступ к общим ресурсам (типо счетчик (мьютекс - это счетчик из 1 объекта
)), семафоры могут быть рекурсивными, освободить семафор можно из любого потока):
- new Semaphore (int current, int max) //Создать семафор в определённом состоянии с заданным лимитом
- semaphore.WaitOne( ) //получить контроль над одним из N объектов
- semaphore.Release( ) //освободить один из N объектов
SemaphoreSlim - упрощенная альтернатива Semaphore, не использует семафоры ядра Windows.
События синхронизации
1) ManualResetEvent (событие с ручным сбросом):
- WaitOne()
- Дождаться сигнального состояния. Текущий поток блокируется в ожидании
- Set()
- Перевести событие в сигнальное состояние
- Reset()
- Вернуть событие в барьерное состояние
2) AutoResetEvent (Событие с автоматическим сбросом):
- WaitOne()
- Дождаться сигнального состояния. Текущий поток блокируется в ожидании
- Set()
- Перевести событие в сигнальное состояние (один поток разблокируется и войдёт, а остальные продолжат ожидать. Событие тут же вернётся в барьерное состояние).
3) CountdownEvent (переходит в сигнальное состояние, когда счётчик достигает нуля):
- Wait()
- Надо дождаться сигнального состояния. Текущий поток блокируется в ожидании
- Siganl()
- Уменьшить счётчик на 1
Monitor (Критическая секция)
Критическая секция - Участок кода, который должен выполняться не более чем 1 потоком одновременно.
Monitor - механизм синхронизации .NET, позволяет маркировать регионы кода:
- Monitor.Enter(object key)
- Маркирует начало критической секции. Берёт контроль над ресурсом. Блокируется при необходимости
- Monitor.TryEnter(object key)
- Осторожно пытается взять контроль над ключом. Возможно, не получается.
- Monitor.Exit(object key)
- Маркирует конец критической секции. Отдаёт ресурс
lock
lock - синтаксический сахар, исп. Monitor:
lock (объект-ключ) {
//Действия критической секции
}
TPL (Task Parallel Library)
TPL (Task Parallel Library) - библиотека классов для многопоточного программирования, представляет собой абстракцию над Thread, основан на пуле потоков.
Комбинирование задач:
- Последовательное соединение:
Task.Run (( ) => 5)
.ContinueWith (prevTask => prevTask.Result * 2))
.ContinueWith (prevTask => prevTask.Result * 2)
.Wait ( );
- Параллельное соединение (оборачивают группу Task-ов в один Task):
Task.WhenAll(tasks); //завершится, когда все задачи завершатся
Task.WhenAny(tasks); //завершится, когда хотя бы одна задача завершится
async/await
Цель паттерна async/await
— позволить писать асинхронный код в привычном, последовательном стиле:
- весь код после await неявно становится одним большим колбэком
- при await текущий (асинхронный) метод засыпает, а управление возвращается в вызывающий поток, поэтому он не блокируется при длительных операциях, а когда ожидаемая задача выполнится асинхронный метод просыпается и продолжает выполнение
async - модификатор, который применяется к методу, указывая, что метод является асинхронным и внутри него можно использовать await.
- возвращаемые значения: void, Task, Task<T>, ValueTask<T></T></T>
await - ключевое слово, используется для приостановки выполнения асинхронного метода до тех пор, пока асинхронная задача, которую оно сопровождает, не будет завершена, не блокирует поток, на котором он был вызван, позволяя другим операциям выполняться в этом потоке. После завершения асинхронной задачи выполнение метода продолжается с точки, где оно было приостановлено.
Tсли этот асинхронный метод вызывается очень часто и не всегда нужно делать длительную операцию, то нужно использовать структуру ValueTask в качестве возвращаемого значения, чтоб компилятор не создавал машину состояний для этого асинхронного метода.
Абстрактный класс vs Интерфейс
Абстрактный класс и интерфейс - это два механизма, которые обеспечивают наследование и полиморфизм, но они используются по-разному и для разных целей.
Абстрактный класс - класс, от которого нельзя создать экземпляр напрямую, предназначен для описания общего поведения и состояния своих подклассов:
- может содержать как абстрактные методы (без реализации), так и методы с реализацией
- наследники обязаны реализовать все абстрактные методы
- класс может наследовать только от одного класса
- могут иметь конструкторы и деструкторы
- можно использовать различные модификаторы доступа
- Абстрактный класс используется, когда классы-наследники имеют много общего и должны наследовать некоторую реализацию
Интерфейс - определяет контракт, который классы или структуры могут реализовывать:
- не содержит реализации
- Класс или структура, реализующие интерфейс, должны предоставить реализацию для всех его членов
- класс может реализовывать несколько интерфейсов, что обеспечивает форму множественного наследования
- все члены по умолчанию являются public
Делегат
Делегат — это тип, который безопасно инкапсулирует метод, подобно указателю на функцию в других языках программирования, но с проверкой типов во время компиляции.
Делегаты могут ссылаться на метод, который принимает параметры и возвращает значение. Они используются для реализации обратных вызовов и событий, а также для определения пользовательских операций, которые могут быть выполнены методом, принимаемым в качестве параметра, позволяют абстрагироваться от конкретных методов, передавая вместо этого ссылку на метод, что делает код более гибким и масштабируемым.
// Определение делегата
public delegate int Operation(int x, int y);
// Создание экземпляра делегата, ссылающегося на метод Add
Operation op = Add;
// Вызов метода через делегат
int result = op(5, 5);
// Делегат теперь ссылается на метод Subtract
op = Subtract;
Сборка мусора (Garbage Collection, GC)
Сборка мусора (Garbage Collection, GC) - автоматически управляет выделением и освобождением памяти в управляемом куче.
Сборка мусора инициируется CLR автоматически в следующих ситуациях:
1) Недостаток памяти
2) Достигнут порог выделения памяти поколения объектов
3) Вызов GC.Collect()
4) Завершение работы приложения
5) Низкий уровень памяти в системе
string VS StringBuilder
string - класс, представляет неизменяемые строки (при каждой операции, изменяющей строку, создается новый экземпляр).
StringBuilder - класс для создания и модификации строк без необходимости каждый раз создавать новый экземпляр, поддерживает изменяемый буфер для хранения символов, что позволяет выполнять операции, такие как добавление, удаление и вставка символов, с меньшими накладными расходами по памяти и производительности:
StringBuilder sb = new StringBuilder(“Hello”);
sb.Append(“, World!”); // Просто добавляет “, World!” к существующему буферу
Лямбда-выражения в С#
Лямбда-выражения - сокращённый способ записи анонимных методов, то есть методов без имени, особенно полезны в LINQ:
() => Console.WriteLine(“Привет, мир!”);
x => x * x; // Возвращает квадрат x
(x, y) => x + y; // Складывает x и y
Middleware (ASP.NET core)
Middleware - программные компоненты, которые выполняются при каждом запросе к приложению и обрабатываются в порядке добавления в конвейер обработки запроса, могут выполнять различные задачи, такие как аутентификация, логирование, обработка ошибок, управление сессиями
- компоненты middleware добавляются в конвейер с помощью метода Use
Inversion of Control (IoC) и Dependency Injection (DI)
Inversion of Control (IoC) - принцип программирования, при котором управление потоком программы передаётся из пользовательского кода во внешнюю библиотеку или фреймворк.
Dependency Injection - конкретный способ реализации IoC, при котором создание объектов и управление их зависимостями не осуществляется самими объектами, а делегируется внешнему компоненту (например, IoC-контейнеру). Вместо того чтобы компоненты создавали нужные им зависимости самостоятельно, они получают их извне. Это позволяет сделать код более модульным, упрощает замену компонентов системы и их тестирование, поскольку зависимости можно легко подменять, например, моками (mock) в тестах.
является ли List потокобезопасной коллекцией?
нет, можно сделать потокобезопасную реализацию, обернув все операции в lock
В чем различие между операцией cast (приведения типов) и оператором as?
В случае ошибки cast выбрасывает исключение InvalidCastException, а оператор as возвращает null.