روش‌های مختلف انجام چند کار به صورت همزمان در C# .NET - قسمت اول
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهار دقیقه

آیا تا به حال لیستی از دیتا داشته‌اید که بخواهید بر روی آنها کاری را انجام دهید؟ مثلا لیستی از مشتریان که باید برای تک تک آنها Pdf ای را بسازید، یا لیستی از مشتریان که باید برای تک تک آنها بیمه نامه صادر کنید، یا مثلا لیست اطلاعات بلیط‌های قابل فروش را گرفته‌اید و برای تک تک آنها می‌خواهید کمیسیون حساب کنید و ...

در اکثر مواقعی کاری که برای تک تک آیتم‌ها قرار است انجام شود، ساده است و با استفاده از یک حلقه foreach کار تمام میشود. اما در بعضی مواقع کار زمانبر است؛ حال یا به علت وجود کاری CPU bound مثل درست کردن Pdf و محاسبات، یا کار IO Bound است مثل ارسال یک HTTP Request به ازای هر مشتری، یا ذخیره کردن چیزی در دیتابیس که هم CPU bound است و هم IO bound و ترکیبی از مواردی که گفتیم را دارد.

فرض کنیم صد مشتری داریم و برای تک تک آنها میخواهیم کاری انجام دهیم. اگر از یک foreach ساده استفاده کنیم و هر عمل یک ثانیه طول بکشد، کل روال 100 ثانیه طول می‌کشد که جالب نیست.
public async Task Sample()
{
    var customers = await GetCustomersFromSomeWhere();

    foreach (var customer in customers)
    {
        await DoSomethingWithCustomer(customer);
    }
}
با اندکی جستجو در اینترنت به Task.WhenAll می‌رسیم و مشکلی که دارد این است که هر 100 کار را با هم شروع می‌کند که میتواند اثرات مخربی روی کلیت عملکرد سرور بگذارد:
public async Task Sample()
{
    var customers = await GetCustomersFromSomeWhere();
    await Task.WhenAll(customers.Select(c => DoSomethingWithCustomer(c)));
}
اگر چه می‌توانیم خودمان آیتم‌ها را دسته بندی کنیم و مثلا هر 25 تا 25 آنها را با هم پردازش کنیم، ولی این دسته بندی خیلی معقول نیست، چون RX اینجاست!
public async Task Sample()
{
    var customers = await GetCustomersFromSomeWhere();

    await customers.Select(c => Observable.FromAsync(() => DoSomethingWithCustomer(c))).Merge(maxConcurrent: 25);
}
به خاطر وجود maxConcurrent: 25 در دستور فوق، مشتری‌ها بسته به وضعیت کلی سرور، حداکثر 25 تا 25 تا پردازش می‌شوند، ولی ممکن است مثلا 10 تا 10 پردازش شوند. البته انتظار هوشمندی خیلی زیادی از آن نداشته باشید.

استفاده از Rx وقتی که دستورات داخل DoSomethingWithCustomer به صورت IO bound باشند (اتصال به دیتابیس و ارسال Http Request و ...) به خوبی جواب می‌دهد. ولی اگر دستورات داخل DoSomethingWithCustomer به صورت CPU bound باشند، مثل محاسبات یا ساختن Pdf و ... دیگر این روش جواب نمی‌دهد و اینجاست که باید از Task Parallel Library استفاده کنیم ( البته Task Parallel Libraray یا به اختصار TPL هم برای IO Bound و هم برای CPU Bound مناسب است).
برای استفاده از TPL داریم:
public async Task Sample()
{
    var customers = await GetCustomersFromSomeWhere();

    ActionBlock<Customer> action = new ActionBlock<Customer>(c => DoSomethingWithCustomer(c), new ExecutionDataflowBlockOptions
    {
        MaxDegreeOfParallelism = 25
    }); 

    foreach (var customer in customers)
    {
        action.Post(customer);
    }

    action.Complete();

    await action.Completion;
}
همانطور که می‌بینید، بحث 25 تا 25 تا اجرا کردن در اینجا نیز وجود دارد، با این تفاوت که بسیار هوشمندانه‌تر کارها را به صورتی پیش می‌برد که از منابع سرور به بهینه‌ترین شکل ممکن استفاده شود و همین TPL را هم برای اعمال IO bound و هم اعمال CPU bound مناسب می‌کند.

سایر گزینه هایی که داریم شامل Parallel Linq و Parallel.ForEach است که عموما برای کارهای CPU bound مناسبند.
گزینه‌هایی از قدیم نیز وجود دارند، مانند استفاده از Thread و Semaphore و ... که ابدا استفاده مستقیم از آنها توصیه نمیشود.
البته با TPL و RX میشود کارهای خیلی بیشتری نیز انجام داد و این دو فقط برای این سناریو ساخته نشده‌اند و همه جا جایگزین یکدیگر نیستند و هر دو دنیای وسیعی هستند که توصیه می‌کنم به هر دو نگاهی بیاندازید. همچنین TPL تا جایی که می‌دانم جزو مواردی است که در بیرون از دنیای NET. وجود ندارد و یکی از ارزشمندترین ویژگی‌های Unique در NET. است که به این سادگی چنین مسئله‌ای با این کیفیت حل شود.

