Теперішнє розроблення додатків вимагає високої відгукності, і побудова відгукних додатків значною мірою залежить від асинхронного програмування (asynchronous programming), особливо коли йдеться про завдання, пов'язані з I/O. Якщо ви працюєте з базами даних, маєте справу з файлами або здійснюєте API виклики, асинхронне програмування забезпечує швидкість і зручність вашого додатка. Якщо ви працювали з async/await у .NET, напевно, ви використовували Task
, щоб представляти асинхронні операції. Але чи знали ви, що є ще одна альтернатива?
У цьому блозі я хочу познайомити вас із ValueTask
— легковажною альтернативою Task
, розробленою для сценаріїв, де важливі ефективність роботи та ресурси. Хоча обидва типи мають однакову мету, вони призначені для різних випадків. Розуміння різниці між Task
і ValueTask
допоможе вам писати більш ефективний і підтримуваний код.
Давайте розглянемо ці типи, щоб зрозуміти їх відмінності та визначити, коли кожен з них варто використовувати.
Що таке Task?
Task
представляє асинхронну операцію в .NET. Коли ви викликаєте асинхронний метод, зазвичай він повертає Task
, який виконує операцію у фоновому режимі та зрештою надає результат.
Простота Task
робить його основним вибором для більшості асинхронних методів. Ось кілька важливих фактів про Task
:
- Важкий:
Task
— це клас, що означає більші вимоги до пам'яті. При створенні він виділяє місце в хіпі. - Гнучкий у використанні: Після завершення
Task
можна повторно використовувати в різних контекстах, що робить його дуже гнучким для різних сценаріїв. - Обробка помилок:
Task
підтримує обробку виключень, що допомагає ловити помилки під час асинхронних операцій.
Наприклад, читання або запис файлів — це зазвичай довготривала операція, пов'язана з I/O, яку слід виконувати асинхронно. Оскільки читання з файлу є відносно повільним і навряд чи завершиться синхронно, Task
ідеально підходить для цього випадку.
public async Task ReadFileAsync(string filePath)
{
using (var reader = new StreamReader(filePath))
{
return await reader.ReadToEndAsync();
}
}
У цьому випадку кожен виклик пов'язаний з великими витратами (читання з диска) і навряд чи буде виконаний миттєво.
Цей метод також можна використовувати в різних контекстах без обмежень на кількість викликів або передачу через додаток, що відповідає можливостям Task
.
Task
також сумісний з обробкою помилок, є багаторазовим і природно підтримує асинхронне продовження.
Ось чому Task
є гарним вибором для операцій з файловим I/O.
Що таке ValueTask?
ValueTask
— це легковажна альтернатива Task
. Він був введений в .NET, щоб підвищити продуктивність у специфічних випадках, коли створення повноцінного Task
є непотрібним. Це особливо корисно, коли ви очікуєте, що результат асинхронної операції буде доступний швидко або навіть синхронно в деяких випадках.
Основні характеристики ValueTask
:
- Ефективність пам'яті: На відміну від
Task
,ValueTask
є структурою. Це означає, що його можна зберігати в стосі, що зменшує виділення пам'яті в хіпі і підвищує ефективність пам'яті. - Умовне використання:
ValueTask
слід використовувати лише в тих випадках, коли ви знаєте, що асинхронний метод може завершитись синхронно або дуже швидко. Він ідеально підходить для таких сценаріїв, як операції кешування або невеликі фонові завдання. - Одноразове використання: На відміну від
Task
,ValueTask
не можна використовувати повторно. Після того, як його чекають, не слід чекати його знову, оскільки він призначений для одноразового використання. - Без блокувань:
ValueTask
не можна заблокувати, тобто не можна синхронно чекати на його завершення (наприклад, за допомогою.Wait()
).
Завжди споживайтеValueTask
за допомогоюawait
, щоб забезпечити правильну поведінку.
Наприклад, розглянемо метод, який отримує дані з кешу в пам'яті. Якщо дані доступні в пам'яті, їх можна повернути одразу (синхронно). В іншому випадку, вони будуть отримані асинхронно з бази даних. Тут ValueTask
дозволяє уникнути непотрібного створення Task
, коли результат доступний негайно.
private readonly Dictionary _cache = new();
public async ValueTask GetCachedValueAsync(string key)
{
if (_cache.TryGetValue(key, out int cachedValue))
{
return cachedValue; // Синхронне завершення
}
// Якщо немає в кеші, симулюємо асинхронний виклик до бази даних
int dbValue = await FetchFromDatabaseAsync(key);
_cache[key] = dbValue; // Кешуємо результат
return dbValue;
}
private async Task FetchFromDatabaseAsync(string key)
{
await Task.Delay(100); // Симуляція затримки при доступі до бази даних
return new Random().Next(1, 100); // Симульовані дані
}
Зверніть увагу, що GetCachedValueAsync
може завершитись синхронно, якщо значення знайдено в кеші. Використання ValueTask
дозволяє уникнути непотрібного виділення пам'яті в таких випадках. Він заощаджує пам'ять, не створюючи новий об'єкт Task
, якщо кешоване значення повертається синхронно.
Примітка: Пам'ятайте, що
ValueTask
не слід очікувати кілька разів. Після того, як результат отримано зGetCachedValueAsync
, уникайте повторного використанняawait
.
Насправді, хоча ви технічно можете блокувати або повторно використовувати ValueTask
, це суперечить рекомендованій практиці асинхронного програмування і підриває переваги продуктивності та гарантії коректності, що надаються ValueTask
. Коротше кажучи, основна перевага зникає. Ось чому вам слід завжди використовувати await
для обробки операцій ValueTask
.
Однак, якщо вам необхідно блокувати з якоїсь причини або потрібно очікувати більше ніж один раз, ви можете зберегти ValueTask
, перетворивши його на Task
за допомогою методів .AsTask()
або .Preserve()
. Це створює стандартний екземпляр Task
, який можна безпечно чекати кілька разів.
// Правильне використання
var cachedValue = await GetCachedValueAsync("key");
// Перетворити на Task:
var task = GetCachedValueAsync("key").AsTask();
// Або зберегти:
var preservedTask = GetCachedValueAsync("key").Preserve();
// Лише у випадку крайньої необхідності, але все одно не рекомендується.
task.Wait();
preservedTask.Wait();
З іншого боку, повторне використання або неправильне очікування ValueTask
може призвести до неочікуваної поведінки або помилок під час виконання, оскільки ValueTask
є структурою і не завжди представляє унікальний, одноразовий результат, як Task
.
// Небезпечне: Очікування ValueTask кілька разів
ValueTask valueTask = GetCachedValueAsync("key");
// Результати НЕ гарантують однакове значення.
int value1 = await valueTask;
int value2 = await valueTask; // Помилка: ValueTask можна очікувати лише один раз!
// Безпечне: Зберігаємо, перетворюючи в Task
Task preservedTask = valueTask.AsTask();
// Результати завжди будуть однакові.
int value3 = await preservedTask;
int value4 = await preservedTask; // Безпечно: Повертає вже обчислений результат.
Пам'ятайте, що повторне використання ValueTask
не гарантує однакове значення. Оскільки, коли ви повторно використовуєте ValueTask
, якщо він підтримується синхронним значенням (наприклад, літерал або результат), повторне використання може постійно давати однакове значення. В іншому випадку, він підтримується станом асинхронної операції (наприклад, I/O виклик
або async метод
), і кожне використання може викликати нове обчислення, що потенційно призведе до різних результатів.
Коли використовувати кожен
У більшості випадків, Task
повинен бути вашим стандартним вибором для асинхронних операцій у .NET. Це просто у використанні, широко підтримується і чудово підходить для більшості асинхронних сценаріїв.
Розглядайте використання ValueTask
тільки в конкретних, високопродуктивних випадках, коли важливо мінімізувати алокації пам'яті та максимізувати ефективність.
Ось швидка інструкція для прийняття рішення:
Використовуйте Task
, якщо:
- Операція є тривалою або за своєю природою асинхронною, наприклад, мережевий або диск I/O.
- Результат задачі буде очікуватися (awaited) кілька разів.
- Ви розробляєте публічний API, де споживачі можуть потребувати повторного використання.
- Не потрібно оптимізувати алокації пам'яті спеціально.
Використовуйте ValueTask
, якщо:
- Операція може завершитися синхронно, наприклад, результат із кешу.
- Метод викликається в швидкому циклі, де алокація пам'яті є дорогою.
- Ви перевірили за допомогою профілювання, що алокації об'єктів
Task
спричиняють проблеми з продуктивністю.
Отже, як Task
, так і ValueTask
мають свої переваги, і вибір правильного типу може допомогти покращити продуктивність.
Важливі моменти, які слід пам'ятати
- Повторне використання:
ValueTask
не можна використовувати повторно, тому віддавайте перевагуTask
, якщо вам потрібна гнучкість для повторного використання результату. - Обробка помилок: І
Task
, іValueTask
підтримують виключення, алеTask
зазвичай використовується та краще підтримується в сценаріях, що передбачають складну обробку помилок. - Сумісність: Багато бібліотек і фреймворків .NET побудовано з урахуванням
Task
. ВикористанняValueTask
може викликати проблеми сумісності при інтеграції з існуючими кодовими базами або API, що очікуютьTask
.
Підсумок
У більшості випадків використання Task
є простішим і суміснішим з існуючим кодом. Однак, якщо ви працюєте в області високої продуктивності, де важливо зменшити алокацію пам'яті, ValueTask
може бути відмінним варіантом.
Використання Task
для загальних асинхронних методів і зарезервування ValueTask
для конкретних оптимізованих випадків є відмінним підходом.
Дякую за прочитане!
Якщо ви вважаєте цей пост корисним і хочете підтримати, не забудьте поставити лайк та поділитися ним з іншими, хто може з цього скористатися.👏👏👏👏👏
Бажаю успішного навчання 📈, і сподіваюся скоро поділитися новими статтями з вами.
Слідкуйте за мною в Twitter, Github для більше контенту.
Залишайтеся на зв'язку в LinkedIn.
Перекладено з: Task vs. ValueTask in C#