.NET Разработчик
6.51K subscribers
427 photos
2 videos
14 files
2.04K links
Дневник сертифицированного .NET разработчика.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День сто восьмидесятый. #ЗаметкиНаПолях
Многопоточность. Начало
Многопоточность позволяет увеличивать скорость реагирования приложения и, если приложение работает в многопроцессорной или многоядерной системе, скорость его исполнения.
Процесс — это набор ресурсов, используемый отдельным экземпляром приложения. Операционная система использует процессы для разделения исполняемых приложений. Поток — это основная единица, которой операционная система выделяет время процессора.

Когда используется многопоточность
Несколько потоков используются, чтобы повысить скорость отклика вашего приложения и использовать преимущества многопроцессорной или многоядерной системы для увеличения скорости приложения.
1. В приложении основной поток отвечает за пользовательский интерфейс и отвечает на действия пользователя. Рабочие потоки используются для выполнения ресурсоёмких операций, которые в противном случае заняли бы основной поток и сделали бы пользовательский интерфейс невосприимчивым. 2. Выделенный поток может использоваться для получения данных по сети или от внешнего устройства.
3. Если ваша программа выполняет операции, которые могут исполняться параллельно, общее время выполнения можно уменьшить, выполнив эти операции в отдельных потоках и запустив программу в многопроцессорной или многоядерной системе. В такой системе использование многопоточности может ускорить вычисления наряду с повышением отзывчивости приложения.
4. Потоки позволяют изолировать один код от другого, повышая надёжность приложения. В отдельном потоке можно запускать сторонний код, если нет уверенности в его надёжности и качестве.
5. Иногда код проще писать, если предположить, что он будет исполняться в собственном потоке. Однако при этом потребуются дополнительные ресурсы и, возможно, код синхронизации, что снизит эффективность кода.

Приоритеты потоков
Операционные системы с многозадачностью должны использовать некий алгоритм, определяющий порядок и продолжительность исполнения потоков. Windows называют многопоточной ОС с вытесняющей многозадачностью, т.к. каждый поток может быть остановлен в произвольный момент и вместо него выбран для исполнения другой. Чтобы управлять этим процессом, каждому потоку назначается уровень приоритета от 0 (самого низкого) до 31 (самого высокого). При выборе потока, который будет передан процессору, сначала рассматриваются потоки с самым высоким приоритетом. В Windows определены приоритеты процессов (приложений): Idle, Below Normal, Normal (по умолчанию), Above Normal, High и Realtime. Кроме того, поддерживаются следующие относительные приоритеты потоков: Idle, Lowest, Below Normal, Normal, Above Normal, Highest, Time-Critical. Соотношение между приоритетом процесса и относительным приоритетом потока и определяет итоговый уровень приоритета процесса. Например, по умолчанию оба приоритета установлены в Normal, и итоговый приоритет процесса – 8. Потоки с относительным приоритетом Time-Critical при любом приоритете процесса, кроме Realtime, имеют приоритет 15. В Realtime процессе потоки имеют приоритеты от 16 до 31.

Фоновые и активные потоки
Фоновые потоки приложения завершаются немедленно и без появления исключений при завершении активных потоков. Таким образом активные потоки используются для исполнения заданий, которые обязательно нужно завершить. Фоновые потоки можно оставить для некритичных операций. Потоки можно превращать из активного в фоновый и обратно. Основной поток приложения и все потоки, явно созданные через создание объекта Thread, являются активными. Потоки из пула потоков (ThreadPool) по умолчанию фоновые.
Особенности потоков
- В настоящее время потоки в CLR аналогичны Windows-потокам, но не исключено, что со временем эти понятия начнут различаться. Может появиться собственная концепция логического потока, не совпадающая с физическим потоком Windows.
- Для каждого потока выделяются память для хранения информации о потоке, кэша потока, пользовательского стека и т.п.
- В произвольный момент времени Windows передаёт процессору на исполнение один поток. Этот поток исполняется в течение некоторого временного интервала, называемого тактом. После завершение такта происходит переключение на другой поток. Это называется переключением контекста.
- Из-за затрат памяти и времени на переключение контекста, затрат на создание, управление и завершение потока, затрат на сборку мусора и т.п. использования потоков нужно по возможности избегать. Лучше прибегать к доступному в CLR пулу потоков (об этом далее).

Продолжение следует…

