День 2813. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 1
Почему существует асинхронность: цена блокировки потока
Прежде чем углубиться в то, как работает асинхронность, рассмотрим, какую проблему она решает, поскольку обычные объяснения либо расплывчаты («это делает ваше приложение быстрее»), либо неверны («она запускает ваш код в фоновом режиме»).
async/await ничего не ускоряет. Сетевой вызов занимает столько времени, сколько потребуется. Что меняется, так это то, что ваш процесс делает во время ожидания, и это, оказывается, имеет большое значение.
Рассмотрим метод, который вызывает веб-сервис и возвращает длину ответа:
Пока этот запрос находится в работе, вызывающий поток находится внутри блокирующего вызова. Он ничего не делает, он просто занят, и процессор может запускать что-то другое. Но поток не бесплатен. Он всё ещё существует, ему по-прежнему принадлежит свой стек, и, что особенно важно, если он поступил из пула потоков, он больше не доступен для выполнения другой работы.
В блокирующей версии поток занят на все время выполнения ввода-вывода. В асинхронной же версии поток освобождается, как только операция запускается, и поток снова необходим только после того, как станет доступен результат.
Сколько стоит поток
Обычный аргумент против лишних потоков – лишняя память. Поток по умолчанию резервирует мегабайт адресного пространства стека, поэтому десять тысяч потоков резервируют десять гигабайт. Это правда, но в 64-битной версии это не так критично, потому что память резервируется, а не выделяется.
На самом же деле проблемы больше исходят из пула потоков. Пул потоков .NET начинается с количества рабочих потоков, равного числу процессоров. Когда каждый из них занят и в очереди стоит больше работы, это не приводит к немедленному созданию новых. Пул добавляет потоки постепенно, используя алгоритм восхождения к вершине, который отслеживает пропускную способность, чтобы решить, действительно ли помогают дополнительные потоки. Это правильное поведение для работы с нагрузкой на CPU, когда дополнительные потоки, превышающие количество ядер, добавляют только переключения контекста. Это совершенно неправильное поведение для потоков, заблокированных для ввода-вывода, т.к. они не потребляют ресурсы CPU, и никакие измерения пропускной способности не покажут, что добавление потоков как-то исправит ситуацию. В результате заблокированные потоки пула не просто тратят память. Они душат всё остальное, что хочет сделать процесс.
Как это выглядит
Поставим в очередь 1000 операций, каждая из которых ожидает 50мс. Один раз путём блокировки потока, а второй раз путём ожидания:
На машине с 15 ядрами:
Обе версии ожидают по 50мс 1000 раз, и обе в принципе могут завершиться за время чуть более 50мс. Асинхронная версия делает это. Версия с блокировкой занимает в 62 раза больше времени. Пиковое количество потоков одинаково в обоих прогонах. Это то, что обычно удивляет людей. Пул не запустил 1000 потоков для обслуживания блокировки. Он поддерживал примерно один поток на ядро и выполнял операции волнами примерно по 16, поэтому 1000 операций по 50мс превратились в 63 последовательных раунда.
Но это нельзя считать ускорением в 62 раза. Всё зависит от количества повторений. При параллельности 10 на машине с 15 ядрами потоков хватает на всех, поэтому их блокировка не требует никаких измеримых затрат. Async здесь не быстрее, но выделяет почти вдвое больше памяти. Но уже на 100 блокирующая версия примерно в 5 раз медленнее. Асинхронная же версия в каждом случае занимает те же 51мс, т.к. она не блокирует поток для каждой операции.
Таким образом, асинхронность — это не оптимизация производительности в обычном понимании. Это масштабируемость, которая:
- не уменьшает задержку для единственной операции, даже немного добавляет;
- не уменьшает выделение памяти, а добавляет;
- лишь устраняет связь между количеством выполняемых операций и количеством необходимых вам потоков.
Для настольного приложения, загружающего 3 файла при запуске, это очевидно, не стоит того, и синхронный ввод-вывод может быть лучшим выбором. Для сервера, обрабатывающего 1000+ одновременных запросов, это разница между работой и зависанием.
Другая причина: отзывчивость
WinForms, WPF и MAUI обрабатывают цикл обработки сообщений в одном выделенном потоке. Блокировка этого потока останавливает цикл сообщений, что означает, что окно перестаёт перерисовываться и перестаёт отвечать на ввод. Такой поток существует только один, поэтому вариант «просто использовать другой» невозможен: работа должна покинуть UI-поток, а результат должен вернуться в него. async/await делает и то, и другое (как - об этом позже).
Продолжение следует...
Источник: https://www.meziantou.net/why-async-exists-the-cost-of-blocking-a-thread.htm
Ещё Раз Про Async/Await в C#. Часть 1
Почему существует асинхронность: цена блокировки потока
Прежде чем углубиться в то, как работает асинхронность, рассмотрим, какую проблему она решает, поскольку обычные объяснения либо расплывчаты («это делает ваше приложение быстрее»), либо неверны («она запускает ваш код в фоновом режиме»).
async/await ничего не ускоряет. Сетевой вызов занимает столько времени, сколько потребуется. Что меняется, так это то, что ваш процесс делает во время ожидания, и это, оказывается, имеет большое значение.
Рассмотрим метод, который вызывает веб-сервис и возвращает длину ответа:
public static int GetLength(string url)
{
using var client = new HttpClient();
var response = client
.Send(new HttpRequestMessage(HttpMethod.Get, url));
return (int)response
.Content
.Headers
.ContentLength
.GetValueOrDefault();
}
Пока этот запрос находится в работе, вызывающий поток находится внутри блокирующего вызова. Он ничего не делает, он просто занят, и процессор может запускать что-то другое. Но поток не бесплатен. Он всё ещё существует, ему по-прежнему принадлежит свой стек, и, что особенно важно, если он поступил из пула потоков, он больше не доступен для выполнения другой работы.
В блокирующей версии поток занят на все время выполнения ввода-вывода. В асинхронной же версии поток освобождается, как только операция запускается, и поток снова необходим только после того, как станет доступен результат.
Сколько стоит поток
Обычный аргумент против лишних потоков – лишняя память. Поток по умолчанию резервирует мегабайт адресного пространства стека, поэтому десять тысяч потоков резервируют десять гигабайт. Это правда, но в 64-битной версии это не так критично, потому что память резервируется, а не выделяется.
На самом же деле проблемы больше исходят из пула потоков. Пул потоков .NET начинается с количества рабочих потоков, равного числу процессоров. Когда каждый из них занят и в очереди стоит больше работы, это не приводит к немедленному созданию новых. Пул добавляет потоки постепенно, используя алгоритм восхождения к вершине, который отслеживает пропускную способность, чтобы решить, действительно ли помогают дополнительные потоки. Это правильное поведение для работы с нагрузкой на CPU, когда дополнительные потоки, превышающие количество ядер, добавляют только переключения контекста. Это совершенно неправильное поведение для потоков, заблокированных для ввода-вывода, т.к. они не потребляют ресурсы CPU, и никакие измерения пропускной способности не покажут, что добавление потоков как-то исправит ситуацию. В результате заблокированные потоки пула не просто тратят память. Они душат всё остальное, что хочет сделать процесс.
Как это выглядит
Поставим в очередь 1000 операций, каждая из которых ожидает 50мс. Один раз путём блокировки потока, а второй раз путём ожидания:
// block: Thread.Sleep удерживает поток
tasks[i] = Task.Run(() => Thread.Sleep(time));
// async: Task.Delay освобождает поток
tasks[i] = Task.Delay(time);
На машине с 15 ядрами:
Block
- всего времени: 3307мс
- максимум потоков: 16
Async
- всего времени: 53мс
- максимум потоков: 16
Обе версии ожидают по 50мс 1000 раз, и обе в принципе могут завершиться за время чуть более 50мс. Асинхронная версия делает это. Версия с блокировкой занимает в 62 раза больше времени. Пиковое количество потоков одинаково в обоих прогонах. Это то, что обычно удивляет людей. Пул не запустил 1000 потоков для обслуживания блокировки. Он поддерживал примерно один поток на ядро и выполнял операции волнами примерно по 16, поэтому 1000 операций по 50мс превратились в 63 последовательных раунда.
Но это нельзя считать ускорением в 62 раза. Всё зависит от количества повторений. При параллельности 10 на машине с 15 ядрами потоков хватает на всех, поэтому их блокировка не требует никаких измеримых затрат. Async здесь не быстрее, но выделяет почти вдвое больше памяти. Но уже на 100 блокирующая версия примерно в 5 раз медленнее. Асинхронная же версия в каждом случае занимает те же 51мс, т.к. она не блокирует поток для каждой операции.
Таким образом, асинхронность — это не оптимизация производительности в обычном понимании. Это масштабируемость, которая:
- не уменьшает задержку для единственной операции, даже немного добавляет;
- не уменьшает выделение памяти, а добавляет;
- лишь устраняет связь между количеством выполняемых операций и количеством необходимых вам потоков.
Для настольного приложения, загружающего 3 файла при запуске, это очевидно, не стоит того, и синхронный ввод-вывод может быть лучшим выбором. Для сервера, обрабатывающего 1000+ одновременных запросов, это разница между работой и зависанием.
Другая причина: отзывчивость
WinForms, WPF и MAUI обрабатывают цикл обработки сообщений в одном выделенном потоке. Блокировка этого потока останавливает цикл сообщений, что означает, что окно перестаёт перерисовываться и перестаёт отвечать на ввод. Такой поток существует только один, поэтому вариант «просто использовать другой» невозможен: работа должна покинуть UI-поток, а результат должен вернуться в него. async/await делает и то, и другое (как - об этом позже).
Продолжение следует...
Источник: https://www.meziantou.net/why-async-exists-the-cost-of-blocking-a-thread.htm
👍16
День 2816. #Карьера #AI
Станет ли ИИ-Кодинг Концом Программирования для Людей?
Автор оригинала: Pavel Yosifovich
Мое знакомство с компьютерами состоялось в 1983 г. В ZX-Spectrum я мог использовать язык программирования (BASIC), чтобы заставить компьютер выполнять различные задачи, создавать игры и другие интересные программы. Важной частью процесса было именно удовольствие — создание чего-то из ничего; это было увлекательно и бросало вызов, напоминая игру, независимо от того, что я писал. Вскоре у меня появился Commodore 64. Хотя BASIC оставался основным языком, нередко приходилось писать код и на ассемблере. Главной причиной была низкая скорость BASIC (он был интерпретируемым).
Прошли годы — и BASIC ушёл в прошлое. Ассемблер тоже практически не используется. Популярные языки справляются с большинством задач. Даже интерпретируемые языки вроде Python и JavaScript пользуются огромным успехом: пусть они и «медленные», их быстродействия хватает для множества приложений. С появлением языков программирования, предлагающих более высокие уровни абстракции, потребность в ассемблере свелась к минимуму.
Эра ИИ-программирования
Вы наверняка видели возможности ИИ в написании кода и, вероятно, уже пользовались ими. Признаюсь, мне немного грустно от того, что люди будут писать меньше кода вручную, перепоручая эту задачу ИИ-агентам. Возможно, это естественный эволюционный процесс — переход к использованию абстракций более высокого уровня. Однако здесь есть принципиальное различие. Раньше под более высокой абстракцией обычно подразумевался другой язык программирования — возможно, с более совершенными инструментами и средой разработки.
ИИ-кодинг — процесс особый: мы используем естественный язык, который по своей природе допускает неоднозначность и неточность, — а ведь именно для устранения этих проблем в своё время и создавались языки программирования. ИИ активно используется для написания кода, причем степень участия человека варьируется от существенной до отсутствующей; однако ответственность за качество кода (как бы оно ни определялось для конкретной кодовой базы) по-прежнему лежит на человеке — по крайней мере, пока.
Классические споры энтузиастов разных языков программирования — C# или Java? Python или Ruby? C или C++? — похоже, теряют свою актуальность. Так ли необходимо знать о последних новинках в C#? Если код генерирует ИИ, а мы лишь проводим код-ревью (если проводим), важны ли нам тонкости самого процесса написания? Сторонники использования ИИ в программировании, вероятно, видят будущее (причём не столь отдаленное), в котором ИИ будет писать весь код на основе спецификаций, составленных людьми или даже самим ИИ, причём сам код может быть скрыт от человеческих глаз. Но почему бы не пойти ещё дальше? Возможно, ИИ сможет создать более эффективный язык — ведь использование текстовых токенов удобно лишь для людей; быть может, более компактный и производительный бинарный язык позволил бы ИИ писать код ещё быстрее.
Один из аргументов против такой модели заключается в том, что неизбежно будут возникать ошибки, а значит, участие человека всё же необходимо. ИИ не безошибочен — по крайней мере, пока. В коде могут встречаться состояния гонки, проблемы с памятью, логические ошибки или проблемы с производительностью. Именно здесь требуется вмешательство человека: чтобы исправить ошибку, нужно понимать код. С другой стороны, ИИ способен помочь и в устранении ошибок — он уже это делает. Достаточно дать промпт, предоставить логи и т.д., и ИИ с готовностью возьмётся за поиск и исправление багов. Гарантирован ли успех? Пока нет, но однажды ИИ сможет устранять ошибки самостоятельно — возможно, лишь изредка прибегая к помощи человека. Даже в тех случаях, когда требуется вмешательство человека, «видеть» сам код может и не понадобиться. ИИ сам внесёт исправления: если ожидаемое поведение системы обеспечено и целевые показатели производительности достигнуты, то какая разница, как выглядит код? Конечно, возможны исключительные ситуации, когда ошибки не удается устранить таким способом. Но ИИ всегда способен преобразовать бинарный код в вид, понятный человеку, чтобы тот мог детально изучить проблему.
Кому-то подобные перспективы могут показаться чрезмерными. Неужели мы действительно полностью переложим программирование на плечи машин? Возможно, история сможет нас чему-то научить.
До появления карманных калькуляторов учёные прибегали к помощи людей-«вычислителей», которые выполняли рутинную работу, в то время как сам учёный сосредотачивался на «научной» составляющей. Потом роль таких помощников исчезла — мы безоговорочно доверяем технике. Когда мы пишем код на языке высокого уровня, мы используем компилятор для его преобразования в машинный код, понятный физическому процессору. В компиляторах случаются ошибки, но мы всё равно им доверяем. Ошибку в компиляторе рано или поздно исправят. Почему бы не рассматривать написание кода с помощью ИИ в том же ключе? ИИ может ошибаться, как и компиляторы, но он также способен исправить эти ошибки после их обнаружения. Нам не обязательно видеть сам код.
Конец эпохи ручного программирования?
Означает ли это, что людям больше не нужно писать код вручную? Полагаю, мы движемся именно в этом направлении. Кажется, что участие человека в написании кода больше не требуется. Неудивительно, что многие программисты всерьез опасаются, что ИИ «отнимет» у них работу. Что же ждет программиста-человека в будущем?
Недавно я в прямом эфире демонстрировал процесс написания драйвера ядра Windows с нуля — задачи, требующей учёта множества нюансов. Один из участников спросил, обязательно ли знать все эти детали: ведь ИИ наверняка может написать код вместо меня. Возможно, ИИ справится с написанием кода даже лучше меня. Однако я делал упор не на деталях реализации, а на архитектуре и концепциях, лежащих в основе кода: как работает драйвер ядра? Как он загружается? Как управляет ресурсами? Как взаимодействует с ядром? И т.д. Суть в том, что детали реализации кода можно доверить ИИ, но фундаментальные концепции — нет. Во-первых, как человек может провести проверку кода, написанного ИИ? Даже если предположить, что ИИ пишет код без ошибок, как быть с общей архитектурой и замыслом проекта? ИИ может не видеть всей картины целиком. И даже поручив написание всего кода ИИ, всё равно придётся объяснить ему, что именно нужно создать. Какова роль драйвера в этом проекте? Как он должен взаимодействовать с пользователем? Какие логи использовать? Здесь возможны разные варианты, и именно поэтому человеку необходимо глубокое понимание предметной области, чтобы составить качественное техническое задание для ИИ. Человек становится проектировщиком и архитектором, делегируя рутинную работу и технические детали ИИ.
Этот постепенный переход — длительный процесс. Он не произойдет за месяц, но и десятилетия на это не уйдут. Темпы развития ИИ трудно предсказать, поскольку они, похоже, носят экспоненциальный характер, а человеческая природа не приспособлена к восприятию таких процессов — мы мыслим линейно.
Итого
В глубине души мне грустно от такого положения дел. Мне по-прежнему нравится писать код вручную, создавая то, что я могу назвать своим творением. Все мои знания тонкостей тех или иных языков программирования со временем устареют. Стоит ли изучать новейшие версии языков программирования? Пока да. Но полагаю, что в недалёком будущем это изменится.
Если код моих приложений пишет ИИ, можно ли считать этот код моим? А может, это неверная постановка вопроса. «Моим» является замысел приложения, его архитектура, функционал, возможности и дизайн. Сам же код — это лишь деталь реализации, с чем лучше справляются машины.
Источник: https://scorpiosoftware.net/2026/09/21/is-ai-coding-the-end-of-coding/
Станет ли ИИ-Кодинг Концом Программирования для Людей?
Автор оригинала: Pavel Yosifovich
Мое знакомство с компьютерами состоялось в 1983 г. В ZX-Spectrum я мог использовать язык программирования (BASIC), чтобы заставить компьютер выполнять различные задачи, создавать игры и другие интересные программы. Важной частью процесса было именно удовольствие — создание чего-то из ничего; это было увлекательно и бросало вызов, напоминая игру, независимо от того, что я писал. Вскоре у меня появился Commodore 64. Хотя BASIC оставался основным языком, нередко приходилось писать код и на ассемблере. Главной причиной была низкая скорость BASIC (он был интерпретируемым).
Прошли годы — и BASIC ушёл в прошлое. Ассемблер тоже практически не используется. Популярные языки справляются с большинством задач. Даже интерпретируемые языки вроде Python и JavaScript пользуются огромным успехом: пусть они и «медленные», их быстродействия хватает для множества приложений. С появлением языков программирования, предлагающих более высокие уровни абстракции, потребность в ассемблере свелась к минимуму.
Эра ИИ-программирования
Вы наверняка видели возможности ИИ в написании кода и, вероятно, уже пользовались ими. Признаюсь, мне немного грустно от того, что люди будут писать меньше кода вручную, перепоручая эту задачу ИИ-агентам. Возможно, это естественный эволюционный процесс — переход к использованию абстракций более высокого уровня. Однако здесь есть принципиальное различие. Раньше под более высокой абстракцией обычно подразумевался другой язык программирования — возможно, с более совершенными инструментами и средой разработки.
ИИ-кодинг — процесс особый: мы используем естественный язык, который по своей природе допускает неоднозначность и неточность, — а ведь именно для устранения этих проблем в своё время и создавались языки программирования. ИИ активно используется для написания кода, причем степень участия человека варьируется от существенной до отсутствующей; однако ответственность за качество кода (как бы оно ни определялось для конкретной кодовой базы) по-прежнему лежит на человеке — по крайней мере, пока.
Классические споры энтузиастов разных языков программирования — C# или Java? Python или Ruby? C или C++? — похоже, теряют свою актуальность. Так ли необходимо знать о последних новинках в C#? Если код генерирует ИИ, а мы лишь проводим код-ревью (если проводим), важны ли нам тонкости самого процесса написания? Сторонники использования ИИ в программировании, вероятно, видят будущее (причём не столь отдаленное), в котором ИИ будет писать весь код на основе спецификаций, составленных людьми или даже самим ИИ, причём сам код может быть скрыт от человеческих глаз. Но почему бы не пойти ещё дальше? Возможно, ИИ сможет создать более эффективный язык — ведь использование текстовых токенов удобно лишь для людей; быть может, более компактный и производительный бинарный язык позволил бы ИИ писать код ещё быстрее.
Один из аргументов против такой модели заключается в том, что неизбежно будут возникать ошибки, а значит, участие человека всё же необходимо. ИИ не безошибочен — по крайней мере, пока. В коде могут встречаться состояния гонки, проблемы с памятью, логические ошибки или проблемы с производительностью. Именно здесь требуется вмешательство человека: чтобы исправить ошибку, нужно понимать код. С другой стороны, ИИ способен помочь и в устранении ошибок — он уже это делает. Достаточно дать промпт, предоставить логи и т.д., и ИИ с готовностью возьмётся за поиск и исправление багов. Гарантирован ли успех? Пока нет, но однажды ИИ сможет устранять ошибки самостоятельно — возможно, лишь изредка прибегая к помощи человека. Даже в тех случаях, когда требуется вмешательство человека, «видеть» сам код может и не понадобиться. ИИ сам внесёт исправления: если ожидаемое поведение системы обеспечено и целевые показатели производительности достигнуты, то какая разница, как выглядит код? Конечно, возможны исключительные ситуации, когда ошибки не удается устранить таким способом. Но ИИ всегда способен преобразовать бинарный код в вид, понятный человеку, чтобы тот мог детально изучить проблему.
Кому-то подобные перспективы могут показаться чрезмерными. Неужели мы действительно полностью переложим программирование на плечи машин? Возможно, история сможет нас чему-то научить.
До появления карманных калькуляторов учёные прибегали к помощи людей-«вычислителей», которые выполняли рутинную работу, в то время как сам учёный сосредотачивался на «научной» составляющей. Потом роль таких помощников исчезла — мы безоговорочно доверяем технике. Когда мы пишем код на языке высокого уровня, мы используем компилятор для его преобразования в машинный код, понятный физическому процессору. В компиляторах случаются ошибки, но мы всё равно им доверяем. Ошибку в компиляторе рано или поздно исправят. Почему бы не рассматривать написание кода с помощью ИИ в том же ключе? ИИ может ошибаться, как и компиляторы, но он также способен исправить эти ошибки после их обнаружения. Нам не обязательно видеть сам код.
Конец эпохи ручного программирования?
Означает ли это, что людям больше не нужно писать код вручную? Полагаю, мы движемся именно в этом направлении. Кажется, что участие человека в написании кода больше не требуется. Неудивительно, что многие программисты всерьез опасаются, что ИИ «отнимет» у них работу. Что же ждет программиста-человека в будущем?
Недавно я в прямом эфире демонстрировал процесс написания драйвера ядра Windows с нуля — задачи, требующей учёта множества нюансов. Один из участников спросил, обязательно ли знать все эти детали: ведь ИИ наверняка может написать код вместо меня. Возможно, ИИ справится с написанием кода даже лучше меня. Однако я делал упор не на деталях реализации, а на архитектуре и концепциях, лежащих в основе кода: как работает драйвер ядра? Как он загружается? Как управляет ресурсами? Как взаимодействует с ядром? И т.д. Суть в том, что детали реализации кода можно доверить ИИ, но фундаментальные концепции — нет. Во-первых, как человек может провести проверку кода, написанного ИИ? Даже если предположить, что ИИ пишет код без ошибок, как быть с общей архитектурой и замыслом проекта? ИИ может не видеть всей картины целиком. И даже поручив написание всего кода ИИ, всё равно придётся объяснить ему, что именно нужно создать. Какова роль драйвера в этом проекте? Как он должен взаимодействовать с пользователем? Какие логи использовать? Здесь возможны разные варианты, и именно поэтому человеку необходимо глубокое понимание предметной области, чтобы составить качественное техническое задание для ИИ. Человек становится проектировщиком и архитектором, делегируя рутинную работу и технические детали ИИ.
Этот постепенный переход — длительный процесс. Он не произойдет за месяц, но и десятилетия на это не уйдут. Темпы развития ИИ трудно предсказать, поскольку они, похоже, носят экспоненциальный характер, а человеческая природа не приспособлена к восприятию таких процессов — мы мыслим линейно.
Итого
В глубине души мне грустно от такого положения дел. Мне по-прежнему нравится писать код вручную, создавая то, что я могу назвать своим творением. Все мои знания тонкостей тех или иных языков программирования со временем устареют. Стоит ли изучать новейшие версии языков программирования? Пока да. Но полагаю, что в недалёком будущем это изменится.
Если код моих приложений пишет ИИ, можно ли считать этот код моим? А может, это неверная постановка вопроса. «Моим» является замысел приложения, его архитектура, функционал, возможности и дизайн. Сам же код — это лишь деталь реализации, с чем лучше справляются машины.
Источник: https://scorpiosoftware.net/2026/09/21/is-ai-coding-the-end-of-coding/
👍6👎2
День 2817. #ЧтоНовенького #VS
Прекращаем Сборку После Первой Ошибки
Мэдс Кристенсен продолжает поставлять небольшие и на первый взгляд очевидные улучшения процесса разработки в Visual Studio.
Если один из проектов в решении не удается собрать, процесс сборки всего решения обычно продолжается. У вас уже есть ошибка, которую нужно исправить, но остальные проекты продолжают собираться, несмотря на то что сборка решения всё равно закончится неудачей.
В VS2026 (версии 18.11) появилась опция, позволяющая автоматически останавливать сборку решения сразу после того, как в одном из проектов произошел сбой. Если ваш следующий шаг — исправление этой ошибки, то такая настройка поможет сэкономить время, прекращая дальнейший процесс сборки.
Настройка остановки сборки
Перейдите в раздел Tools > Options > Projects and Solutions > Build and Run (Сервис > Параметры > Проекты и решения > Сборка и запуск) и включите параметр Stop solution build after first failure (Останавливать сборку решения после первой ошибки).
При возникновении ошибки в проекте Visual Studio перестанет запускать задачи для проектов, стоящих в очереди, и безопасно остановит те, что уже находятся в процессе сборки. При этом результат сборки помечается как «сбой» (failed), а не «отмена» (canceled). Вам больше не нужно следить за появлением первой ошибки и вручную прерывать выполнение остальных задач.
Эта функция останавливает сборку всего решения, а не только тех проектов, которые зависят от проблемного компонента. Это важное отличие: вы быстрее переходите к устранению первопричины, но при этом не получаете результаты сборки остальных проектов, входящих в решение.
Если вы предпочитаете видеть результаты по всем проектам, прежде чем приступать к исправлению ошибок, оставьте эту настройку отключенной — в таком случае сохранится привычное поведение системы. При необходимости функцию всегда можно отключить на той же странице параметров.
Итоговая экономия времени зависит от структуры вашего решения и того, на каком этапе произошёл сбой. В решениях с длинными цепочками зависимостей или большим объёмом параллельных задач экономия времени будет более заметной, чем в небольших проектах, сборка которых и так завершается практически мгновенно.
Данная настройка определяет, когда следует остановить процесс сборки в случае возникновения ошибки. Она не влияет на то, какие именно проекты будут включены в сборку. Существующие настройки Build and Run (Сборка и запуск) уже позволяют задать, например, сборку только запускаемых проектов и их зависимостей при выполнении или отладке.
Попробуйте использовать функцию остановки при сбое для решения, в котором вы обычно отменяете сборку после появления ошибки. Сравните время ожидания после возникновения сбоя и выберите тот подход, который лучше всего соответствует вашему процессу поиска и устранения неполадок.
Источник: https://devblogs.microsoft.com/visualstudio/stop-waiting-on-a-failed-build/
Прекращаем Сборку После Первой Ошибки
Мэдс Кристенсен продолжает поставлять небольшие и на первый взгляд очевидные улучшения процесса разработки в Visual Studio.
Если один из проектов в решении не удается собрать, процесс сборки всего решения обычно продолжается. У вас уже есть ошибка, которую нужно исправить, но остальные проекты продолжают собираться, несмотря на то что сборка решения всё равно закончится неудачей.
В VS2026 (версии 18.11) появилась опция, позволяющая автоматически останавливать сборку решения сразу после того, как в одном из проектов произошел сбой. Если ваш следующий шаг — исправление этой ошибки, то такая настройка поможет сэкономить время, прекращая дальнейший процесс сборки.
Настройка остановки сборки
Перейдите в раздел Tools > Options > Projects and Solutions > Build and Run (Сервис > Параметры > Проекты и решения > Сборка и запуск) и включите параметр Stop solution build after first failure (Останавливать сборку решения после первой ошибки).
При возникновении ошибки в проекте Visual Studio перестанет запускать задачи для проектов, стоящих в очереди, и безопасно остановит те, что уже находятся в процессе сборки. При этом результат сборки помечается как «сбой» (failed), а не «отмена» (canceled). Вам больше не нужно следить за появлением первой ошибки и вручную прерывать выполнение остальных задач.
Эта функция останавливает сборку всего решения, а не только тех проектов, которые зависят от проблемного компонента. Это важное отличие: вы быстрее переходите к устранению первопричины, но при этом не получаете результаты сборки остальных проектов, входящих в решение.
Если вы предпочитаете видеть результаты по всем проектам, прежде чем приступать к исправлению ошибок, оставьте эту настройку отключенной — в таком случае сохранится привычное поведение системы. При необходимости функцию всегда можно отключить на той же странице параметров.
Итоговая экономия времени зависит от структуры вашего решения и того, на каком этапе произошёл сбой. В решениях с длинными цепочками зависимостей или большим объёмом параллельных задач экономия времени будет более заметной, чем в небольших проектах, сборка которых и так завершается практически мгновенно.
Данная настройка определяет, когда следует остановить процесс сборки в случае возникновения ошибки. Она не влияет на то, какие именно проекты будут включены в сборку. Существующие настройки Build and Run (Сборка и запуск) уже позволяют задать, например, сборку только запускаемых проектов и их зависимостей при выполнении или отладке.
Попробуйте использовать функцию остановки при сбое для решения, в котором вы обычно отменяете сборку после появления ошибки. Сравните время ожидания после возникновения сбоя и выберите тот подход, который лучше всего соответствует вашему процессу поиска и устранения неполадок.
Источник: https://devblogs.microsoft.com/visualstudio/stop-waiting-on-a-failed-build/
👍1👎1
День 2818. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
50. Фреймворки для создания тестовых заглушек
«Расскажите, как использовать тестовые заглушки для модульного тестирования? Приведите примеры создания заглушек для зависимостей и проверки того, что код ведёт себя ожидаемым образом».
Хороший ответ
Использование фреймворков, вроде Moq или NSubstitute в .NET-приложениях для модульного тестирования предполагает создание заглушек для зависимостей, которые могут быть у класса. Это позволяет всесторонне протестировать класс в изоляции.
Тестовые заглушки – общий термин, описывающий все разновидности фиктивных зависимостей в тестах. Среди них можно выделить два основных:
- Моки (mock) – позволяют эмулировать и проверять выходные взаимодействия, т.е. вызовы, совершаемые тестируемой системой к её зависимостям для изменения их состояния. Например, моком можно заменить почтовый сервер, на который отправляется письмо.
- Стабы (stub) – помогают эмулировать входные взаимодействия, т.е. вызовы к зависимостям для получения входных данных. Например, стабом можно заменить базу, из которой запрашиваются данные. Стабы настраиваются для возвращения разных значений для разных сценариев.
Вот пример использования фреймворка NSubstitute:
NSubstitute позволяет проверять, что методы мок-объекта были вызваны с ожидаемыми аргументами. Это важно для того, чтобы убедиться, что тестируемый класс правильно взаимодействует со своими зависимостями. Для этого в NSubstitute используется метод
Рекомендации
1. Проверка поведения: важно всегда проверять поведение, а не детали реализации. Т.е. фокусироваться на том, что возвращает мок или его влияние на результат, а не на том, как именно он это делает.
Замечание: не стоит проверять взаимодействие со стабами. В примере выше ICalculator по сути является стабом. Он предоставляет входные значения для сервиса. Проверка
При этом заглушка может быть моком и стабом одновременно, когда в тесте задаётся поведение одного метода зависимости, а проверяется взаимодействие с другим методом. Например, при тестировании сервиса бронирования такой заглушкой может быть репозиторий, где задаётся поведение метода проверки наличия свободных мест, а проверяется, что сервис бронирования вызывает метод резервации места в репозитории.
2. Один мок на тест: в идеале для каждого теста следует создавать отдельный мок-объект. Это помогает избежать взаимного влияния тестов друг на друга и делает их более понятными.
3. Чёткая настройка взаимодействий: важно явно задавать взаимодействия с моками и проверять их — это обеспечит ясность тестов и упростит их дальнейшее сопровождение.
Распространённые ошибки
- Чрезмерное использование заглушек: делает тесты хрупкими и излишне сложными. Заменять стоит только те зависимости, которые являются внешними для тестируемого класса. Использование заглушек для приватных зависимостей, простых структур данных или сущностей обычно приводит к тому, что тесты ломаются при малейшем изменении структуры данных.
И даже внешние зависимости не всегда стоит заменять заглушками. Например, база данных, которая используется только вашим приложением, и к которой не имеют доступа внешние системы, полностью скрыта от клиентов вашей системы. Её даже можно заменить другой базой, и никто не заметит. Здесь вместо замены базы заглушкой в тестах можно рассмотреть тестирование с использованием БД в памяти.
- Игнорирование реальных реализаций: заменяя слишком много объектов — особенно те, что содержат бизнес-логику, которую тоже следовало бы протестировать, — вы рискуете пропустить серьёзные проблемы интеграции или логические ошибки в реальном коде.
- Мокирование конкретных классов: Moq позволят задать поведение отдельных методов зависимости, при этом вызывая методы реального объекта, поведение которых не было задано. Например:
Настройка
Это один из антипаттернов тестирования. Если вам приходится так делать, это часто означает, что класс зависимости объединяет две ответственности, и стоит его разделить.
Источники:
- https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
- «Принципы юнит-тестирования» (Владимир Хориков — СПб.: Питер, 2022).
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
50. Фреймворки для создания тестовых заглушек
«Расскажите, как использовать тестовые заглушки для модульного тестирования? Приведите примеры создания заглушек для зависимостей и проверки того, что код ведёт себя ожидаемым образом».
Хороший ответ
Использование фреймворков, вроде Moq или NSubstitute в .NET-приложениях для модульного тестирования предполагает создание заглушек для зависимостей, которые могут быть у класса. Это позволяет всесторонне протестировать класс в изоляции.
Тестовые заглушки – общий термин, описывающий все разновидности фиктивных зависимостей в тестах. Среди них можно выделить два основных:
- Моки (mock) – позволяют эмулировать и проверять выходные взаимодействия, т.е. вызовы, совершаемые тестируемой системой к её зависимостям для изменения их состояния. Например, моком можно заменить почтовый сервер, на который отправляется письмо.
- Стабы (stub) – помогают эмулировать входные взаимодействия, т.е. вызовы к зависимостям для получения входных данных. Например, стабом можно заменить базу, из которой запрашиваются данные. Стабы настраиваются для возвращения разных значений для разных сценариев.
Вот пример использования фреймворка NSubstitute:
using NSubstitute;
using Xunit;
public class ServiceTests
{
private IService _sut;
private ICalculator _calc;
public ServiceTests()
{
// Создаём заглушку для зависимости ICalculator
_calc = Substitute.For<ICalculator>();
_sut = new Service(_calc);
}
[Fact]
public void AddNumbers_ShouldReturnCorrectSum()
{
// Задаём поведение метода Add
_calc.Add(5, 10).Returns(15);
var result = _sut.AddNumbers(5, 10);
Assert.Equal(15, result);
// Проверяем, что Add был вызван
_calc.Received(1).Add(5, 10);
}
}
NSubstitute позволяет проверять, что методы мок-объекта были вызваны с ожидаемыми аргументами. Это важно для того, чтобы убедиться, что тестируемый класс правильно взаимодействует со своими зависимостями. Для этого в NSubstitute используется метод
Received(), как показано в примере выше.Рекомендации
1. Проверка поведения: важно всегда проверять поведение, а не детали реализации. Т.е. фокусироваться на том, что возвращает мок или его влияние на результат, а не на том, как именно он это делает.
Замечание: не стоит проверять взаимодействие со стабами. В примере выше ICalculator по сути является стабом. Он предоставляет входные значения для сервиса. Проверка
Received() показана для примера, но она излишняя и делает тест хрупким. Тест не должен интересоваться, как тестируемая система получает результат, если результат верный.При этом заглушка может быть моком и стабом одновременно, когда в тесте задаётся поведение одного метода зависимости, а проверяется взаимодействие с другим методом. Например, при тестировании сервиса бронирования такой заглушкой может быть репозиторий, где задаётся поведение метода проверки наличия свободных мест, а проверяется, что сервис бронирования вызывает метод резервации места в репозитории.
2. Один мок на тест: в идеале для каждого теста следует создавать отдельный мок-объект. Это помогает избежать взаимного влияния тестов друг на друга и делает их более понятными.
3. Чёткая настройка взаимодействий: важно явно задавать взаимодействия с моками и проверять их — это обеспечит ясность тестов и упростит их дальнейшее сопровождение.
Распространённые ошибки
- Чрезмерное использование заглушек: делает тесты хрупкими и излишне сложными. Заменять стоит только те зависимости, которые являются внешними для тестируемого класса. Использование заглушек для приватных зависимостей, простых структур данных или сущностей обычно приводит к тому, что тесты ломаются при малейшем изменении структуры данных.
И даже внешние зависимости не всегда стоит заменять заглушками. Например, база данных, которая используется только вашим приложением, и к которой не имеют доступа внешние системы, полностью скрыта от клиентов вашей системы. Её даже можно заменить другой базой, и никто не заметит. Здесь вместо замены базы заглушкой в тестах можно рассмотреть тестирование с использованием БД в памяти.
- Игнорирование реальных реализаций: заменяя слишком много объектов — особенно те, что содержат бизнес-логику, которую тоже следовало бы протестировать, — вы рискуете пропустить серьёзные проблемы интеграции или логические ошибки в реальном коде.
- Мокирование конкретных классов: Moq позволят задать поведение отдельных методов зависимости, при этом вызывая методы реального объекта, поведение которых не было задано. Например:
var moq = new Mock<Calculator> {CallBase = true };
// метод Add должен быть виртуальным
moq.Setup(x => x.Add(5, 10)).Returns(15);Настройка
CallBase = true позволяет моку сохранить реальное поведение базового класса, если оно не было переопределено. Таким образом вызов moq.Add(5, 10) в тесте будет использовать заданное поведение, а вызов, например, moq.Multiply(5, 10) будет использовать метод реального объекта Calculator. Так можно, к примеру, задать заглушки только для методов класса, меняющих состояние системы (пишущих в базу), при этом вызывая реальные методы, не меняющие состояния (читающие из базы).Это один из антипаттернов тестирования. Если вам приходится так делать, это часто означает, что класс зависимости объединяет две ответственности, и стоит его разделить.
Источники:
- https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
- «Принципы юнит-тестирования» (Владимир Хориков — СПб.: Питер, 2022).
👎2
День 2819. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 4
Паттерн awaitable: создание объектов, поддерживающих ожидание
Если спросить большинство C#-разработчиков, что можно использовать с оператором
Понимание этой структуры поможет разобраться в конечном автомате, который генерируется компилятором (о нём позже). Кроме того, это позволит создавать собственные «ожидаемые» объекты.
Структура
Чтобы выражение
Вот и весь контракт. Компилятор выполняет привязку этих членов по их именам, что имеет два важных следствия:
1. Метод
2. Объект ожидания может быть структурой. TaskAwaiter, ValueTaskAwaiter и YieldAwaitable.YieldAwaiter являются структурами; поэтому при ожидании уже завершённой задачи не происходит выделения памяти под объект ожидания.
Как используются эти три члена
Для каждого вызова
- Свойство
- Метод
- Метод
То, что
ICriticalNotifyCompletion
Существует еще один интерфейс —
Слово «Unsafe» звучит тревожно, но на деле всё не так страшно: создаваемый компилятором конечный автомат уже захватывает
Реализуйте оба интерфейса. В
Полезный объект, поддерживающий ожидание
Особенности:
- Тип сам является своим собственным «ожидателем».
- Свойство
- Это readonly-структура. Для операции ожидания не требуется выделение памяти в куче.
Примечание: Тот же механизм, только наоборот, позволяет в UI-фреймворках использовать конструкцию
Когда стоит создавать свой объект ожидания
Довольно редко. В большинстве случаев правильным выбором будут
1. Вы хотите, чтобы
2. Вы работаете с механизмом завершения операций, который существенно отличается от стандартного, и хотите избежать выделения памяти под объект
3. Вы хотите сделать существующий тип «ожидаемым», не меняя его исходный код — здесь поможет прием с методами расширения.
Важно помнить: собственный объект ожидания не поддерживает композицию. Он не будет работать с
Источник: https://www.meziantou.net/the-awaitable-pattern-writing-your-own-awaitable.htm
Ещё Раз Про Async/Await в C#. Часть 4
Паттерн awaitable: создание объектов, поддерживающих ожидание
Если спросить большинство C#-разработчиков, что можно использовать с оператором
await, они ответят: «Task». Это распространённый, но далеко не единственный случай. Механизм await работает так же, как и foreach, на основе структуры. Компилятор ищет определенную «форму» (набор элементов), и Task как раз ей соответствует.Понимание этой структуры поможет разобраться в конечном автомате, который генерируется компилятором (о нём позже). Кроме того, это позволит создавать собственные «ожидаемые» объекты.
Структура
Чтобы выражение
await expr успешно скомпилировалось, у объекта expr должен быть метод GetAwaiter(). Возвращаемый им тип — так называемый «awaiter» (объект ожидания) — должен содержать три элемента и реализовывать один интерфейс:public readonly struct MyAwaiter : INotifyCompletion
{
// завершилась ли уже операция?
public bool IsCompleted { get; }
// значение, либо повторный выброс исключения
public TResult GetResult();
// вызывается при завершении
public void OnCompleted(Action continuation);
}
Вот и весь контракт. Компилятор выполняет привязку этих членов по их именам, что имеет два важных следствия:
1. Метод
GetAwaiter может быть методом расширения. Это позволяет сделать тип «ожидаемым», даже если вы не являетесь его владельцем:public static class
TimeSpanAwaitableExtensions
{
public static TaskAwaiter
GetAwaiter(this TimeSpan timeSpan) =>
Task.Delay(timeSpan).GetAwaiter();
}
…
await TimeSpan.FromMilliseconds(200);
2. Объект ожидания может быть структурой. TaskAwaiter, ValueTaskAwaiter и YieldAwaitable.YieldAwaiter являются структурами; поэтому при ожидании уже завершённой задачи не происходит выделения памяти под объект ожидания.
Как используются эти три члена
Для каждого вызова
await компилятор генерирует примерно следующий код:var awaiter = expr.GetAwaiter();
if (!awaiter.IsCompleted)
{
// сохранение состояния
// регистрация продолжения
// возврат к вызывающему методу
awaiter.OnCompleted(MoveNext);
return;
}
var result = awaiter.GetResult();
- Свойство
IsCompleted определяет, будет ли выполнение метода приостановлено. Если оно возвращает true, выполнение продолжается без прерываний, в том же потоке, без планирования задачи и выделения дополнительной памяти. Это «быстрый путь», ради которого и существует ValueTask.- Метод
OnCompleted вызывается только в том случае, если IsCompleted вернуло false. Он получает продолжение и отвечает за его последующий запуск. Где именно будет выполнено это продолжение — решать вам.- Метод
GetResult вызывается в обоих случаях. Он возвращает результат операции и, что важно, именно здесь повторно выбрасываются исключения. Если операция завершилась неудачей, ожидающий объект сообщает об этом, выбрасывая исключение из GetResult, а не через какой-то отдельный канал для передачи ошибок.То, что
GetResult может возвращать void, нормально: именно так ведёт себя await при работе с непараметризованным Task.ICriticalNotifyCompletion
Существует еще один интерфейс —
ICriticalNotifyCompletion, который добавляет метод UnsafeOnCompleted. Практически все реальные реализации ожидающих объектов поддерживают его. Разница заключается в работе с ExecutionContext. Ожидается, что OnCompleted захватит текущий ExecutionContext и восстановит его перед выполнением продолжения. UnsafeOnCompleted этого делать не обязан.Слово «Unsafe» звучит тревожно, но на деле всё не так страшно: создаваемый компилятором конечный автомат уже захватывает
ExecutionContext при запуске метода и восстанавливает его при каждом вызове MoveNext. Повторный захват контекста для каждого await был бы излишней работой. Поэтому конечный автомат вызывает UnsafeOnCompleted, если ожидающий объект его предоставляет, а в противном случае использует «безопасную» версию (OnCompleted).Реализуйте оба интерфейса. В
UnsafeOnCompleted должна выполняться основная логика, а OnCompleted может просто вызывать UnsafeOnCompleted — при условии, что у вас нет собственного контекстного состояния, которое нужно передавать дальше.Полезный объект, поддерживающий ожидание
Task.Yield() возвращает управление в текущий SynchronizationContext (например, в UI-приложении это означает возврат в UI-поток). Иногда же требуется обратное: безусловно покинуть текущий поток и перейти в поток из пула потоков.public readonly struct
ThreadPoolAwaitable : ICriticalNotifyCompletion
{
public ThreadPoolAwaitable GetAwaiter() => this;
public bool IsCompleted => false;
public void GetResult()
{
}
public void OnCompleted(Action continuation)
=> UnsafeOnCompleted(continuation);
public void UnsafeOnCompleted(Action continuation)
=> ThreadPool.UnsafeQueueUserWorkItem(
static state => ((Action)state!)(), continuation);
}
…
await default(ThreadPoolAwaitable);
// дальнейший код выполняется в потоке из пула
Особенности:
- Тип сам является своим собственным «ожидателем».
GetAwaiter() возвращает this. Это распространённый прием, который позволяет избежать лишнего выделения памяти и дополнительного уровня косвенности; YieldAwaitable использует такой же подход.- Свойство
IsCompleted всегда возвращает false. Это сделано намеренно: если бы оно возвращало true, продолжение выполнялось бы синхронно в текущем потоке, а именно этого мы и стараемся избежать.- Это readonly-структура. Для операции ожидания не требуется выделение памяти в куче.
Примечание: Тот же механизм, только наоборот, позволяет в UI-фреймворках использовать конструкцию
await someControl.Dispatcher; для возврата в UI-поток. Метод OnCompleted объекта ожидания в данном случае помещает продолжение в диспетчер, а не в пул потоков.Когда стоит создавать свой объект ожидания
Довольно редко. В большинстве случаев правильным выбором будут
Task, ValueTask или TaskCompletionSource. Создание собственного объекта ожидания оправдано в следующих ситуациях:1. Вы хотите, чтобы
await означал «смену контекста» (как в случае с Task.Yield() или ThreadPoolAwaitable). При этом не нужно передавать какой-либо результат — важен только факт планирования выполнения.2. Вы работаете с механизмом завершения операций, который существенно отличается от стандартного, и хотите избежать выделения памяти под объект
Task для каждой операции. Для этого существует интерфейс IValueTaskSource (про него позже в этой серии).3. Вы хотите сделать существующий тип «ожидаемым», не меняя его исходный код — здесь поможет прием с методами расширения.
Важно помнить: собственный объект ожидания не поддерживает композицию. Он не будет работать с
Task.WhenAll, WaitAsync или любыми другими механизмами, ожидающими объект Task. Если вам нужны такие возможности, просто возвращайте Task.Источник: https://www.meziantou.net/the-awaitable-pattern-writing-your-own-awaitable.htm
👍9
День 2820. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 5
Как компилятор C# переписывает асинхронный метод. Начало
Вплоть до версии .NET 10,
Метод содержит все элементы, с которыми приходится работать компилятору: параметр, локальные переменные, значения которых должны сохраняться после
Чтобы увидеть, что с методом делает компилятор, можно либо декомпилировать его с помощью ILSpy, либо использовать онлайн инструмент sharplab.io.
Что осталось от исходного метода
По порядку:
1. Создаётся экземпляр конечного автомата.
2. Для него создаётся построитель —
3. Параметры копируются в конечный автомат. Теперь они становятся его полями, поскольку код, который их использует, находится в другом месте.
4. Состояние устанавливается в значение -1 (не запущено, не приостановлено).
5. Вызывается метод
Сгенерированные имена с угловыми скобками не являются допустимыми идентификаторами в языке C#. Это сделано намеренно: они не могут конфликтовать с именами, которые вы задаёте в своём коде, и вы не сможете случайно сослаться на них.
Примечание: Атрибут
Важное следствие: вызов асинхронного метода приводит к выполнению его тела до первого ожидания (
Сгенерированный тип
Ниже представлены поля конечного автомата:
-
-
-
-
-
-
Важно:
1. Локальная переменная становится полем класса только в том случае, если она необходима после оператора
2. Слоты для объектов ожидания привязаны к их типу, а не к конкретному оператору
Окончание следует…
Источник: https://www.meziantou.net/how-the-csharp-compiler-rewrites-an-async-method.htm
Ещё Раз Про Async/Await в C#. Часть 5
Как компилятор C# переписывает асинхронный метод. Начало
Вплоть до версии .NET 10,
async и await не являются встроенными возможностями среды выполнения. Компилятор переписывает код метода с модификатором async. Он извлекает тело метода, переносит его в автоматически созданный тип, а исходный метод заменяет небольшим фрагментом кода, который инициализирует этот тип и запускает его метод. Всё остальное — планирование выполнения, обработка исключений, формирование значения, которое в итоге попадает в возвращаемый объект Task, — представляет собой обычный код, который вы могли бы написать самостоятельно. Сегодня рассмотрим небольшой асинхронный метод и проанализируем, во что его превращает компилятор.Метод содержит все элементы, с которыми приходится работать компилятору: параметр, локальные переменные, значения которых должны сохраняться после
await, два await разных типов, цикл, а также блок try/finally:public static async Task<int>
SumPageSizesAsync(string[] pages)
{
var total = 0;
var sw = Stopwatch.StartNew();
try
{
foreach (var page in pages)
{
var content = await LoadAsync(page);
total += content.Length;
}
await Task.Yield();
return total;
}
finally
{
Console.WriteLine($"Loaded {pages.Length} pages in {sw.ElapsedMilliseconds}ms");
}
}
private static async Task<string>
LoadAsync(string page)
{
await Task.Delay(1);
return page;
}
Чтобы увидеть, что с методом делает компилятор, можно либо декомпилировать его с помощью ILSpy, либо использовать онлайн инструмент sharplab.io.
Что осталось от исходного метода
[AsyncStateMachine(typeof(<SumPageSizesAsync>d__0))]
public static Task<int> SumPageSizesAsync(string[] pages)
{
<SumPageSizesAsync>d__0 stateMachine = default(<SumPageSizesAsync>d__0);
stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.pages = pages;
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
По порядку:
1. Создаётся экземпляр конечного автомата.
2. Для него создаётся построитель —
AsyncTaskMethodBuilder<int>. Он управляет объектом Task, который будут ожидать вызывающие стороны; именно он отвечает за завершение.3. Параметры копируются в конечный автомат. Теперь они становятся его полями, поскольку код, который их использует, находится в другом месте.
4. Состояние устанавливается в значение -1 (не запущено, не приостановлено).
5. Вызывается метод
Start, который запускает выполнение тела метода, после чего возвращается объект Task.Сгенерированные имена с угловыми скобками не являются допустимыми идентификаторами в языке C#. Это сделано намеренно: они не могут конфликтовать с именами, которые вы задаёте в своём коде, и вы не сможете случайно сослаться на них.
Примечание: Атрибут
[AsyncStateMachine] используют отладчики, профилировщики и анализаторы, чтобы связать метод с типом, который фактически содержит его логику.Важное следствие: вызов асинхронного метода приводит к выполнению его тела до первого ожидания (
await) операции, которая ещё не завершилась. Только после этого управление возвращается вызывающей стороне.Сгенерированный тип
Ниже представлены поля конечного автомата:
private struct <SumPageSizesAsync>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder<int> <>t__builder;
public string[] pages;
private int <total>5__2;
private Stopwatch <stopwatch>5__3;
private string[] <>7__wrap3;
private int <>7__wrap4;
private TaskAwaiter<string> <>u__1;
private YieldAwaitable.YieldAwaiter <>u__2;
}
-
<>1__state- указывает точку возобновления выполнения: -1 — выполняется, -2 — завершено; значения 0 и выше соответствуют отдельным операторам await, на которых выполнение может быть приостановлено.-
<>t__builder- объект-построитель, который управляет возвращаемым объектом Task.-
pages - параметр метода; сохраняет своё исходное имя.-
<total>5__2, <sw>5__3 - локальные переменные, которые требуются после выполнения оператора await; отчасти сохраняют свои исходные имена.-
<>7__wrap3, <>7__wrap4 - состояние цикла foreach: массив и текущий индекс. Компилятору они необходимы, чтобы сохранить контекст при выполнении await внутри цикла.-
<>u__1, <>u__2 - по одному слоту на каждый тип объектов ожидания; удерживают их, пока метод находится в приостановленном состоянии.Важно:
1. Локальная переменная становится полем класса только в том случае, если она необходима после оператора
await. Если же переменная используется исключительно в промежутке между двумя операторами await, она остаётся обычной локальной переменной. Поэтому асинхронный метод, сохраняющий большое количество данных, оказывается более «дорогим», чем метод, который этого не делает. Кроме того, перенос части логики в отдельный, синхронный метод позволяет уменьшить размер конечного автомата.2. Слоты для объектов ожидания привязаны к их типу, а не к конкретному оператору
await. Метод LoadAsync возвращает Task<string>, поэтому его «ожидатель» типа TaskAwaiter<string>. А Task.Yield() возвращает YieldAwaitable и его «ожидатель» другого типа. Сколько типов, столько и полей.Окончание следует…
Источник: https://www.meziantou.net/how-the-csharp-compiler-rewrites-an-async-method.htm
👍8
День 2822. #ЗаметкиНаПолях
Таймаут, Который Никогда не Срабатывал
У нас был такой код:
Отправка запроса выполняется с помощью
Выглядит логично. Сможете найти здесь ошибку? Напишите в комментариях, если нашли, не читая дальше.
На самом деле их тут целых две :)
Ошибка 1. Тайм-аут никогда не срабатывает
Конструкция
Небольшой пример для воспроизведения:
Тайм-аут составляет 1 секунду, однако задержка длится все 3 секунды.
Таким образом, в изначальном коде единственным действующим тайм-аутом остается
Ошибка 2. При повторной попытке используется тот же токен
Токен отмены
При выполнении повторной попытки через Polly условие
Из этого также следует, что простого переноса конструкции
Исправление:
Теперь CancellationTokenSource существует на протяжении всей попытки, а каждая попытка получает собственный локальный токен, что исключает перезапись захваченного параметра.
Источник: https://josef.codes/the-timeout-that-never-fired/
Таймаут, Который Никогда не Срабатывал
У нас был такой код:
public async Task<Result<TData>> SendAsync<TData>(
Func<HttpRequestMessage> requestFactory,
RequestOptions? opts = null,
CancellationToken ct = default)
{
opts ??= new RequestOptions();
var policy = opts.Policy ?? DefaultPollyPolicy;
var result = await
policy.ExecuteAndCaptureAsync(async () =>
{
using var request = requestFactory();
if (ct == CancellationToken.None)
{
// Если таймаут не задан, ставим 5 сек
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
ct = cts.Token;
}
using var response =
await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
// …
});
// …
}
Отправка запроса выполняется с помощью
HttpClient, который обёрнут в политику Polly (по умолчанию настроенную на одну повторную попытку). Если вызывающий код не передаёт токен отмены, мы создаём свой токен с тайм-аутом 5 секунд.Выглядит логично. Сможете найти здесь ошибку? Напишите в комментариях, если нашли, не читая дальше.
На самом деле их тут целых две :)
Ошибка 1. Тайм-аут никогда не срабатывает
Конструкция
using var освобождает ресурс по завершении области видимости, в которой она объявлена. В данном случае такой областью является блок if, а не лямбда-выражение. В результате CancellationTokenSource уничтожается (вызывается метод Dispose()) в момент закрывающей фигурной скобки — ещё до того, как запрос будет отправлен. При вызове Dispose() на CancellationTokenSource его внутренний таймер также уничтожается. Токен, полученный из этого объекта, формально продолжает «работать» (исключения не возникает), но он никогда не перейдёт в отменённое состояние.Небольшой пример для воспроизведения:
var ct = CancellationToken.None;
var sw = Stopwatch.StartNew();
if (ct == CancellationToken.None)
{
using var cts =
new CancellationTokenSource(TimeSpan.FromSeconds(1));
ct = cts.Token;
}
try
{
await Task.Delay(TimeSpan.FromSeconds(3), ct);
Console.WriteLine($"Завершено спустя
{sw.Elapsed.TotalSeconds:F1}сек");
}
catch (Exception e)
{
Console.WriteLine($"{e.GetType().Name} спустя
{sw.Elapsed.TotalSeconds:F1}сек");
}
// Вывод
Завершено спустя 3.0сек
Тайм-аут составляет 1 секунду, однако задержка длится все 3 секунды.
Таким образом, в изначальном коде единственным действующим тайм-аутом остается
HttpClient.Timeout, значение которого по умолчанию составляет 100 секунд.Ошибка 2. При повторной попытке используется тот же токен
Токен отмены
ct является параметром метода, и лямбда-выражение захватывает его. Следовательно, когда лямбда-выражение присваивает ему значение, меняется не локальная копия, а одна и та же переменная при каждом вызове лямбды.При выполнении повторной попытки через Polly условие
ct == CancellationToken.None перестаёт быть истинным. В результате блок if пропускается, и для повторной попытки используется токен из первой попытки — тот самый, который относится к уже уничтоженному объекту CancellationTokenSource.Из этого также следует, что простого переноса конструкции
using var за пределы блока if недостаточно. Это решило бы проблему для первой попытки, но повторная попытка все равно выполнялась бы без тайм-аута.Исправление:
using var timeoutCts = ct == CancellationToken.None
// таймаут 5 сек
? new CancellationTokenSource(TimeSpan.FromSeconds(5))
: null;
var token = timeoutCts?.Token ?? ct;
using var response =
await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
Теперь CancellationTokenSource существует на протяжении всей попытки, а каждая попытка получает собственный локальный токен, что исключает перезапись захваченного параметра.
Источник: https://josef.codes/the-timeout-that-never-fired/
👍7👎5
День 2823. #СтатьяДня #AltenarCollab
Heartbeat: как мы создали систему управления системой вместо обычных сигналов
Обычно никто не обращает особого внимания на вспомогательные сервисы вроде мониторинга здоровья (HeartBeat). Они обслуживающие, клиент не видит ошибок в них, да и вообще не имеет к ним доступа. Поэтому (если только у команды не было негативного опыта и – больше того – извлечённых из этого опыта правильных уроков), заботятся об их правильной работе или отказоустойчивости вроде как в последнюю очередь. Есть? Хорошо. Что-то там мониторят? Отлично. А правильно мониторят? Могут своевременно сообщить о сбое до того, как вам начнут звонить разгневанные клиенты? Этот ваш HeartBeat, он вообще, вот прямо сейчас, работает?
А что, если система представляет собой высоконагруженный поток данных в реальном времени, неполадки в одном из сервисов обнаруживаются клиентами также в реальном времени, и устранить их желательно не завтра, после утреннего кофе, а также в реальном времени?
Александр Илларионов из Altenar в своей статье на Хабре рассказывает, как они разрабатывали такую систему, которая могла не просто мониторить real-time сервисы, но и своевременно поднимать тревогу и даже предпринимать некоторые действия, вроде отключения сбоящего сервиса.
Статье немного не хватает последовательности, и автор упускает из повествования некоторые детали. Очевидно, он «в контексте» и понимает, что, зачем и почему. Мне этого понимания в некоторых местах не хватало. Например:
При этом ранее не упоминалось, что значит заблокировать трафик и чем это отличается от отключения.
Но в принципе тема довольно интересная. Хотелось бы, чтобы Александр её развил и рассказал поподробнее. Возможно, в серии статей. Какие конкретно сервисы мониторинга и управления были созданы, как распределялись между ними обязанности и, возможно, детали их реализации.
Heartbeat: как мы создали систему управления системой вместо обычных сигналов
Обычно никто не обращает особого внимания на вспомогательные сервисы вроде мониторинга здоровья (HeartBeat). Они обслуживающие, клиент не видит ошибок в них, да и вообще не имеет к ним доступа. Поэтому (если только у команды не было негативного опыта и – больше того – извлечённых из этого опыта правильных уроков), заботятся об их правильной работе или отказоустойчивости вроде как в последнюю очередь. Есть? Хорошо. Что-то там мониторят? Отлично. А правильно мониторят? Могут своевременно сообщить о сбое до того, как вам начнут звонить разгневанные клиенты? Этот ваш HeartBeat, он вообще, вот прямо сейчас, работает?
А что, если система представляет собой высоконагруженный поток данных в реальном времени, неполадки в одном из сервисов обнаруживаются клиентами также в реальном времени, и устранить их желательно не завтра, после утреннего кофе, а также в реальном времени?
Александр Илларионов из Altenar в своей статье на Хабре рассказывает, как они разрабатывали такую систему, которая могла не просто мониторить real-time сервисы, но и своевременно поднимать тревогу и даже предпринимать некоторые действия, вроде отключения сбоящего сервиса.
Статье немного не хватает последовательности, и автор упускает из повествования некоторые детали. Очевидно, он «в контексте» и понимает, что, зачем и почему. Мне этого понимания в некоторых местах не хватало. Например:
Именно поэтому для нас принципиально важна последовательность в этих действиях: сначала заблокировать трафик, а потом уже отключить его и сообщить клиенту о проблеме.
При этом ранее не упоминалось, что значит заблокировать трафик и чем это отличается от отключения.
Но в принципе тема довольно интересная. Хотелось бы, чтобы Александр её развил и рассказал поподробнее. Возможно, в серии статей. Какие конкретно сервисы мониторинга и управления были созданы, как распределялись между ними обязанности и, возможно, детали их реализации.
👍3
День 2824. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
51. Стратегии интеграционного тестирования
«Опишите ваш подход к интеграционному тестированию .NET-приложений. Расскажите, как вы планируете, реализуете и автоматизируете такие тесты, а также какие инструменты или фреймворки при этом используете».
Хороший ответ
Интеграционное тестирование в .NET-приложениях предполагает объединение модулей и их проверку как единого целого, чтобы убедиться, что взаимодействие между ними происходит ожидаемым образом. Мой подход к интеграционному тестированию включает следующие этапы:
1. Определение сценариев тестирования: выявление критических точек интеграции в приложении, где происходит взаимодействие между различными модулями, сервисами или внешними системами.
2. Подготовка тестовых данных: планирование и создание данных, необходимых для каждой точки интеграции, чтобы имитировать реальные сценарии использования.
3. Настройка тестовой среды: создание отдельной среды, максимально приближённой к проду, что позволяет избежать расхождений между результатами тестов и реальным поведением системы.
Для написания тестов можно использовать xUnit, а также пакет Microsoft.AspNetCore.TestHost, который предоставляет тестовый сервер для запуска приложения. Пример организации теста:
Автоматизация интеграционного тестирования
- Непрерывная интеграция: выполнение интеграционных тестов запускаются в рамках CI-конвейера, чтобы гарантировать их запуск при каждом значимом изменении кодовой базы.
- Отчётность: подробное протоколирование и формирование отчётов о результатах в CI-конвейере позволит оперативно выявлять сбои и устранять их причины.
Лучшие практики
- Изоляция сценариев тестирования: несмотря на то что интеграционные тесты фокусируются на взаимодействии между компонентами, важно изолировать сценарии тестирования, привязывая их к конкретным точкам интеграции. Это упрощает поиск неисправностей и повышает наглядность тестов.
- Использование заглушек для внешних зависимостей, чтобы имитировать внешние системы (см. подробнее о заглушках). Это уменьшает количество зависимостей, а также повышает надёжность и скорость выполнения тестов.
- Мониторинг производительности: важно следить за тем, сколько времени занимают интеграционные тесты, чтобы они не стали «узким местом» в процессе разработки.
Такой подход гарантирует, что интеграционные тесты будут всесторонними и эффективными, а также внесут реальный вклад в общее качество приложения.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
51. Стратегии интеграционного тестирования
«Опишите ваш подход к интеграционному тестированию .NET-приложений. Расскажите, как вы планируете, реализуете и автоматизируете такие тесты, а также какие инструменты или фреймворки при этом используете».
Хороший ответ
Интеграционное тестирование в .NET-приложениях предполагает объединение модулей и их проверку как единого целого, чтобы убедиться, что взаимодействие между ними происходит ожидаемым образом. Мой подход к интеграционному тестированию включает следующие этапы:
1. Определение сценариев тестирования: выявление критических точек интеграции в приложении, где происходит взаимодействие между различными модулями, сервисами или внешними системами.
2. Подготовка тестовых данных: планирование и создание данных, необходимых для каждой точки интеграции, чтобы имитировать реальные сценарии использования.
3. Настройка тестовой среды: создание отдельной среды, максимально приближённой к проду, что позволяет избежать расхождений между результатами тестов и реальным поведением системы.
Для написания тестов можно использовать xUnit, а также пакет Microsoft.AspNetCore.TestHost, который предоставляет тестовый сервер для запуска приложения. Пример организации теста:
public class IntegrationTests : IDisposable
{
private readonly TestServer _server;
private readonly HttpClient _client;
public IntegrationTests()
{
var builder = WebApplication.CreateBuilder();
// … Добавляем сервисы …
builder.Services.AddControllers();
_server = new TestServer(
new WebHostBuilder().UseStartup<Startup>());
_client = _server.CreateClient();
}
[Fact]
public async Task TestEndpointReturnsSuccess()
{
var response = await
_client.GetAsync("/api/values");
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
public void Dispose()
{
_client.Dispose();
_server.Dispose();
}
}
Автоматизация интеграционного тестирования
- Непрерывная интеграция: выполнение интеграционных тестов запускаются в рамках CI-конвейера, чтобы гарантировать их запуск при каждом значимом изменении кодовой базы.
- Отчётность: подробное протоколирование и формирование отчётов о результатах в CI-конвейере позволит оперативно выявлять сбои и устранять их причины.
Лучшие практики
- Изоляция сценариев тестирования: несмотря на то что интеграционные тесты фокусируются на взаимодействии между компонентами, важно изолировать сценарии тестирования, привязывая их к конкретным точкам интеграции. Это упрощает поиск неисправностей и повышает наглядность тестов.
- Использование заглушек для внешних зависимостей, чтобы имитировать внешние системы (см. подробнее о заглушках). Это уменьшает количество зависимостей, а также повышает надёжность и скорость выполнения тестов.
- Мониторинг производительности: важно следить за тем, сколько времени занимают интеграционные тесты, чтобы они не стали «узким местом» в процессе разработки.
Такой подход гарантирует, что интеграционные тесты будут всесторонними и эффективными, а также внесут реальный вклад в общее качество приложения.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md