توجه داشته باشید که اگر فرآیندی که برای تک تک Customer‌ها در مثال فوق قرار است انجام شود، خود یک روال سنگین و زمان بر باشد، بهتر است از روش‌های دیگری مبتنی بر Event processing و ابزارهایی چون Azure Service Bus یا Mass Transit استفاده کنیم که کمک می‌کنند اگر مثلا سه سرور داریم، بار پردازش آن 100 مشتری مثال ما، بین سه سرور هم پخش شود که این مورد پیچیدگی‌های خود را دارد و در اینجا که فرض بر این است که سناریو خیلی پیچیده و میزان بار خیلی زیاد نیست و همچنین نیازی هم به استفاده از این موارد و اضافه کردن پیچیدگی‌های بیشتر به برنامه نیست. در واقع TPL علیرغم کار بسیار ارزشمندی که می‌کند، در نهایت یک Nuget Package است که در یک دستگاه موبایل Android و با Xamarin نیز قابل استفاده است.

البته این همه داستان نیست. برای مثال در صورتی که فرآیندی بخواهد به صورت Concurrent / Parallel انجام شود و در انجام آن از Entity Framework Db Context استفاده شده باشد، کد به مشکل بر میخورد و خطا می‌دهد، چون یک Instance از DbContext مناسب انجام چند کار همزمان نیست. به واقع تمامی Objectهایی که Thread Safe نباشند، در روش‌های فوق به مشکل بر میخوردند. همچنین بحث مدیریت کردن Transaction در صورتی که بخواهید با دیتابیس هم کار کنید نیز خود مسئله‌ای است که باید حل شود.
حل مسائلی که گفته شد و ادغام کردن روش‌های فوق با بحث Dependency Injection و ... موضوع بحث قسمت بعدی این مطلب است.
  • #
    ‫۴ سال و ۱۰ ماه قبل، سه‌شنبه ۳۰ مهر ۱۳۹۸، ساعت ۱۲:۰۹
    یک روش دیگر در صف قرار دادن Taskها برای اجرای موازی، در مطلب «ParallelExtensionsExtras Tour – #7 – Additional TaskSchedulers» توسط خود تیمی که Taskها را برای اولین بار معرفی کرد، در فایل:
    C# and C++ and F# and VB\C#,C++,F#,VB\ParallelExtensionsExtras\TaskSchedulers\QueuedTaskScheduler.cs
    مثال‌های آن مطلب ارائه شده. از یک نمونه‌ی ساده شده‌ی آن در برنامه‌ی GitHubFolderDownloader برای دریافت لیستی از فایل‌ها استفاده کردم.
  • #
    ‫۴ سال و ۱۰ ماه قبل، سه‌شنبه ۳۰ مهر ۱۳۹۸، ساعت ۱۳:۳۲
    فرآیندی به ازای هر درخواست هست که باید اطلاعات از بانک اطلاعاتی فراخوانی شود (از جدول فروش، بلیط‌های آزاد)
    بعد اطلاعات در حافظه پردازش شود و بعدصندلی‌های خالی و پر با رنگ‌های متفاوت و دسترسی‌های متفاوت نمایش داده شود
    همانطور که شما گفتید حلقه foreeach زمانبر و باعث عدم اطمینان سیستم شده است. آیا این روش برای این سناریو قابل اجرا هست؟
    ** چون بعضی مواقع پیش میاد که 2 شخص یک صندلی رو درخواست میدهند
    • #
      ‫۴ سال و ۱۰ ماه قبل، سه‌شنبه ۳۰ مهر ۱۳۹۸، ساعت ۱۴:۴۰
      حقیقت درک نمیکنم for each که هر بار اجرای حلقه اش زمان بر باشه کجای سناریو ای که گفتید لازم میشه، ولی فرض کنیم لازمه. آموزه‌های بالا فقط باعث میشه کارها سریع‌تر و بهینه‌تر انجام بشن، ولی کمکی به اینکه یه صندلی رو دو نفر بگیرن نمیکنه و باید براش فکری کنید که به این مطلب ارتباطی هم پیدا نمیکنه.
      • #
        ‫۴ سال و ۱۰ ماه قبل، پنجشنبه ۲ آبان ۱۳۹۸، ساعت ۱۷:۴۸
        حس میکنم منظورشون اینطور هست که n نفر صندلی خاصی رو در آن واحد رزرو میکنند در ابتدا مشتری 1 که سریعتر اقدام کرده تا مرحله پرداخت جلو میره ولی خب، پرداخت با موفقیت انجام نمیشه و لغو عملیات رخ میده ،در اینجا مشتری دوم از صف بیرون کشیده میشه و همون عملیات نهایی کردن رزرو رو انجام خواهد داد و....
  • #
    ‫۴ سال و ۳ ماه قبل، جمعه ۲۳ خرداد ۱۳۹۹، ساعت ۰۱:۱۰
    در winform چطور میشه خروجی DoSomethingWithCustomer و لاگ زد
    شرح خطا :
    control accessed from a thread other than the thread it was created on