Источники:
- Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 25.
-
https://docs.microsoft.com/dotnet/standard/threading/threads-and-threading
День сто восемьдесят первый. #ЗаметкиНаПолях
Многопоточность.
2. Пул потоков в CLR
CLR способна управлять собственным пулом потоков. Для каждого процесса существует свой пул, используемый всеми доменами приложений в CLR. Пул потоков позволяет найти золотую середину в ситуации, когда малое количество потоков экономит ресурсы, а большое позволяет воспользоваться преимуществами многопроцессорных систем, а также многоядерных и гиперпотоковых процессоров. Он действует по эвристическому алгоритму, создавая или уничтожая потоки по мере необходимости.
В пуле различают два типа потоков: рабочие потоки (worker thread) и потоки ввода-вывода (I/O thread). Первые используются для асинхронных вычислительных операций, вторые служат для уведомления кода о завершении асинхронной операции ввода-вывода.
Для добавления в очередь пула потоков асинхронных вычислительных операций обычно вызывают один из методов QueueUserWorkItem класса ThreadPool. В метод передаётся делегат WaitCallback, а также может передаваться объект состояния state:
static void Main(string[] args)
{
Console.WriteLine("Основной поток: вызываем асинхронную операцию");
ThreadPool.QueueUserWorkItem(Compute, 5);
Console.WriteLine("Основной поток: выполняем другую работу...");
Thread.Sleep(10000);
Console.ReadLine();
}

private static void Compute(object state)
{
//Метод выполняется потоком из пула
Console.WriteLine($"В Compute: state={state}");
Thread.Sleep(1000);
}
После возвращения управления асинхронным методом, поток возвращается в пул и ожидает следующего задания.

Возможны ситуации, когда требуется явно создать поток, исполняющий конкретную вычислительную операцию:
- Поток требуется запустить с нестандартным приоритетом (хотя, этого делать не рекомендуется).
- Чтобы приложение не закрылось до завершения потоком задания, требуется, чтобы поток исполнялся в активном режиме. В примере выше по умолчанию метод Compute выполняется в фоновом потоке, поэтому приложение может прекратить работу перед тем, как завершится выполнение этого метода.
- Задания, связанные с критически важными вычислениями, которые могут выполняться долго, лучше не отдавать на откуп пула потоков, а выделить им отдельный поток.
- Если есть необходимость преждевременно завершить исполняющийся поток через Thread.Abort().

Продолжение следует…

Источник: Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 26.
👍1
День сто восемьдесят второй. #ЗаметкиНаПолях
Многопоточность
3. Скоординированная отмена
.NET предлагает стандартный шаблон операций отмены. Он называется скоординированным (cooperative), то есть необходима явная поддержка отмены операций. Это значит, что как код, выполняющий отменяемую операцию, так и код, пытающийся отменить операцию, должны использовать одинаковые типы.
В вызывающем коде создаётся объект CancellationTokenSource. C его помощью можно создать структуру CancellationToken, которая передаётся вызываемому коду. Для отмены операции вызывается метод Cancel объекта CancellationTokenSource.
Вот наиболее полезные члены структуры CancellationToken:
- bool IsCancellationRequested – вызываемый код может периодически обращаться к этому свойству, чтобы узнать, не запрошена ли отмена операции;
- void ThrowIfCancellationRequested() – метод используется в заданиях (Task) аналогично предыдущему свойству;
- CancellationToken None – статическое свойство, обозначающее отсутствие токена отмены, чтобы исключить возможность отмены операции;
- CancellationTokenRegistration Register(…) – метод используется для регистрации одного или нескольких делегатов обратного вызова, которые будут вызваны при отмене операции. Возвращаемая методом структура CancellationTokenRegistration содержит метод Dispose, который позволяет отменить регистрацию.

В примере ниже токен отмены передаётся методу Compute, внутри которого периодически проверяется свойство IsCancellationRequested. Когда пользователь нажимает Enter, вызывается отмена операции, и на следующей итерации цикла он прерывается:
static void Main(string[] args)
{
var cts = new CancellationTokenSource();
ThreadPool.QueueUserWorkItem(o => Compute(cts.Token, 1000));
Console.WriteLine("Нажмите <Enter> для отмены...");
Console.ReadLine();
cts.Cancel();
Console.ReadLine();
}

private static void Compute(CancellationToken token, int countTo)
{
for (int i = 0; i < countTo; i++)
{
if(token.IsCancellationRequested)
{
Console.WriteLine("Счёт отменён");
break;
}
Console.WriteLine(i);
Thread.Sleep(200);
}
Console.WriteLine("Счёт закончен");
}

