День сто восьмидесятый. #ЗаметкиНаПолях
Многопоточность. Начало
Многопоточность позволяет увеличивать скорость реагирования приложения и, если приложение работает в многопроцессорной или многоядерной системе, скорость его исполнения.
Процесс — это набор ресурсов, используемый отдельным экземпляром приложения. Операционная система использует процессы для разделения исполняемых приложений. Поток — это основная единица, которой операционная система выделяет время процессора.
Когда используется многопоточность
Несколько потоков используются, чтобы повысить скорость отклика вашего приложения и использовать преимущества многопроцессорной или многоядерной системы для увеличения скорости приложения.
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.
Фоновые и активные потоки
Фоновые потоки приложения завершаются немедленно и без появления исключений при завершении активных потоков. Таким образом активные потоки используются для исполнения заданий, которые обязательно нужно завершить. Фоновые потоки можно оставить для некритичных операций. Потоки можно превращать из активного в фоновый и обратно. Основной поток приложения и все потоки, явно созданные через создание объекта
Многопоточность. Начало
Многопоточность позволяет увеличивать скорость реагирования приложения и, если приложение работает в многопроцессорной или многоядерной системе, скорость его исполнения.
Процесс — это набор ресурсов, используемый отдельным экземпляром приложения. Операционная система использует процессы для разделения исполняемых приложений. Поток — это основная единица, которой операционная система выделяет время процессора.
Когда используется многопоточность
Несколько потоков используются, чтобы повысить скорость отклика вашего приложения и использовать преимущества многопроцессорной или многоядерной системы для увеличения скорости приложения.
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
- В настоящее время потоки в 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). Первые используются для асинхронных вычислительных операций, вторые служат для уведомления кода о завершении асинхронной операции ввода-вывода.
Для добавления в очередь пула потоков асинхронных вычислительных операций обычно вызывают один из методов
Возможны ситуации, когда требуется явно создать поток, исполняющий конкретную вычислительную операцию:
- Поток требуется запустить с нестандартным приоритетом (хотя, этого делать не рекомендуется).
- Чтобы приложение не закрылось до завершения потоком задания, требуется, чтобы поток исполнялся в активном режиме. В примере выше по умолчанию метод Compute выполняется в фоновом потоке, поэтому приложение может прекратить работу перед тем, как завершится выполнение этого метода.
- Задания, связанные с критически важными вычислениями, которые могут выполняться долго, лучше не отдавать на откуп пула потоков, а выделить им отдельный поток.
- Если есть необходимость преждевременно завершить исполняющийся поток через
Продолжение следует…
Источник: Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 26.
Многопоточность.
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), то есть необходима явная поддержка отмены операций. Это значит, что как код, выполняющий отменяемую операцию, так и код, пытающийся отменить операцию, должны использовать одинаковые типы.
В вызывающем коде создаётся объект
Вот наиболее полезные члены структуры
-
-
-
-
В примере ниже токен отмены передаётся методу
Источник: Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 26.
Многопоточность
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.
День сто восемьдесят четвёртый. #ЗаметкиНаПолях
Многопоточность.
4. Задания. Начало
Проблема с асинхронными вычислениями, запущенными с помощью метода
Отмена задания
Для отмены задания можно воспользоваться объектом
Продолжение следует…
Источник: Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 26.
Многопоточность.
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. Задания. Продолжение
Автоматический запуск задания по завершении предыдущего
Вызов метода
Кроме того, вызвать
-
-
-
-
-
-
Вывод результата в предыдущем примере можно переписать с этими флагами:
Если необходим набор заданий в одном состоянии и с одинаковыми параметрами, можно создать фабрику заданий
Продолжение следует…
Источники:
- Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 26.
- https://docs.microsoft.com/dotnet/api/system.threading.tasks.task.factory
Многопоточность.
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
Для реализации наиболее популярных сценариев посредством заданий создан класс
1. Вызывающий поток также принимает участие в обработке и не блокируется, за исключением случаев, когда он заканчивает выполнение работы, а остальные потоки ещё нет.
2. Если какая-либо операция выбрасывает необработанное исключение, то вызванный вами метод класса
3. Использовать методы класса
Опции
Методам класса
-
-
-
Продолжение следует…
Источник: Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 26.
Многопоточность.
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.
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), также называемый шаблоном
Источники:
- Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 27.
- https://docs.microsoft.com/ru-ru/dotnet/standard/asynchronous-programming-patterns/
Многопоточность
7. Шаблоны Асинхронного Программирования
.NET предоставляет три шаблона для выполнения асинхронных операций:
1. Модель асинхронного программирования (APM), также называемый шаблоном
IAsyncResult
, является устаревшей моделью, использующей интерфейс IAsyncResult
для обеспечения асинхронного поведения. В этом шаблоне для синхронных операций требуются методы Begin
и End
(например, BeginWrite
и EndWrite
для реализации асинхронной операции записи). Этот шаблон больше не рекомендуется для новых разработок. Например, рассмотрим метод Read, который считывает указанное количество данных в буфер, начиная с указанного смещения:public class MyClassВ APM нужно реализовать методы
{
public int Read(byte [] buffer, int offset, int count);
}
BeginRead
и EndRead
:public class MyClass2. Асинхронный шаблон на основе событий (EAP) является устаревшим шаблоном на основе событий для обеспечения асинхронного поведения. Для этого требуется метод с суффиксом Async и одно или несколько событий, делегатов обработчиков событий и типов, производных от
{
public IAsyncResult BeginRead(
byte[] buffer, int offset, int count,
AsyncCallback callback, object state);
public int EndRead(IAsyncResult asyncResult);
}
EventArg
. EAP был представлен в .NET Framework 2.0. Больше не рекомендуется для новых разработок. Для примера выше в EAP потребуется метод и событие:public class MyClass3. Асинхронный шаблон на основе задач (TAP) использует общий метод для инициирования и завершения асинхронной операции. TAP был представлен в .NET Framework 4. В настоящее время это рекомендуемый подход к асинхронному программированию в .NET. Языковую поддержку для TAP обеспечивают ключевые слова async и await в C#. Для примера выше в TAP потребуется реализация одного метода:
{
public void ReadAsync(byte[] buffer, int offset, int count);
public event ReadCompletedEventHandler ReadCompleted;
}
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/
Предпросмотр Инструментов в 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
Многопоточность
8. В чем разница между асинхронным программированием и многопоточностью?
Эти понятия часто путают. Многие думают, что многопоточность и асинхронность - это одно и то же, но это не так. Обычно помогает аналогия. Вы готовите в ресторане. Приходит заказ на яйца и тосты.
- Синхронно: вы готовите яйца, затем вы готовите тост.
- Асинхронно, однопоточно: вы начинаете готовить яйца и устанавливаете таймер. Вы начинаете готовить тосты и устанавливаете таймер. Пока они готовятся, вы убираетесь на кухне. Когда таймеры выключаются, вы снимаете яйца с огня и тосты из тостера и подаете их.
- Асинхронно, многопоточно: вы нанимаете еще двух поваров, один для приготовления яиц и один для приготовления тостов. Теперь у вас есть проблема координации поваров, чтобы они не конфликтовали друг с другом на кухне при совместном использовании ресурсов. И вы должны заплатить им.
Итак, значит ли это, что многопоточность - это только один вид асинхронности? Потоки - это рабочие; асинхронность - это задачи. В многопоточных рабочих процессах вы назначаете задачи работникам. В асинхронных однопоточных рабочих процессах у вас есть график задач, где некоторые задачи зависят от результатов других; По завершении каждой задачи вызывается код, который планирует следующую задачу, которая может быть запущена, с учетом результатов только что выполненной задачи. Но вам (надеюсь) нужен только один работник для выполнения всех задач, а не один работник на задачу.
Это помогает понять, что многие задачи не связаны с процессором. Для задач, связанных с процессором, имеет смысл нанять столько рабочих (потоков), сколько имеется процессоров, назначить по одной задаче каждому рабочему, назначить по одному процессору каждому рабочему, и каждый процессор будет выполняет только работу по вычислению результата как можно быстрее. Но для задач, которые не ждут обработки процессором, вам вообще не нужно назначать работника. Вы просто ждёте сообщения о том, что результат доступен, и делаете что-то еще, пока ждёте. Когда это сообщение прибудет, вы можете запланировать продолжение выполненной задачи в качестве следующего пункта в вашем списке дел.
Источник: https://stackoverflow.com/questions/34680985/what-is-the-difference-between-asynchronous-programming-and-multithreading
День сто девяносто второй. #ЗаметкиНаПолях
Заметки по обобщённым типам
1. Инициализация и состояния обобщённых типов
Если выполнить следующий код:
1. Значение
2. Статический конструктор запускается дважды: один раз для каждого закрытого составного типа. Если бы статического конструктора не было, было бы меньше гарантий того, когда точно происходит инициализация каждого типа. Но можно однозначно сказать, что вы можете рассматривать
Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
Заметки по обобщённым типам
1. Инициализация и состояния обобщённых типов
Если выполнить следующий код:
Console.WriteLine(typeof(List<int>));На экран выведется следующее:
Console.WriteLine(typeof(List<string>));
System.Collections.Generic.List`1[System.String]List`1 означает обобщённый тип List арности 1 (один параметр типа), а аргументы параметра типа указаны в квадратных скобках. Таким образом,
System.Collections.Generic.List`1[System.Int32]
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. Вывод типа из аргументов методов
Рассмотрим следующий код:
Хотя вывод типов применяется только к методам, его можно использовать для более простого создания экземпляров обобщённых типов. Например, рассмотрим семейство типов
Они выглядят бессмысленно тривиальными, но позволяют использовать вывод типа. То есть, вместо этого:
Разработчики языка постоянно работают над усовершенствованием процесса вывода типа, и порой логика вывода становится достаточно сложной из-за наследований, перегрузок и необязательных параметров. Поэтому надо понимать, что иногда на практике выведение типа может приводить к неожиданным результатам. Например, если вам нужен
Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
Заметки по обобщённым типам
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() еще более тесно связанным с тем, что вы хотите сделать со значениями прямо сейчас. Что если вы хотите складывать значения, а не печатать их? Писать другой метод? Это ужасное нарушение разделения ответственности.
Решение через итератор - это именно то, что вам нужно: представление бесконечной последовательности и всё. Вызывающий код может выполнять итерацию по своему усмотрению (по крайней мере, до переполнения
Реализация последовательности Фибоначчи вручную проста. Не нужно сохранять много данных состояния (только два предыдущих числа), а управлять логикой выполнения довольно просто (здесь только один оператор
Кроме того, компилятор также достаточно умён, чтобы правильно обрабатывать блоки
Источник: 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 в итераторах
Рассмотрим следующий код:
- Если считать выполнение прерываемым на каждом вызове
- Если считать, что на самом деле на каждом
Выполним этот код:
На самом деле, и да, и нет. Если реализовать итератор вручную и вызвать
Источник: 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 добавлена дополнительный функционал для частичных классов - частичные методы. Это методы, объявленные без тела в одной части, а затем опционально реализованные в другой части. Частичные методы неявно являются
Источник: 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/
Поиск по коду в панели быстрого поиска
В 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/