
Мне никогда не нравилась многословность кода. Длинные и подробные названия упрощают работу с бизнес-логикой, но технические детали кода хочется держать краткими, чтобы они отвлекали на себя минимум внимания.
Одна из многословных конструкций .NET связана с деталями реализации асинхронности и обросла кучей мифов. Про неё спрашивают на собеседованиях, код-ревью, делают обязательной, добавляя в правила линтера. Это .ConfigureAwait(false), сопровождающий каждый await в коде.
В этой статье я расскажу, зачем нужен ConfigureAwait(false) и как обойтись без него.
async/await: continuation
Перед тем, как перейти к ConfigureAwait напомню, что такое асинхронный код, где у таска continuation, и что такое SynchronizationContext.
- Асинхронный код с использованием
async/awaitнарезается на отдельные блоки синхронного кода (разделение происходит поawait). Каждый из этих блоков назовём continuation. - Переходы между блоками происходят либо синхронно, либо путём подписки на завершение асинхронного действия. Если асинхронное действие завершилось до
await, то выполнение продолжится в том же потоке.
Как именно компилятор трансформирует код можно посмотреть, например, на sharplab.io
async void Btn_OnClick(object? sender, RoutedEventArgs e)
{
Task<string> task = GetTextAsync();
var text = await task;
// continuation
Text.Text = text;
}
Выше приведён код обработчика события нажатия на кнопку. Где будет выполняться continuation этого обработчика событий после завершения асинхронного Task? Нет, не в Thread Pool. Все действия с UI должны производиться в одном потоке, на котором крутится event loop. Этот код будет работать только в том случае, если continuation, обновляющий содержимое TextBox Text вернётся на UI-поток, в котором началась обработка события.
Для этого UI-фреймворки устанавливают SynchronizationContext, который возвращает continuation в очередь основного потока.
Без SynchronziationContext пришлось бы явно перекладывать UI-код на UI-поток:
async void Btn_OnClick(object? sender, RoutedEventArgs e)
{
var text = await GetTextAsync().ConfigureAwait(false); // теряем SynchronizationContext и переходим в Thread Pool
await Dispatcher.UIThread.InvokeAsync(() => Text.Text = text); // переданный делегат выполняется в контексте UIThread
}
SynchronizationContext встречается не только в UI-коде. Например, xUnit переопределяет его, для отслеживания async void методов и обработки исключений в них. В старом ASP.NET тоже был задан SynchronizationContext для доступа к HttpContext. К счастью, в ASP.NET Core его нет.
Кроме SynchronizationContext также может быть переопределён TaskScheduler, примерно с теми же последствиями.
И где здесь проблема?
void Btn_OnClick(object? sender, RoutedEventArgs e)
{
var text = GetTextAsync().GetAwaiter().GetResult(); // синхронное ожидание
Text.Text = text;
}
async Task<string> GetTextAsync()
{
var request = CreateRequest();
var response = await client.SendAsync(request);
// GetTextAsync continuation
var text = Deserialize(response);
return text;
}
Блокировка UI-потока
Разработчик UI может ожидать выполнения метода GetTextAsync синхронно (или в коде используется библиотека с плохим, синхронным ожиданием внутри).
В этом случае:
- UI-поток заблокируется до завершения этого метода
- В соответствии с
SynchronizationContext, внутренний continuation методаGetTextAsync(в котором вызываетсяDeserialize) должен выполняться на UI-потоке - Но UI-поток заблокирован и не может выполнить этот continuation
- Результат: deadlock, хотя поток при этом всего один
В некоторых случаях deadlock может не произойти: если GetTextAsync выполнится синхронно, либо если в нём произойдёт переход в другой контекст, например на Thread Pool.
Стоит отметить, что желательно избегать блокирующего ожидания, особенно на UI-потоке. Даже если deadlock не произойдёт, во время блокировки UI-потока программа будет выглядеть зависшей.
Излишняя нагрузка на UI-поток
Если GetTextAsync ожидается асинхронно (с использованием await), то возникнет другая проблема. Контекст синхронизации попадает в метод GetTextAsync и его continuation с методом Deserialize тоже выполнится на UI-потоке. Блокировки не будет, но UI-поток во время выполнения этого метода не сможет выполнять более полезную нагрузку. Если на UI-поток попадает много лишнего кода, который мог бы выполняться в фоне — приложение станет менее отзывчивым.
ConfigureAwait(false) как решение
Из-за этих проблем и сложности их отлова, в .NET сложилась практика писать код, который потенциально может быть вызван внутри SynchronizationContext (т.е. в коде библиотек) так, чтобы эти проблемы не возникли, каким бы этот контекст не был.
А средство для этого — .ConfigureAwait(false), обеспечивающий перекладывание continuation на Thread Pool.
async void Btn_OnClick(object? sender, RoutedEventArgs e)
{
var text = await GetTextAsync();
// Btn_OnClick continuation
Text.Text = text;
}
async Task<string> GetTextAsync()
{
var request = CreateRequest(authToken);
var response = await client.SendAsync(request).ConfigureAwait(false);
// GetTextAsync continuation
// в случае, если `SendAsync` выполнился асинхронно
// SynchronizationContext.Current теперь null
var text = Deserialize(response);
return text;
}
В этом случае, continuation метода GetTextAsync будет выполняться на потоке из Thread Pool, а возврат в исходный контекст синхронизации произойдёт лишь при выходе из GetTextAsync — в результате, Btn_OnClick continuation выполнится на UI потоке, как и ожидалось изначально.
Если await выполнится синхронно — переход в Thread Pool не произойдёт. Отсюда берётся рекомендация использовать .ConfigureAwait(false) вместе с каждым await.
Также, .ConfigureAwait(false) лишает вызывающий код возможности управлять тем, где будет выполняться асинхронный код переопределением SynchronizationContext и TaskScheduler. Какая-то часть кода "сбежит" в стандартный Thread Pool из-за повсеместного использования .ConfigureAwait(false).
.ConfigureAwait(true) задаёт поведение по-умолчанию и не несёт в себе никакого смысла.
Не только Task
Кроме Task/Task<T> .ConfigureAwait(false) актуален для ValueTask/ValueTask<T>, IAsyncEnumerable<T> и IAsyncDisposable (и некоторых других типов).
Особую боль представляет собой IAsyncDisposable. Обёртка ConfiguredAsyncDisposable — не generic, и не даёт возможности получить доступ к оригинальному объекту, в результате требуется разделять создание объекта и его использование в конструкции using. Область видимости переменной при этом выходит за границы блока using, что создаёт риск ошибки в коде.
Как обойтись без .ConfigureAwait(false)
1. Решить проблему на стороне вызывающего кода
Наивно считать, что во всём используемом коде расставлены .ConfigureAwait(false). Можно изначально писать код, выполняющийся в контексте синхронизации так, чтобы ему было безразлично, как написаны await в вызываемом коде.
Этого можно достичь, если запускать весь код, которому не нужен контекст синхронизации на Thread Pool, например с помощью Task.Run. Делегат, переданный в Task.Run будет выполнен без контекста синхронизации — на стандартном Thread Pool. В отсутствии контекста синхронизации ConfugureAwait не несёт смысла.
Task, возвращённый методом Task.Run ожидается уже в контексте синхронизации, поэтому continuation Btn_OnClick будет выполнен на UI-потоке и значение в Text успешно изменится.
async void Btn_OnClick(object? sender, RoutedEventArgs e)
{
var text = await Task.Run(() => GetTextAsync()); // внутри этой лямбды SynchronizationContext.Current == null
// Btn_OnClick continuation
Text.Text = text;
}
async Task<string> GetTextAsync()
{
var request = CreateRequest(authToken);
var response = await client.SendAsync(request);
// GetTextAsync continuation
var text = Deserialize(response);
return text;
}
Этот способ переносит сложность в вызывающий код, но имеет несколько преимуществ:
- будет работать, независимо от наличия
.ConfigureAwait(false)в вызываемом коде - позволяет вынести больше работы в фон — теперь в Thread Pool выполняется код не только после первого сработавшего
.ConfigureAwait(false), но и весь код до, в нашем примере — не толькоDeserialize, но иCreateRequest.
Также, в Task.Run можно обернуть не только асинхронный метод, но и синхронный, например на случай, если внутри есть блокировка, приводящая к deadlock, или для выноса тяжелых вычислений в фон.
2. Использовать правильное синхронное ожидание
Предыдущий способ будет также работать и при синхронном ожидании. Можно сделать синхронную обёртку над асинхронным методом, которая в отличие от простого .Wait()/.Result/.GetAwaiter().GetResult() будет устойчива к описанному deadlock.
В идеальном мире хочется, чтобы синхронная версия метода была реализована отдельно и не задействовала Thread Pool. Часто это нереалистично из-за необходимости поддерживать две реализации сразу. Этот способ как раз для таких случаев.
public void Do()
{
if (SynchronizationContext.Current == null && TaskScheduler.Current == TaskScheduler.Default)
DoAsync().GetAwaiter().GetResult();
else
Task.Run(() => DoAsync()).GetAwaiter().GetResult();
}
3. Однократный переход в Thread Pool
Этот способ предназначен для разработчиков асинхронного кода в библиотеках, которым нужно гарантировать работу кода независимо от SynchronizationContext и того, как код используется извне, но нет желания засорять код .ConfigureAwait(false).
Вместо повсеместных .ConfigureAwait(false) в вызываемом коде, предлагается написать конструкцию, уводящую выполнение метода на Thread Pool один раз в начале метода. Можно ограничиться только публичными методами.
При таком способе переход на Thread Pool произойдёт сразу, до первого асинхронного await. Это может снизить производительность, если обычно все await выполняются синхронно, без смены потока, или наоборот, повысить — если до первого асинхронного await выполняется вычислительный код, зря занимающий UI-поток. В реальных условиях разницу вряд ли получится заметить вовсе.
async Task<string> GetTextAsync()
{
await TaskEx.EscapeContext(); // await TaskScheduler.Default;
var request = CreateRequest(authToken);
var response = await client.SendAsync(request); // .ConfigureAwait больше не нужен
// GetTextAsync continuation
var text = Deserialize(response);
return text;
}
Способ подсмотрен в dotnet/runtime. Также есть issue о добавлении публичного API и готовая реализация в Microsoft.VisualStudio.Threading.
Ниже приведена реализация, переходящая в Thread Pool только если задан контекст синхронизации или TaskScheduler:
readonly struct EscapeAwaiter : ICriticalNotifyCompletion
{
public bool IsCompleted
=> SynchronizationContext.Current == null &&
TaskScheduler.Current == TaskScheduler.Default;
public void GetResult() { }
public void OnCompleted(Action continuation)
=> Task.Run(continuation);
public void UnsafeOnCompleted(Action continuation)
=> ThreadPool.QueueUserWorkItem(state => ((Action)state!)(), continuation);
}
readonly struct EscapeAwaitable
{
public EscapeAwaiter GetAwaiter() => new EscapeAwaiter();
}
static class TaskEx
{
public static EscapeAwaitable EscapeContext() => new EscapeAwaitable();
}
Однако, есть случай, когда такой подход не сработает — при реализации IAsyncEnumerable<T>. В этом случае, если вызывающий метод имеет SynchronizationContext, то итератор будет получать контекст при каждом вызове .MoveNextAsync(). В итоге, уход с контекста потребуется делать заново после каждого yield return.
async IAsyncEnumerable<int> Process()
{
for (int i = 0; i < 3; ++i)
{
await Task.Delay(1000).ConfigureAwait(false);
// перешли на Thread Pool
yield return i;
// получили контекст обратно, т.к. это уже новый вызов метода `.MoveNextAsync()`
}
}4. Кодогенерация
Чтобы не писать .ConfigureAwait(false) вручную — их можно сгенерировать сразу по всей сборке или для отдельных классов и методов. Например, с помощью ConfigureAwait.Fody.
На мой взгляд, предыдущее решение лучше, как более явное, но если в проекте уже используется Fody, то выбрать этот вариант вполне логично.
Выводы
Проблемы, возникающие при работе асинхронного кода во внешнем контексте синхронизации могут быть решены и без повсеместного иcпользования .ConfigureAwait(false), причём как со стороны вызывающего, так и со стороны вызываемого кода.
Сейчас уже сложно сказать, почему работа с контекстом синхронизации в C# была спроектирована именно таким образом. Да и это лишено смысла — изменить это уже невозможно, т.к. это огромный breaking change.
В .NET сложилась практика использования .ConfigureAwait(false) в коде библиотек, однако это не является обязательным:
- перейти на Thread Pool можно и другими способами, например с помощью своего
Awaiter - клиентский код всегда может сам вызывать код библиотеки в правильном контексте
- в библиотеках созданных для использования, например из ASP.NET Core кода
.ConfigureAwait(false)не нужны, т.к. их нет в самом ASP.NET Core
Так что на вопрос "нужен ли ConfigureAwait?" можно ответить: если вы его не используете и никто не жалуется — не нужен. А если уже используете, то всё зависит от кода, который ваш код использует.
Ссылки
Комментарии (19)