Продолжение следует…

Источник: Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 26.
День сто восемьдесят третий. #юмор
18+
День сто восемьдесят четвёртый. #ЗаметкиНаПолях
Многопоточность.
4. Задания. Начало
Проблема с асинхронными вычислениями, запущенными с помощью метода ThreadPool.QueueUserWorkItem() состоит в том, что отсутствует встроенный механизм, позволяющий узнать о завершении операции и получить возвращаемое значение. Для решения этой проблемы используются задания. Следующие вызовы асинхронных операций аналогичны:
ThreadPool.QueueUserWorkItem(Compute, 5);
new Task(Compute, 5).Start();
Объекту Task передаётся делегат Action или Action<object> (в последнем случае следует также передать аргумент метода, как в примере выше). При желании также можно передать структуру CancellationToken для отмены задания. Допустим, метод Calc принимает целое число и производит вычисления. Тогда запустить его в новом задании можно следующим образом:
var t = new Task<int>((n) => Calc((int)n), 10000);
t.Start();
t.Wait();
Console.WriteLine($"Результат: " + t.Result);
Console.ReadLine();
Поток запускает задание и дожидается его выполнения при вызове метода Wait или свойства Result. При этом само задание может выполниться как в новом потоке (тогда поток, вызвавший метод Wait, блокируется), либо в том же потоке.

Отмена задания
Для отмены задания можно воспользоваться объектом CancellationTokenSource (см. https://t.iss.one/NetDeveloperDiary/219 ). Однако, в отличие от обычной асинхронной операции, для отмены задания нужно обратиться не к свойству IsCancellationRequested, а вызывать метод ThrowIfCancellationRequested() токена отмены. Это приводит к исключению OperationCancelledException, которое нужно перехватить в вызывающем коде. Причина в том, что задания возвращают результат. Поэтому, чтобы отличить законченное задание от незаконченного, используется исключение.
private static int Calc(CancellationToken ct, int n)
{

ct.ThrowIfCancellationRequested();

}

var cts = new CancellationTokenSource();
var t = new Task<int>(() => Calc(cts.Token, 10000), cts.Token);
t.Start();

// вызывается асинхронно
// к этому моменту задание может быть уже выполнено
cts.Cancel();

try
{
Console.WriteLine($"Результат: {t.Result}");
}
catch(AggregateException ex)
{
ex.Handle(x => x is OperationCanceledException);
Console.WriteLine("Операция отменена");
}
Все необработанные исключения, возникающие в задании, проглатываются в исполняющем потоке и сохраняются в коллекции InnerExceptions объекта AggregateException. Поэтому в вызывающем коде перехватывается именно исключение типа AggregateException. Далее с помощью метода Handle() можно отметить нужные нам исключения, как обработанные. В данном случае мы считаем все исключения типа OperationCanceledException обработанными. Если в коллекции после вызова метода Handle останутся необработанные исключения, они попадут в новый объект AggregateException.

Продолжение следует…

Источник: Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 26.
День сто восемьдесят пятый. #ЗаметкиНаПолях
Многопоточность.
5. Задания. Продолжение
Автоматический запуск задания по завершении предыдущего
Вызов метода Wait или свойства Result при незавершённом задании скорее всего приведёт к блокировке текущего потока и появлению в пуле нового потока. Чтобы этого не происходило, можно переписать код из предыдущих примеров так, чтобы результат выводился в новом задании, которое стартует после завершения предыдущего:
var t = new Task<int>((n) => Calc((int)n), 10000);
t.Start();
Task cwt = t.ContinueWith(task => Console.WriteLine($"Результат: {task.Result}"));
При этом поток, исполняющий этот код и ожидающий завершения каждого из этих заданий, не блокируется, а выполняет другую работу или возвращается в пул. Поскольку метод ContinueWith также возвращает Task, то и к нему, в свою очередь, можно применять Wait, Result или ContinueWith.
Кроме того, вызвать ContinueWith можно с флагами TaskContinuationOptions. Наиболее интересные из них:
- AttachedToParent – присоединяет задание к родительскому (родительское задание не закончится, пока не исполнятся все дочерние)
- ExecuteSynchronously – новое задание будет выполнено тем же потоком, что и предыдущее
- OnlyOnCanceled – выполнять, если предыдущее задание отменено
- OnlyOnFaulted – выполнять, если предыдущее задание привело к ошибке
- OnlyOnRanToCompletion – выполнять, если предыдущее задание завершилось успешно
- NotOnCanceled, NotOnFaulted, NotOnRanToCompletion – соответствующие отрицательные флаги.
Вывод результата в предыдущем примере можно переписать с этими флагами:
t.ContinueWith(task => 
Console.WriteLine($"Результат: {task.Result}"),
TaskContinuationOptions.OnlyOnRanToCompletion);
t.ContinueWith(task =>
Console.WriteLine($"Ошибка: {task.Exception}"),
TaskContinuationOptions.OnlyOnFaulted);
t.ContinueWith(task =>
Console.WriteLine("Операция отменена"),
TaskContinuationOptions.OnlyOnCanceled);

Фабрики заданий
Если необходим набор заданий в одном состоянии и с одинаковыми параметрами, можно создать фабрику заданий TaskFactory:
string[] files = null
string[] dirs = null;
string docsDir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);

