public class WeatherService
{
private readonly ConcurrentDictionary<string, Weather> _cache;
public WeatherService()
{
_cache = new();
}
public async Task<Weather> GetWeatherTask(string city)
{
if (!_cache.ContainsKey(city))
{
var weather = await DummyWeatherProvider.Get(city);
_cache.TryAdd(city, weather);
}
return _cache[city];
}
public async ValueTask<Weather> GetWeatherValueTask(string city)
{
if (!_cache.ContainsKey(city))
{
var weather = await DummyWeatherProvider.Get(city);
_cache.TryAdd(city, weather);
}
return _cache[city];
}
کلاس WeatherService شامل یک فیلد private از نوع collection و دو متد است. ما از _cache جهت نگهداری اطلاعاتی که قبلا دریافت شده، استفاده میکنیم و به نوعی in-memory cache را پیاده سازی میکنیم. پیاده سازی منطق هر دو متد GetWeatherTask و GetWeatherValueTask کاملا شبیه به هم است؛ بهطوریکه اول بررسی میکنیم اطلاعات آب و هوای شهر مورد نظر در _cache وجود دارد یا خیر؟ اگر وجود داشت، اطلاعات به صورت مستقیم برگشت داده میشود؛ در غیر این صورت ()DummyWeatherProvider.Get فراخوانی خواهد شد.
در قدم بعدی اطلاعات بهدست آمده را در cache_ ذخیره میکنیم. سپس مقدار ذخیره شده را برگشت میدهیم. در واقع تنها تفاوت دو متد ذکر شده، نوع خروجی آن میباشد؛ یکی از Taskو دیگری از ValueTask استفاده میکند.
برای مقایسهی مصرف حافظهی این دو روی هر دو متد، Benchmark میگیریم. برای پیاده سازی نیار به کدهای زیر داریم :
csharp
[MemoryDiagnoser]
public class TaskAndValueTaskBenchmark
{
private readonly WeatherService _weatherService;
public TaskAndValueTaskBenchmark()
{
_weatherService = new();
}
[Benchmark]
[Arguments("Denver")]
public async Task<Weather> TaskBenchmark(string city)
{
return await _weatherService.GetWeatherTask(city);
}
[Benchmark]
[Arguments("London")]
public async ValueTask<Weather> ValueTaskBenchmark(string city)
{
return await _weatherService.GetWeatherValueTask(city);
}
}
نتیجه به دست آمده به شرح زیر است :
TaskBenchmark
{
"Gen0" : "0.0229",
"Allocated" : "144 B"
}
ValueTaskBenchmark
{
"Gen0" : "----",
"Allocated" : "------"
}
با توجه به نتیجه بهدست آمده، متدی که خروجی ValueTask دارد، حافظهای را تخصیص ندادهاست؛ این دقیقا مزیت مهم ValueTask نسبت به Task میباشد.
🛠مزیت <ValueTask<T
بهدلیل اینکه از نوع struct هستند، بر روی حافظه، در قسمت Stack ذخیره میشوند و به صورت خودکار بعد از اینکه نیازی به آنها نباشد، از حافظه حذف میشوند . به همین دلیل به شکل قابل توجهی، فشار را از روی GC کاهش میدهد .
علاوه بر این، در سناریویی که اکثر کدها به صورت sync اجرا میشوند، در این مواقع استفاده از ValueTask، بهتر از Task میباشد .
این سری متد GetWeatherValueTask را جهت تشخص اینکه اغلب کدها به صورت sync یا async اجرا میشوند، بررسی میکنیم. در متد ذکر شده اگر اطلاعات شهر مورد نظر وجود داشته باشد، کار به صورت sync اجرا میشود و اگر شهر وجود نداشته باشد، کار به صورت async اجرا میشود. با بررسی دقیقتر متوجه میشویم اکثر مواقع در این متد کار به صورت sync اجرا میشود؛ چرا که بعد ازدریافت اطلاعات، مجدد آن را دریافت نمیکند، بلکه از حافظه میخواند (همان cache_ ) .
🔸️محدودیتهای استفاده از <ValueTask<T
1️⃣ در اینجا تنها یکبار امکان استفاده از await وجود دارد. وقتی یکبار valueTask را await میکنیم، بهتر است کار دیگری بر روی آن انجام ندهیم؛ چراکه ممکن است از حافظه پاک شده باشد.
2️⃣ اگر در سناریویی لازم دارید چندین بار await را بر روی valueTask اجرا کنید، لازم است ابتدا آن را به Task تبدیل کنیم. برای این کار متد AsTask را فراخوانی میکنیم (بهتر است صرفا یکبار متد AsTask را فراخوانی کنیم).
3️⃣ نمیتوانیم به یک ValueTask به صورت هم زمان در حالت MultiThreads دسترسی داشته باشیم.
4️⃣ به صورت پیش فرض خروجی عملیات async، نوع Task میباشد؛ مگر اینکه اغلب مراحل کار به صورت sync اجرا شود، مانند مثالی که بالاتر اشاره شد.
🔖هشتگها:
#Task #ValueTask