ARad
00.00.0000 00:00+1Есть вопросы, возможно глупые...
Решить проблему на стороне вызывающего кода.
Чем
Task.RunлучшеConfigureAwait(false)? Можно лиvar text = await Task.Run(() => GetTextAsync()); // внутри этой лямбды SynchronizationContext.Current == nullзаменить наvar text = await GetTextAsync().ConfigureAwait(false);. Что правильнее, быстрее, лучше и почему? В доках вроде как написано использовать второе.Использовать правильное синхронное ожидание
Тот же вопрос, почему используется
Task.Run, а неConfigureAwait(false)? Чем это лучше и правильнее?Однократный переход в Thread Pool
Предложенный код не выглядит оптимальным...

epeshk Автор
00.00.0000 00:00Можно ли
var text = await Task.Run(() => GetTextAsync());заменить наvar text = await GetTextAsync().ConfigureAwait(false);
Тот же вопрос, почему используется Task.Run, а не ConfigureAwait(false).ConfigureAwait(false)имеет смысл только в методах, которым не нужно возвращаться на UI поток. Если использовать его внутри метода, которому важен контекст, то произойдёт исключение при попытке обновить UI (Text.Text = text;), если выполнение перейдёт на Thread Pool.Метод
Task.Runв этих примерах разделяет код, которому контекст важен, и код, который может работать на Thread Pool без возврата в исходный контекст. Весь код, запущенный черезTask.Runничего не узнает про контекст синхронизации, в то же времяTask, возвращённыйTask.Runможет ожидаться в нужном контексте — хоть синхронно, хоть асинхронно.
ARad
00.00.0000 00:00Спасибо за ответ, я понял в чем разница.
По итогу выходит лучшее использовать первый способ если у вас асинхронный процесс редко отдает управление (может надолго заморозить UI поток). Например можно добавить
Thread.Delay(1000)в методGetTextAsync()для моделирования такого случая.Второй совет нужно использовать обязательно для синхронного кода который вызывает асинхронный...
Остальные советы выглядят для меня больше как костыли... Или нужны в сложных случаях которые мне пока непонятны...
Кстати для таких сложных случаев можно ли использовать https://github.com/microsoft/vs-threading которую вы привели как пример или эта библиотека потянет много ненужных зависимостей? По названию она не выглядит как универсальная...