var tf = new TaskFactory();
var tasks = new[]
{
tf.StartNew(() => files = Directory.GetFiles(docsDir)),
tf.StartNew(() => dirs = Directory.GetDirectories(docsDir))
};
tf.ContinueWhenAll(tasks, completed => {
Console.WriteLine("{0} содержит: ", docsDir);
Console.WriteLine(" {0} папок", dirs.Length);
Console.WriteLine(" {0} файлов", files.Length);
});
В коде выше создаётся новая фабрика заданий. Ей можно передать общий токен отмены, параметры создания и продолжения заданий и т.п. Далее задания создаются вызовом метода StartNew. Впоследствии можно добавить задание-продолжение по окончании всех или любого из заданий, вызвав соответственно ContinueWhenAll или ContinueWhenAny. В примере выше в фабрике заданий два параллельных задания подсчитывают количество папок и файлов в папке «Мои документы» текущего пользователя, а после завершения всех заданий результаты выводятся в консоль.

Продолжение следует…

Источники:
- Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 26.
-
https://docs.microsoft.com/dotnet/api/system.threading.tasks.task.factory
День сто восемьдесят шестой. #юмор
День сто восемьдесят седьмой. #ЗаметкиНаПолях
Многопоточность.
6. Класс Parallel
Для реализации наиболее популярных сценариев посредством заданий создан класс System.Threading.Tasks.Parallel. Например, следующий код вызывает метод DoWork последовательно:
for(int i = 0; i < 1000; i++) DoWork(i);
Вместо этого можно распределить обработку между несколькими потоками пула:
Parallel.For(0, 1000, i => DoWork(i));

Аналогично метод foreach
foreach(var item in collection) DoWork(item);
можно заменить
Parallel.ForEach(collection, item => DoWork(item));

Параллельно запустить несколько методов можно с помощью метода Invoke:
Parallel.Invoke(
() => Method1(),
() => Method2(),
() => Method3());

