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

C++17 با معرفی سیاست‌های اجرایی موازی (Parallel Execution Policies) در هدر <execution>، این قابلیت را به‌صورت استاندارد در اختیار برنامه‌نویسان قرار داده است. این سیاست‌ها که در فضای نام std::execution تعریف شده‌اند، امکان استفاده از توان پردازش چند هسته‌ای را با تغییر تنها یک پارامتر در فراخوانی الگوریتم‌های STL فراهم می‌کنند .

برداری‌سازی پردازشی چیست؟

برداری‌سازی (Vectorization) رویکردی متفاوت از موازی‌سازی است که در سطح پایین‌تری از پردازش عمل می‌کند. در برداری‌سازی، یک دستور واحد (Single Instruction) بر روی چندین داده (Multiple Data) به‌طور هم‌زمان اعمال می‌شود؛ مفهومی که با عنوان SIMD (Single Instruction, Multiple Data) شناخته می‌شود. پردازنده‌های مدرن دارای مجموعه‌دستورات خاصی هستند که امکان انجام عملیات‌هایی مانند جمع یا ضرب را روی آرایه‌هایی از اعداد در یک سیکل پردازشی فراهم می‌کنند .

برای درک تفاوت، می‌توان موازی‌سازی را به چندین کارگر تشبیه کرد که هرکدام روی یک بخش از پروژه کار می‌کنند، درحالی‌که برداری‌سازی مانند این است که هر کارگر به‌جای حمل یک قطعه، بتواند چندین قطعه را به‌طور هم‌زمان حمل کند. در C++17، سیاست std::execution::par_unseq هر دو رویکرد را ترکیب می‌کند و به کتابخانه اجازه می‌دهد تا هم از چندین ریسه (موازی‌سازی) و هم از دستورات SIMD (برداری‌سازی) برای بهینه‌سازی اجرا استفاده کند .

ضمانت‌های پیشرفت و تفاوت سیاست‌ها

یکی از مفاهیم مهم در موازی‌سازی، ضمانت پیشرفت (Forward Progress Guarantee) است که مشخص می‌کند آیا ریسه‌های درگیر در اجرا، در نهایت به پیشرفت خود ادامه می‌دهند یا ممکن است متوقف شوند. سیاست std::execution::par ضمانت پیشرفت موازی (Parallel Forward Progress) را ارائه می‌دهد، به این معنا که اگر یک ریسه یک قدم بردارد، در نهایت قدم دیگری نیز خواهد برداشت. این ویژگی به ریسه‌ها اجازه می‌دهد تا وارد بخش‌های بحرانی شده و قفل‌ها را به‌درستی مدیریت کنند، زیرا ریسه‌ای که قفل را در دست دارد، در نهایت دوباره زمان پردازنده دریافت کرده و می‌تواند قفل را آزاد کند .

در مقابل، سیاست std::execution::par_unseq که امکان برداری‌سازی را نیز فراهم می‌کند، ضمانت پیشرفت ضعیف‌تر (Weakly Parallel Forward Progress) ارائه می‌دهد. در این حالت، تنها تضمین می‌شود که یکی از ریسه‌هایی که قدم برداشته، در نهایت قدم دیگری بردارد. این ضمانت به ریسه‌ها اجازه‌ی ورود به بخش‌های بحرانی یا گرفتن قفل را نمی‌دهد، زیرا ممکن است ریسه‌ای که قفل را در اختیار دارد، تا زمانی که ریسه‌ی دیگری اقدام به گرفتن قفل کند، دوباره زمان پردازنده دریافت نکند . به‌همین دلیل، استفاده از std::atomic و سایر ابزارهای همگام‌سازی در توابعی که با سیاست par_unseq فراخوانی می‌شوند، مجاز نیست، زیرا این عملیات‌ها با برداری‌سازی ناسازگار هستند .

مسئولیت برنامه‌نویس در برابر رخدادهای رقابتی

با وجود سهولت استفاده از سیاست‌های موازی، مسئولیت اصلی در برابر خطاهای همزمانی بر عهده‌ی برنامه‌نویس است. هنگام استفاده از سیاست‌های par و par_unseq، باید از دسترسی همزمان به داده‌های مشترک بدون همگام‌سازی مناسب جلوگیری کرد. برای مثال، افزودن عناصر به یک std::vector مشترک از درون یک حلقه‌ی موازی for_each، یک رخداد رقابتی است که می‌تواند به خرابی برنامه منجر شود. همچنین در صورت پرتاب شدن یک استثنا از درون تابع فراخوانی‌شده در الگوریتم، std::terminate فراخوانی خواهد شد و برنامه خاتمه می‌یابد .

انتخاب سیاست مناسب

انتخاب بین سیاست‌های seq، par و par_unseq به عوامل مختلفی بستگی دارد. seq برای مجموعه‌داده‌های کوچک یا عملیات‌هایی که سربار موازی‌سازی از مزایای آن بیشتر است، انتخاب مناسبی محسوب می‌شود . par برای محاسبات سنگین که نیاز به قفل‌گذاری و همگام‌سازی دارند و par_unseq برای عملیات‌های کاملاً مستقل و عددی که می‌توانند از بردارگونه‌سازی بهره ببرند، بهترین گزینه‌ها هستند. همچنین توجه داشته باشید که موازی‌سازی یک داروی جادویی نیست و همیشه منجر به افزایش سرعت نمی‌شود؛ پیاده‌سازی‌های مختلف، مانند MSVC، تنها برخی از الگوریتم‌ها را موازی می‌کنند و الگوریتم‌های ساده‌ای که محدود به پهنای باند حافظه هستند، ممکن است از موازی‌سازی سود قابل‌توجهی نبرند .

کلیدواژه ها : موازی‌سازی-برداری‌سازی-SIMD در C++-C++17 execution policies-ضمانت پیشرفت موازی-parallel forward progress-weakly parallel forward progress-تفاوت par و par_unseq-سیاست اجرایی C++17-الگوریتم‌های موازی C++17-مدیریت ریسه در C++17-اجرای همزمان الگوریتم‌ها-vectorization C++17-parallel STL-هدر execution در C++