qw1
00.00.0000 00:00+1Остальные советы выглядят для меня больше как костыли
Ну почему костыли.
Если вы уже согласились на Task.Run, пункт 2) позволит не делать лишнее переключение потока и обратно, если мы уже на ThreadPool, вызывая Task.Run только в нужных случаях.
Пункт 3) приятный синтаксический сахар.
Единожды написать в начале методаawait TaskEx.EscapeContext();и дальше можно писать await-ы подряд, не задумываясь о ConfigureAwait на каждом awaitvar result1 = await RunQuery1(param); var result2 = await RunQuery2(result1); var result3 = await RunQuery3(result2);

epeshk Автор
00.00.0000 00:00Корень проблемы
.ConfigureAwait(...)на мой взгляд в том, что она сталкивает разработчиков библиотек и UI-кода:разработчики библиотек не хотят засорять код
.ConfigureAwait(false)разработчики UI —
.ConfigureAwait(true),await Task.Run(() => ...)
Наиболее автоматизированное решение сейчас — генерировать
.ConfigureAwait(false)через Fody. Но Fody — сторонняя приблуда, ещё и усложняющая сборку проекта. Конечно хочется нативного решения, на уровне dotnet sdk или рантайма, и идей, как это сделать описано уже много.Атрибут для сборки/параметр
*.csproj
https://github.com/dotnet/csharplang/issues/2542Переопредение оператора
awaitна уровне проекта
https://github.com/dotnet/csharplang/issues/2649Короткий синтаксис для `.ConfigureAwait(false)
https://github.com/dotnet/csharplang/discussions/645Новый вид
Task/Task<T>, свободный от контекста (даже есть реализация, но кажется, что overkill)
https://github.com/ufcpp/ContextFreeTaskЕсли добавить в язык условный
await(false)илиawait!!грязи меньше не станет, поэтому остановимся на первом proposal. Его можно реализовать, например, в видеconfigureawait context, по аналогии сnullable contextС ним в
*.csprojможно было бы писать:<ConfigureAwait>false</ConfigureAwait>А в файле с кодом
*.cs:#configureawait true public static async Task ContextDependentMethod() { ... } #configureawait restoreТолько увы, proposal висит с 2015 года, и никакого результата. Cейчас
.ConfigureAwait(false)реализован в виде структуры-обёртки надTask, т.е. на уровне стандартной библиотеки, реализация же proposal потребует знания оConfigureAwaitна стороне компилятора (Roslyn) или рантайма. Возможно, сложность заключается в этом. Или просто всем без разницы и время на это не выделили.
mvv-rus
00.00.0000 00:00+1<тут было было про ненужноепро
ConfigureAwait(true);?Удалено>А по поводу, что с этим делать, я сторонник того, чтобы указывать компилятору, как именно надо разворачивать await в async-методе: либо подразумевать, что await оставляет код в том же контекте синхронизации (для разработчиков приложений), как это сделано сейчас, либо запускает продолжение выполнения метода в ThreadPool (это для разрабочиков библиотек и всяческих вспомогательных методов, которым котнтекст синхронизации фреймворка для работы не нужен).
Лучший IMHO способ укзать это - использовать атрибут, т.к. его можно указывать и для метода, и для класса, и для всей сборки.

