مقدمه

در درس قبل، با استفاده از کلاس Parallel برای موازی‌سازی حلقه‌ها و عملیات آشنا شدیم. این روش بسیار قدرتمند است، اما می‌تواند برای کارهای مرتبط با داده، کمی طولانی باشد. LINQ یک سینتکس اعلانی و خوانا برای کار با داده‌ها فراهم می‌کند؛ چه می‌شد اگر می‌توانستیم به سادگی و تنها با یک تغییر کوچک، این کوئری‌های خوانا را به صورت موازی اجرا کنیم؟ این دقیقاً همان کاری است که LINQ موازی (Parallel LINQ) یا PLINQ برای ما انجام می‌دهد. PLINQ یک پیاده‌سازی موازی از عملگرهای استاندارد LINQ است که به طور خودکار داده‌ها را بین هسته‌های مختلف CPU تقسیم کرده و کوئری را به صورت موازی اجرا می‌کند تا سرعت را به حداکثر برساند.

از IEnumerable<T> تا ParallelQuery<T>

جادوی PLINQ در یک متد بسطی بسیار ساده به نام AsParallel() نهفته است. این متد که در فضای نام System.Linq قرار دارد، یک کالکشن عادی IEnumerable<T> را به یک منبع داده‌ی موازی از نوع ParallelQuery<T> تبدیل می‌کند. پس از این فراخوانی، تمام عملگرهای بعدی LINQ که بر روی آن زنجیره می‌شوند، از نسخه‌ی موازی خود در PLINQ استفاده خواهند کرد.

تبدیل یک کوئری LINQ به PLINQ

بیایید یک مثال عملی را ببینیم. فرض کنید می‌خواهیم یک عملیات محاسباتی سنگین را بر روی هر عنصر از یک آرایه‌ی بزرگ انجام دهیم.

اجرای ترتیبی (Sequential)

Copy Icon Program.cs
int[] numbers = Enumerable.Range(0, 100_000_000).ToArray();

// A complex calculation
double ComplexCalculation(int n)
{
    Thread.Sleep(1); // Simulate CPU-intensive work
    return Math.Sqrt(n);
}

// Standard, sequential LINQ query
var stopwatch = Stopwatch.StartNew();
var queryResult = numbers.Where(n => n % 2 == 0).Select(n => ComplexCalculation(n));
Console.WriteLine($"Sequential query took: {stopwatch.ElapsedMilliseconds} ms");

اجرای موازی (Parallel)

حالا برای موازی کردن این کوئری، تنها کاری که باید انجام دهیم افزودن یک فراخوانی به AsParallel() در ابتدای زنجیره است.

Copy Icon Program.cs
stopwatch.Restart();

// The ONLY change is adding .AsParallel() here.
var parallelResult = numbers.AsParallel()
    .Where(n => n % 2 == 0)
    .Select(n => ComplexCalculation(n));

Console.WriteLine($"Parallel query took: {stopwatch.ElapsedMilliseconds} ms");

اگر این کد را بر روی یک کامپیوتر با چند هسته‌ی پردازشی اجرا کنید، خواهید دید که نسخه‌ی موازی به طور چشمگیری سریع‌تر از نسخه‌ی ترتیبی اجرا می‌شود. PLINQ به طور خودکار منبع داده را به بخش‌های کوچکتر تقسیم کرده و هر بخش را روی یک نخ جداگانه پردازش می‌کند و در نهایت نتایج را با هم ترکیب می‌کند.

حفظ ترتیب با AsOrdered

یکی از نتایج جانبی موازی‌سازی این است که ترتیب عناصر در نتیجه‌ی خروجی لزوماً با ترتیب آن‌ها در منبع ورودی یکسان نخواهد بود، زیرا بخش‌های مختلف داده ممکن است با سرعت‌های متفاوتی پردازش شوند. در بیشتر موارد، این ترتیب اهمیتی ندارد. اما اگر حفظ ترتیب اصلی برای شما مهم است، می‌توانید از متد بسطی AsOrdered() به جای AsParallel() استفاده کنید.

Copy Icon Program.cs
var orderedParallelResult = numbers.AsParallel().AsOrdered()
                                       .Where(n => n < 10)
                                       .Select(n => n * n);

AsOrdered به PLINQ دستور می‌دهد که با وجود اجرای موازی، ترتیب عناصر را در نتیجه‌ی نهایی حفظ کند. این کار ممکن است کمی هزینه‌ی سربار اضافی داشته باشد، زیرا PLINQ باید نتایج را قبل از برگرداندن، مرتب کند.

چه زمانی از PLINQ استفاده کنیم (و نکنیم)؟

PLINQ یک ابزار قدرتمند است، اما یک راه حل جادویی برای همه‌ی مشکلات نیست.

  • بهترین کاربرد: برای کوئری‌های محاسباتی سنگین (CPU-bound) بر روی داده‌های موجود در حافظه (LINQ to Objects). اگر هر تکرار از کوئری شما (به خصوص در بخش Select یا Where) نیاز به محاسبات قابل توجهی دارد، PLINQ می‌تواند یک گزینه‌ی عالی باشد.
  • کاربردهای نامناسب:
    • عملیات I/O-Bound: مانند LINQ to SQL یا LINQ to Entities، موازی‌سازی در سمت کلاینت معمولاً فایده‌ای ندارد و ممکن است به پایگاه داده فشار بیهوده وارد کند.
    • کارهای بسیار کوچک و سریع: هزینه‌ی سربار موازی‌سازی (تقسیم کار و ترکیب نتایج) ممکن است از زمان اجرای خود کوئری بیشتر باشد و در نهایت باعث کندتر شدن برنامه شود.
    • کدهای غیر ایمن از نظر نخ (Non-Thread-Safe): مهم‌ترین نکته این است که PLINQ نیز شما را از مسئولیت ایمن‌سازی کد در برابر شرایط رقابتی معاف نمی‌کند. اگر عبارات لامبدای شما به داده‌های مشترک و قابل تغییر دسترسی دارند، باید خودتان آن دسترسی را با lock یا روش‌های دیگر همگام‌سازی کنید.

همیشه عملکرد را بسنجید! قبل از تصمیم‌گیری برای استفاده از PLINQ، عملکرد نسخه‌ی ترتیبی و موازی را با داده‌های واقعی خود مقایسه کنید تا مطمئن شوید که واقعاً بهبودی حاصل می‌شود.