اجرای موازی درخواستها در Web API: افزایش کارایی و پاسخگویی
چرا اجرای موازی اهمیت دارد؟
تصور کنید یک کنترلر در Web API شما برای پاسخ به یک درخواست، نیازمند دریافت اطلاعات از سه سرویس خارجی مختلف است. در یک پیادهسازی سنتی و ترتیبی (Sequential)، برنامه ابتدا به سرویس اول درخواست میفرستد، منتظر پاسخ میماند، سپس به سراغ سرویس دوم میرود و همین فرآیند را برای سرویس سوم تکرار میکند. زمان کل پاسخدهی در این حالت، مجموع زمان پاسخدهی هر سه سرویس خواهد بود.
حالا سناریوی موازی را در نظر بگیرید. به جای انتظار برای هر درخواست، میتوان هر سه درخواست را به صورت همزمان ارسال کرد و سپس منتظر ماند تا همهی آنها به پایان برسند. در این حالت، زمان کل پاسخدهی تقریباً برابر با زمان طولانیترین درخواست خواهد بود. این کاهش زمان، به خصوص در سیستمهای میکروسرویسی که یک درخواست ممکن است به چندین سرویس دیگر وابسته باشد، بسیار چشمگیر است.
مزایای اصلی اجرای موازی عبارتند از:
-
کاهش زمان پاسخدهی (Latency): اصلیترین مزیت که منجر به بهبود تجربهی کاربری میشود.
-
افزایش توان عملیاتی (Throughput): با آزاد شدن سریعتر نخها (Threads)، سرور میتواند تعداد بیشتری از درخواستها را در یک بازهی زمانی مشخص پردازش کند.
-
استفادهی بهینه از منابع: در عملیاتهای I/O-bound (مانند درخواستهای شبکه یا دسترسی به دیتابیس)، نخها در حالت انتظار مسدود نمیشوند و میتوانند برای پردازش سایر کارها مورد استفاده قرار گیرند.
روشهای کلیدی برای پیادهسازی اجرای موازی
در پلتفرم .NET، به خصوص با استفاده از C# و ASP.NET Core، ابزارهای قدرتمندی برای مدیریت عملیات آسنکرون و موازی در اختیار داریم. دو روش اصلی برای این کار استفاده از Task.WhenAll و Parallel.ForEachAsync است.
۱. استفاده از async/await و Task.WhenAll
الگوی async/await سنگ بنای برنامهنویسی آسنکرون در C# است. این الگو به شما اجازه میدهد کدهایی بنویسید که از نظر ساختاری شبیه به کد همزمان (Synchronous) هستند، اما به صورت غیرمسدودکننده (Non-blocking) اجرا میشوند.
هنگامی که چندین عملیات آسنکرون مستقل دارید، Task.WhenAll بهترین ابزار برای اجرای موازی آنهاست. این متد یک آرایه از Task ها را به عنوان ورودی میگیرد و یک Task جدید برمیگرداند که تنها زمانی تکمیل میشود که تمام Task های ورودی تکمیل شده باشند.
بیایید یک مثال عملی را بررسی کنیم. فرض کنید میخواهیم اطلاعات یک کاربر، سفارشهای او و محصولات پیشنهادی را از سه سرویس مختلف دریافت کنیم.
[ApiController]
[Route("api/[controller]")]
public class DashboardController : ControllerBase
{
private readonly HttpClient _httpClient;
public DashboardController(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient();
}
[HttpGet("{userId}")]
public async Task GetDashboardData(int userId)
{
var userTask = _httpClient.GetStringAsync($"https://api.example.com/users/{userId}");
var ordersTask = _httpClient.GetStringAsync($"https://api.example.com/orders/user/{userId}");
var recommendationsTask = _httpClient.GetStringAsync($"https://api.example.com/products/recommendations/{userId}");
// اجرای همزمان هر سه درخواست
await Task.WhenAll(userTask, ordersTask, recommendationsTask);
var userData = await userTask;
var ordersData = await ordersTask;
var recommendationsData = await recommendationsTask;
var result = new
{
User = JsonSerializer.Deserialize
در این کد، ما سه Task را بدون await کردن آنها ایجاد میکنیم. این کار باعث میشود هر سه درخواست به صورت تقریباً همزمان شروع به اجرا کنند. سپس با استفاده از await Task.WhenAll(...) منتظر میمانیم تا هر سه عملیات به پایان برسند. در نهایت، با await کردن هر Task به صورت جداگانه، نتیجهی آنها را دریافت میکنیم (این await ها دیگر منتظر نمیمانند، زیرا Task ها قبلاً تکمیل شدهاند).
۲. استفاده از Parallel.ForEachAsync
گاهی اوقات شما نیاز دارید یک عملیات آسنکرون را برای تمام آیتمهای یک مجموعه به صورت موازی اجرا کنید. در این سناریو، Parallel.ForEachAsync (که از .NET 6 به بعد در دسترس است) یک گزینهی عالی و خوانا است.
این متد به شما اجازه میدهد تا درجهی موازیسازی (تعداد عملیاتی که به صورت همزمان اجرا میشوند) را نیز کنترل کنید که برای مدیریت منابع و جلوگیری از فشار بیش از حد به سرویسهای خارجی بسیار مفید است.
فرض کنید لیستی از شناسهی محصولات را داریم و میخواهیم اطلاعات کامل هر محصول را از یک سرویس دیگر به صورت موازی دریافت کنیم.
public class ProductService
{
private readonly HttpClient _httpClient;
public ProductService(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient();
}
public async Task> GetProductDetailsAsync(List productIds)
{
var products = new ConcurrentBag(); // استفاده از کالکشن Thread-safe
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = 5 // محدود کردن تعداد درخواستهای همزمان به ۵
};
await Parallel.ForEachAsync(productIds, parallelOptions, async (productId, cancellationToken) =>
{
var response = await _httpClient.GetStringAsync($"https://api.example.com/products/{productId}", cancellationToken);
var product = JsonSerializer.Deserialize(response);
if (product != null)
{
products.Add(product);
}
});
return products;
}
}
در این مثال، Parallel.ForEachAsync برای هر productId یک درخواست GET ارسال میکند. با تنظیم MaxDegreeOfParallelism، ما تضمین میکنیم که در هر لحظه حداکثر ۵ درخواست به سرویس مقصد ارسال میشود. این کار از ارسال ناگهانی صدها درخواست و احتمالاً از دسترس خارج کردن سرویس مقابل (Rate Limiting) جلوگیری میکند.
نکتهی مهم در این کد، استفاده از ConcurrentBag است. از آنجایی که حلقهی Parallel.ForEachAsync به صورت موازی اجرا میشود، چندین نخ به صورت همزمان تلاش میکنند تا به لیست products آیتم اضافه کنند. استفاده از یک کالکشن عادی مانند List در این شرایط امن نیست و میتواند منجر به خطای Race Condition شود. کالکشنهای موجود در فضای نام System.Collections.Concurrent برای چنین سناریوهایی طراحی شدهاند.

چالشها و ملاحظات مهم
اگرچه اجرای موازی مزایای فراوانی دارد، اما پیادهسازی آن نیازمند دقت و توجه به چالشهای بالقوه است.
ایمنی نخ (Thread Safety)
همانطور که در مثال Parallel.ForEachAsync دیدیم، بزرگترین چالش در برنامهنویسی موازی، مدیریت دسترسی همزمان به منابع مشترک است. این منابع میتوانند متغیرها، کالکشنها، سرویسهای تزریق شده (به خصوص با Scoped یا Singleton Lifetime) یا حتی اتصالات به پایگاه داده باشند.
-
راهکار: همیشه از کالکشنهای thread-safe (مانند ConcurrentDictionary, ConcurrentQueue, ConcurrentBag) برای جمعآوری نتایج استفاده کنید. برای محافظت از بخشهای حساس کد که به منابع مشترک دسترسی دارند، از مکانیزمهای قفلگذاری مانند lock یا SemaphoreSlim استفاده کنید.
مدیریت خطا (Error Handling)
وقتی چندین عملیات به صورت موازی اجرا میشوند، هر یک از آنها ممکن است با خطا مواجه شود. نحوهی مدیریت این خطاها اهمیت ویژهای دارد.
-
در Task.WhenAll، اگر یک یا چند Task با خطا مواجه شوند، Task.WhenAll یک AggregateException پرتاب میکند که شامل تمام خطاهای رخ داده در Task های فرزند است. شما باید این Exception را catch کرده و خطاهای داخلی آن را بررسی کنید.
-
همیشه در نظر داشته باشید که آیا شکست یک عملیات باید باعث لغو سایر عملیات شود یا خیر. میتوانید از CancellationTokenSource برای لغو هماهنگ عملیات موازی استفاده کنید.
استفاده از منابع و محدودیتها
اجرای موازی به معنای استفادهی بیشتر از منابع سیستم (CPU، حافظه و اتصالات شبکه) در یک بازهی زمانی کوتاه است.
-
Throttling و Rate Limiting: اگر در حال فراخوانی یک سرویس خارجی هستید، مراقب محدودیتهای نرخ درخواست (Rate Limiting) آن سرویس باشید. استفاده از MaxDegreeOfParallelism در Parallel.ForEachAsync یا استفاده از SemaphoreSlim برای محدود کردن تعداد Task های همزمان، یک راهکار موثر است.
-
Connection Pool Exhaustion: هر درخواست HttpClient ممکن است یک اتصال از Connection Pool را مصرف کند. اجرای صدها درخواست موازی میتواند به سرعت این Pool را خالی کرده و منجر به خطا شود. محدودسازی درجهی موازیسازی در اینجا نیز کلیدی است.
چه زمانی از اجرای موازی استفاده نکنیم؟
اجرای موازی همیشه بهترین راهحل نیست. در موارد زیر، اجرای ترتیبی ممکن است گزینهی بهتری باشد:
-
عملیاتهای وابسته به هم: اگر نتیجهی یک عملیات ورودی عملیات بعدی است، نمیتوان آنها را موازی کرد.
-
عملیاتهای CPU-bound سنگین: اگر عملیات شما به جای انتظار برای I/O، به شدت از CPU استفاده میکند (مانند محاسبات پیچیده)، اجرای موازی بیش از حد میتواند منجر به Context Switching زیاد و کاهش کارایی شود. در این موارد، استفاده از Parallel.ForEach (نسخهی همزمان) با دقت بیشتر توصیه میشود.
-
سادگی و خوانایی: برای عملیاتهای ساده و سریع، پیچیدگی اضافی ناشی از اجرای موازی ممکن است توجیهپذیر نباشد.
جمعبندی
اجرای موازی درخواستها یک تکنیک قدرتمند در جعبه ابزار هر توسعهدهندهی Web API است که میتواند به طور قابل توجهی عملکرد و مقیاسپذیری برنامهها را بهبود بخشد. با استفاده از ابزارهای مدرن C# و .NET مانند Task.WhenAll و Parallel.ForEachAsync، پیادهسازی این الگوها سادهتر از همیشه شده است.
با این حال، موفقیت در استفاده از این تکنیکها نیازمند درک عمیق مفاهیمی مانند async/await، ایمنی نخ، مدیریت خطا و محدودیتهای منابع است. با در نظر گرفتن این ملاحظات و انتخاب رویکرد مناسب برای هر سناریو، میتوانید سرویسهایی سریعتر، پاسخگوتر و کارآمدتر بسازید که تجربهی کاربری بهتری را برای کاربران نهایی به ارمغان میآورند.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.