mvv-rus
00.00.0000 00:00Поаккуратнее с примерами. Вот вы приводите пример:
async Task<string> GetTextAsync()
{
await TaskEx.EscapeContext(); // await TaskScheduler.Default;
var request = CreateRequest(authToken);
var response = await client.SendAsync(request);
// .ConfigureAwait больше не нужен //
GetTextAsync continuation var text = Deserialize(response);
return text;
}Способ подсмотрен в dotnet/runtime. Также есть issue о добавлении публичного API и готовая реализация в Microsoft.VisualStudio.Threading.
А что такое TaskEx.EscapeContext()? В стандартной библиотеке времени выполнения такого класса и метода нет, в классе Task такого метода нет тоже. И по ссылкам в GitHub в текущей версии его тоже нет.
Конечно, если прочитать issue по вашей сcылке да порыться в сети, то можно выяснить, откуда у этого кода ноги растут - но это явно не для того уровня читателей, на который (полагаю) рассчитана эта статья.PS А вообще-то, на Хабре некогда уже был опубликован перевод статьи Тэлбота из блога разработчиков MS, в которой хорошо, без лишних сложностей - и, главное, из первых рук - объяснено, что это за зверь такой - ConfigureAwait(false). Что IMHO несколько снижает ценность этой статьи
PPS Отдельный упрек редактору в новой версии Хабра, который не позволяет нормально процитировать блок кода.