Особенности:
1. Вызывающий поток также принимает участие в обработке и не блокируется, за исключением случаев, когда он заканчивает выполнение работы, а остальные потоки ещё нет.
2. Если какая-либо операция выбрасывает необработанное исключение, то вызванный вами метод класса Parallel выбросит исключение AggregateException (см. https://t.iss.one/NetDeveloperDiary/221)
3. Использовать методы класса Parallel выгодно, лишь когда задания действительно могут выполняться параллельно, а также не обращаются к общим ресурсам.

Опции
Методам класса Parallel можно передать объект ParallelOptions со следующими свойствами:
- CancellationToken (токен отмены операции, по умолчанию CancellationToken.None)
- MaxDegreeOfParallelizm (максимальное количество рабочих элементов, по умолчанию -1 равно числу доступных процессоров)
- TaskScheduler – планировщик заданий.

Продолжение следует…

Источник: Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 26.
День сто восемьдесят восьмой.
C# In Depth
Еще полгода назад писал про эту книгу Джона Скита (Jon Skeet) и только недавно до неё «дошли руки». Ну, что сказать. Если Рихтер – мастрид для C# мидл-сениор разработчика, то Скит – это абсолютный мастрид после Рихтера. Признаюсь честно, я Рихтера читал дважды. После первого прочтения не осталось в голове почти ничего (практики не было тогда). А после второго в принципе бОльшая часть контента более-менее запомнилась, но осталась парочка серьёзных проблем. Во-первых, всё равно в голове получилась некоторая каша, и многие понятия остались существовать в каком-то вакууме. Не знаю, проблема ли в организации книги, либо сугубо моего восприятия, но факт остаётся фактом. Во-вторых, и это, наверное, главная проблема. Моё издание книги вышло в 2012 году. И даже последнее на данный момент издание 2013 года и охватывает C# 5. А сейчас уже выходит 8я(!!!) версия языка. Таким образом, несмотря на то, что книга невероятно полезна с теоретической точки зрения, она «страшно далека от реальности». Поэтому получается, что в книге есть главы на десятки страниц, например, про делегаты или асинхронные операции, но очень коротко описано то, как они действительно используются сейчас (лямбда-выражения, LINQ или async/await). То есть в знаниях получается «дыра» между хорошей теорией 6-летней давности и современными тенденциями разработки. Это не значит, что описанная теория совсем бесполезна. Но получается что-то вроде того, как один из наших преподавателей в вузе считал, что изучать программирование надо с ассемблера (Ну... как бы, да... но можно и нет :) )
И в этом смысле прочитать Скита именно ПОСЛЕ Рихтера, на мой взгляд, крайне полезно. В моём случае книга Скита – это 4е издание 2019 года, которое описывает самые последние изменения в языке (вплоть до краткого обзора 8й версии). Но самое главное, что все инструменты языка рассмотрены в хронологическом порядке: от первой версии до седьмой. Про каждый инструмент подробно описано: что это, когда возникло, почему возникло, что изменилось от версии к версии и где это применяется. Это позволяет расставить все ваши знания по полочкам и связать различные знания между собой. К примеру, большинство изменений в C#3 (неявное объявление переменных через var, инициализаторы объектов и коллекций, анонимные типы, методы расширения и т.п.) введены для того, чтобы использовать лямбда-выражения и LINQ. И вся книга построена по этому принципу.
Кроме того, в отличие от Рихтера, Скит пишет в очень простой, понятной и дружелюбной манере, поэтому книгу, даже по-английски, читаешь не через силу (потому, что надо), а с удовольствием, как набор постов в блоге.
Далее на канале периодически буду приводить выдержки из книги, которые показались наиболее интересными.

Источники:
- Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012.
- Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019.
День сто восемьдесят девятый. #ЗаметкиНаПолях
Многопоточность
7. Шаблоны Асинхронного Программирования
.NET предоставляет три шаблона для выполнения асинхронных операций:
1. Модель асинхронного программирования (APM), также называемый шаблоном IAsyncResult, является устаревшей моделью, использующей интерфейс IAsyncResult для обеспечения асинхронного поведения. В этом шаблоне для синхронных операций требуются методы Begin и End (например, BeginWrite и EndWrite для реализации асинхронной операции записи). Этот шаблон больше не рекомендуется для новых разработок. Например, рассмотрим метод Read, который считывает указанное количество данных в буфер, начиная с указанного смещения:
public class MyClass
{
public int Read(byte [] buffer, int offset, int count);
}
В APM нужно реализовать методы BeginRead и EndRead:
public class MyClass
{
public IAsyncResult BeginRead(
byte[] buffer, int offset, int count,
AsyncCallback callback, object state);
public int EndRead(IAsyncResult asyncResult);
}

2. Асинхронный шаблон на основе событий (EAP) является устаревшим шаблоном на основе событий для обеспечения асинхронного поведения. Для этого требуется метод с суффиксом Async и одно или несколько событий, делегатов обработчиков событий и типов, производных от EventArg. EAP был представлен в .NET Framework 2.0. Больше не рекомендуется для новых разработок. Для примера выше в EAP потребуется метод и событие:
public class MyClass
{
public void ReadAsync(byte[] buffer, int offset, int count);
public event ReadCompletedEventHandler ReadCompleted;
}

3. Асинхронный шаблон на основе задач (TAP) использует общий метод для инициирования и завершения асинхронной операции. TAP был представлен в .NET Framework 4. В настоящее время это рекомендуемый подход к асинхронному программированию в .NET. Языковую поддержку для TAP обеспечивают ключевые слова async и await в C#. Для примера выше в TAP потребуется реализация одного метода:
public class MyClass
{
public Task<int> ReadAsync(byte[] buffer, int offset, int count);
}

Продолжение следует…

Источники:
- Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 27.
-
https://docs.microsoft.com/ru-ru/dotnet/standard/asynchronous-programming-patterns/
День сто девяностый. #ЧтоНовенького
Предпросмотр Инструментов в Visual Studio
Обновлено окно Предпросмотр Инструментов (Preview Features) в разделе Инструменты > Параметры > Среда (Tools > Options > Environment). Новый вид предоставляет больше информации и возможность оставить отзыв о новых функциях. Пока они находятся в разработке, вы можете отключить любую из них, если столкнетесь с проблемами. Microsoft также рекомендует оставлять отзывы о любых проблемах, с которыми вы столкнётесь. Список на этой странице включают функции на ранней стадии разработки, которые влияют на существующую функциональность, те, которые еще дорабатываются, а также экспериментальные инструменты.
Поскольку разработка нового функционала идёт постоянно, список в окне Предпросмотр Инструментов будет изменяться в каждом выпуске Visual Studio, так как некоторые функции добавляются в продукт, а другие удаляются.

Источник: https://devblogs.microsoft.com/visualstudio/preview-features-in-visual-studio/
День сто девяносто первый. #ЗаметкиНаПолях
Многопоточность
8. В чем разница между асинхронным программированием и многопоточностью?
Эти понятия часто путают. Многие думают, что многопоточность и асинхронность - это одно и то же, но это не так. Обычно помогает аналогия. Вы готовите в ресторане. Приходит заказ на яйца и тосты.
- Синхронно: вы готовите яйца, затем вы готовите тост.
- Асинхронно, однопоточно: вы начинаете готовить яйца и устанавливаете таймер. Вы начинаете готовить тосты и устанавливаете таймер. Пока они готовятся, вы убираетесь на кухне. Когда таймеры выключаются, вы снимаете яйца с огня и тосты из тостера и подаете их.
- Асинхронно, многопоточно: вы нанимаете еще двух поваров, один для приготовления яиц и один для приготовления тостов. Теперь у вас есть проблема координации поваров, чтобы они не конфликтовали друг с другом на кухне при совместном использовании ресурсов. И вы должны заплатить им.

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

Источник: https://stackoverflow.com/questions/34680985/what-is-the-difference-between-asynchronous-programming-and-multithreading
День сто девяносто второй. #ЗаметкиНаПолях
Заметки по обобщённым типам
1. Инициализация и состояния обобщённых типов
Если выполнить следующий код:
Console.WriteLine(typeof(List<int>));
Console.WriteLine(typeof(List<string>));
На экран выведется следующее:
System.Collections.Generic.List`1[System.String]
System.Collections.Generic.List`1[System.Int32]
List`1 означает обобщённый тип List арности 1 (один параметр типа), а аргументы параметра типа указаны в квадратных скобках. Таким образом, List<int> и List<string> - это фактически разные типы, которые созданы из одного и того же определения обобщённого типа List<T>. Они называются закрытыми составными типами (closed, constructed type). Это относится не только к тому, как типы используются, но и к тому, как они инициализируются и как обрабатываются статические поля. Каждый закрытый составной тип инициализируется отдельно и имеет собственный независимый набор статических полей. Следующий листинг демонстрирует это с помощью простого обобщённого счетчика.
class GenericCounter<T>
{
private static int value;
static GenericCounter()
{
Console.WriteLine("Инициализация счётчика для {0}", typeof(T));
}
public static void Increment()
{
value++;
}
public static void Display()
{
Console.WriteLine("Счётчик для {0}: {1}", typeof(T), value);
}
}

GenericCounter<string>.Increment();
GenericCounter<string>.Increment();
GenericCounter<string>.Display();
GenericCounter<int>.Display();
GenericCounter<int>.Increment();
GenericCounter<int>.Display();

Вывод будет следующим:
Инициализация счётчика для System.String
Счётчик для System.String: 2
Инициализация счётчика для System.Int32
Счётчик для System.Int32: 0
Счётчик для System.Int32: 1
Из этого можно сделать два вывода:
1. Значение GenericCounter<string> не зависит от GenericCounter <int>.
2. Статический конструктор запускается дважды: один раз для каждого закрытого составного типа. Если бы статического конструктора не было, было бы меньше гарантий того, когда точно происходит инициализация каждого типа. Но можно однозначно сказать, что вы можете рассматривать GenericCounter<string> и GenericCounter<int> как независимые типы.

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
👍1
День сто девяносто третий. #ЗаметкиНаПолях
Заметки по обобщённым типам
2. Вывод типа из аргументов методов
Рассмотрим следующий код:
public static List<T> CopyAtMost<T>(List<T> input, int elements)
{
return input.Take(elements).ToList();
}

var numbers = new List<int>() { 1, 2, 3, 4 };
var firstTwo = CopyAtMost<int>(numbers, 2);

Вам нужен аргумент типа для вызова CopyAtMost, потому что метод имеет параметр типа. Но вам не нужно указывать этот тип аргумента явно. Можно переписать этот код следующим образом:
var firstTwo = CopyAtMost(numbers, 2);
Это точно такой же вызов метода с точки зрения IL, который сгенерирует компилятор. Но аргумент типа int указывать не нужно, компилятор сделал это для вас на основе аргумента для первого параметра метода. Вы используете аргумент List<int> в качестве значения параметра типа List<T>, поэтому T должно быть int. Вывод типа может использовать только аргументы, передаваемые методу, но не тип результата.
Хотя вывод типов применяется только к методам, его можно использовать для более простого создания экземпляров обобщённых типов. Например, рассмотрим семейство типов Tuple, состоящее из необобщённого статического класса Tuple и нескольких обобщённых классов: Tuple<T1>, Tuple<T1, T2>, Tuple<T1, T2, T3> и так далее (до 8). Статический класс имеет набор перегруженных фабричных методов Create:
public static Tuple<T1> Create<T1>(T1 item1)
{
return new Tuple<T1>(item1);
}
public static Tuple<T1, T2> Create<T1, T2>(T1 item1, T2 item2)
{
return new Tuple<T1, T2>(item1, item2);
}
и так далее…
Они выглядят бессмысленно тривиальными, но позволяют использовать вывод типа. То есть, вместо этого:
new Tuple<int, string, int>(10, "x", 20)
Можно написать:
Tuple.Create(10, "x", 20)
Это мощная техника, о которой полезно знать; как правило, её просто реализовать и она может сделать работу с обобщённым кодом намного приятнее.
Разработчики языка постоянно работают над усовершенствованием процесса вывода типа, и порой логика вывода становится достаточно сложной из-за наследований, перегрузок и необязательных параметров. Поэтому надо понимать, что иногда на практике выведение типа может приводить к неожиданным результатам. Например, если вам нужен Tuple<int, object, int>, то из предыдущего вызова Tuple.Create(10, "x", 20) вы его не получите. В этом случае можно использовать либо new Tuple<int, object, int>(10, "x", 20), либо приведение типа Tuple.Create(10, (object)"x", 20). Аналогично с null: Tuple.Create(null, 50) завершится неудачей, но Tuple.Create((string) null, 50) сработает.

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
День сто девяносто четвёртый. #юмор
Отладка.
Да, бывало и так)))
День сто девяносто пятый. #ИнтересныйКод
Ленивый итератор с помощью yield return
Следующий код показывает метод Fibonacci(), возвращающий бесконечную последовательность чисел Фибоначчи, и использование этого метода для вывода чисел до нужного предела:
static IEnumerable<int> Fibonacci()
{
int current = 0;
int next = 1;
// бесконечный цикл? Только если продолжать запрашивать значения
while (true)
{
yield return current;
int oldCurrent = current;
current = next;
next += oldCurrent;
}
}
static void Main()
{
foreach (var value in Fibonacci())
{
Console.WriteLine(value);
// условие остановки цикла
if (value > 1000) { break; }
}
}
Как реализовать что-то подобное без итераторов? Можно изменить метод для создания List<int> и заполнять его, пока не достигнут предел. Но этот список может быть большим, если предел велик, и почему метод, который знает детали последовательности Фибоначчи, также должен знать, когда остановиться? Предположим, иногда нужно остановиться по значению текущего числа, а иногда по количеству выведенных чисел или через некоторое время.
Можно избежать создания списка, печатая значение в цикле, но это делает метод Fibonacci() еще более тесно связанным с тем, что вы хотите сделать со значениями прямо сейчас. Что если вы хотите складывать значения, а не печатать их? Писать другой метод? Это ужасное нарушение разделения ответственности.
Решение через итератор - это именно то, что вам нужно: представление бесконечной последовательности и всё. Вызывающий код может выполнять итерацию по своему усмотрению (по крайней мере, до переполнения int) и использовать значения, как хочет.
Реализация последовательности Фибоначчи вручную проста. Не нужно сохранять много данных состояния (только два предыдущих числа), а управлять логикой выполнения довольно просто (здесь только один оператор yield return). Но если логика усложняется, реализация её в коде становится крайне непростой. Таким образом, использование итераторов с yield return – это мощный инструмент, сильно упрощающий разработку.
Кроме того, компилятор также достаточно умён, чтобы правильно обрабатывать блоки finally, что не так очевидно, как кажется (об этом далее).

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
День сто девяносто шестой. #ЗаметкиНаПолях
Обработка блоков finally в итераторах
Рассмотрим следующий код:
static IEnumerable<string> Iterator()
{
try
{
Console.WriteLine("Перед первым yield");
yield return "первый";
Console.WriteLine("После первого yield");
yield return "второй";
Console.WriteLine("После второго yield");
}
finally
{
Console.WriteLine("Внутри finally");
}
}
Когда будет выполнен блок finally:
- Если считать выполнение прерываемым на каждом вызове yield return, тогда логически они внутри блока try, и нет надобности выполнять блок finally каждый раз.
- Если считать, что на самом деле на каждом yield return вызывается метод MoveNext() итератора, то можно решить, что происходит выход из try, и тогда finally должен выполняться.
Выполним этот код:
foreach (string value in Iterator())
{
Console.WriteLine("Значение: {0}", value);
}
Вывод:
Перед первым yield
Значение: первый
После первого yield
Значение: второй
После второго yield
Внутри finally

Таким образом, блок finally выполнится только после окончания итерации. Это подходит под концепцию ленивого выполнения. Пока ничего сложного. Но что если мы прервём итерацию после первого значения? Выполнится ли блок finally?
На самом деле, и да, и нет. Если реализовать итератор вручную и вызвать MoveNext() один раз, то блок finally действительно никогда не будет выполнен. Однако, если использовать итератор внутри цикла foreach, как это чаще всего происходит, то компилятор использует скрытый блок using вокруг цикла. При выходе из блока using происходит вызов метода Dispose() итератора, и, соответственно, вызов всех блоков finally. Таким образом, результатом выполнения следующего кода:
foreach (string value in Iterator())
{
Console.WriteLine("Значение: {0}", value);
break;
}
будет:
Перед первым yield
Значение: первый
Внутри finally

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

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
День сто девяносто седьмой. #оффтоп
Иногда мне кажется, что самых рукожопых разработчиков мелкомягкие не увольняют, а отправляют в ссылку на проект скайпа.
День сто девяносто восьмой. #ЗаметкиНаПолях
Частичные методы
В C# 3 добавлена дополнительный функционал для частичных классов - частичные методы. Это методы, объявленные без тела в одной части, а затем опционально реализованные в другой части. Частичные методы неявно являются private, должны возвращать void и не иметь out параметров (можно использовать параметры ref). Во время компиляции сохраняются только частичные методы, имеющие реализации; если частичный метод не был реализован, он и все его вызовы удаляются. Это звучит странно, но позволяет автоматически сгенерированному коду предоставлять точки перехвата (hooks) для добавления дополнительной логики в коде, написанном вручную. Это может быть реально полезно. В примере ниже объявлены два частичных метода:
partial class PartialMethodsDemo
{
public PartialMethodsDemo()
{
OnConstruction();
}
public override string ToString()
{
string ret = "Original return value";
CustomizeToString(ref ret);
return ret;
}
partial void OnConstruction();
partial void CustomizeToString(ref string text);
}
Во втором файле частичного класса один из методов реализован, а другой – нет:
partial class PartialMethodsDemo
{
partial void CustomizeToString(ref string text)
{
text += " - customized!";
}
}
В листинге первая часть кода, скорее всего, будет сгенерирована автоматически, объявляет два метода, позволяющие обеспечить дополнительное поведение в конструкторе и при получении строкового представления объекта. Вторая часть соответствует написанному вручную коду, который не требует дополнительной логики в конструкторе, но хочет изменить строковое представление, возвращаемое ToString(). Несмотря на то, что метод CustomizeToString не может возвращать значение напрямую, он может передавать информацию вызывающему его методу через ref параметр. Поскольку OnConstruction не реализован, он и вызовы его удаляются компилятором.

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
This media is not supported in your browser
VIEW IN TELEGRAM
День сто девяносто девятый. #ЧтоНовенького
Поиск по коду в панели быстрого поиска
В Visual Studio 2019 версии 16.3, которая доступна для предварительного скачивания и должна выйти в конце сентября, добавлена возможность поиска по коду через панель быстрого поиска (Ctrl + Q). Теперь можно искать классы и члены классов в коде C# и VB. Результаты будут отображаться при вводе запроса, а также на отдельной вкладке Code.
Также возможен camel-case поиск. Это позволяет вводить только заглавные буквы имени класса или члена вместо полного имени.
Источник: https://devblogs.microsoft.com/visualstudio/code-recent-items-and-template-search-in-visual-studio/