Mingun
00.00.0000 00:00Так двумя абзацами ниже реализация же приведена. Хотя соглашусь, лучше бы комментарием отметить, что «реализацию смотрите ниже» или даже сразу её в тот же блок кода засунуть, а то действительно поначалу сбивает с толку.

mvv-rus
00.00.0000 00:00Ну да, увидел, в конце концов - в конце длинного блока кода, про связь которого с предыдущем блоком в статье явно не упомянуто. А еще лично меня сбило с толку, что когда-то (лет десять назад в какой-то промежуточной версии .NET Framework) этот класс и этот метод таки были (но в окончательную версию не вошли), и следы этого остались и в памяти, и в интернете, во всяких разных примерах.

equeim
00.00.0000 00:00+1А в чем проблема делать это на стороне библиотечного кода? Библиотека на то и библиотека что у нее нет информации о контексте и о том как ее используют. Например в котлине считается что библиотечную корутину должно быть можно запускать на любом треде/диспетчере, а она сама уже разберётся что ей нужно:
suspend fun loadAndParseJson(): Json { val bytes = withContext(Dispatchers.IO) { // IO is for blocking I/O // Reading from file } val json = withContext(Dispatchers.Default) { // Default is for computations // Parsing data } return json }
qw1
00.00.0000 00:00В данном примере форсируется переключение потоков, а легко можно придумать пример приложения, которому чтение json и его парсинг допустимо делать в одном потоке, без переключения. В общем случае неважно, но для перфекционистов может быть эстетически неприемлемо.

phoenixbk
00.00.0000 00:00По чему не сделать чтобы библиотечный метод просто возвращал Task.Run, а там уже пусть как хотят, так и ждут?
Task<string> GetTextAsync() { return Task.Run(async () => { var request = CreateRequest(authToken); var response = await client.SendAsync(request); var text = Deserialize(response); return text; }); }
epeshk Автор
00.00.0000 00:00+2Такой способ будет работать, но дополнительный
Taskбудет создаваться даже если в вызывающем коде нет контекста.
Можно доработать и перекладывать в Thread Pool только если контекст есть, получится эквивалент способа 3 (сawait EscapeContext())

andrucci
00.00.0000 00:00Для I/O-bound задач Task.Run плох тем, что блокирует поток из ThreadPool'а до момента завершения задачи. А одно из достоинств модели async/await именно в масштабируемости, т.е. в экономии на потоках.

epeshk Автор
00.00.0000 00:00Блокировка UI-потока гораздо заметнее блокировки фонового потока, поэтому
Task.Run(() => ...)может быть меньшим злом.Если запускаемая функция блокирует поток Thread Pool и это ухудшает производительность — её можно запустить не на Thread Pool, а в отдельном потоке, создав его вручную, или следующим образом (в нынешней реализации .NET этот способ тоже создаёт новый поток):
Task.Factory.StartNew( () => { ... }, TaskCreationOptions.LongRunning|TaskCreationOptions.HideScheduler);В этом случае, блокирующий код внутри метода будет выполняться в отдельном потоке и заблокирует только его.
Однако постоянное создание новых потоков может ещё больше навредить производительности, а при выносе вычислений в отдельные потоки пропускная способность Thread Pool также может снизиться, но уже из-за загруженности самих процессорных ядер. Поэтому не всегда целесообразно использование новых потоков или флага
LongRunning.
andrucci
00.00.0000 00:00+1Согласен, эта проблема актуальна больше для сервера. Но вот тоже интересный подход применительно к UI - прокачивать сообщения в момент простоя.

qw1
00.00.0000 00:00Если уж заморачиваться такими вещами, то правильно не использовать блокирующие файловые операции, например
File.ReadAllBytes();
заменить наawait File.ReadAllBytesAsync();
epeshk Автор
Корень проблемы
.ConfigureAwait(...)на мой взгляд в том, что она сталкивает разработчиков библиотек и UI-кода:разработчики библиотек не хотят засорять код
.ConfigureAwait(false)разработчики UI —
.ConfigureAwait(true),await Task.Run(() => ...)Наиболее автоматизированное решение сейчас — генерировать
.ConfigureAwait(false)через Fody. Но Fody — сторонняя приблуда, ещё и усложняющая сборку проекта. Конечно хочется нативного решения, на уровне dotnet sdk или рантайма, и идей, как это сделать описано уже много.Атрибут для сборки/параметр
*.csprojhttps://github.com/dotnet/csharplang/issues/2542
Переопредение оператора
awaitна уровне проектаhttps://github.com/dotnet/csharplang/issues/2649
Короткий синтаксис для `.ConfigureAwait(false)
https://github.com/dotnet/csharplang/discussions/645
Новый вид
Task/Task<T>, свободный от контекста (даже есть реализация, но кажется, что overkill)https://github.com/ufcpp/ContextFreeTask
Если добавить в язык условный
await(false)илиawait!!грязи меньше не станет, поэтому остановимся на первом proposal. Его можно реализовать, например, в видеconfigureawait context, по аналогии сnullable contextС ним в
*.csprojможно было бы писать:А в файле с кодом
*.cs:Только увы, proposal висит с 2015 года, и никакого результата. Cейчас
.ConfigureAwait(false)реализован в виде структуры-обёртки надTask, т.е. на уровне стандартной библиотеки, реализация же proposal потребует знания оConfigureAwaitна стороне компилятора (Roslyn) или рантайма. Возможно, сложность заключается в этом. Или просто всем без разницы и время на это не выделили.