برنامه نویسی موازی با OpenMP

آشنایی با OpenMP

Open Multi-Processing
ماهنامه شبکه - مهر 1389 شماره 115

گردآوری و تألیف: کیومرث سلطانی

اشاره:

شاید به ندرت بتوان در دنیای برنامه‌نویسی موازی رابطی خوشدست‌تر و ساده‌تر از OpenMP (سرنام Open Multi-Processing) یافت. این رابط، انعطاف‌پذیر و ساده بوده و همچنین می‌تواند برای توسعه برنامه‌های موازی روی پلتفرم‌های مختلف به‌کار‌ رود.OpenMP یک API است که می‌تواند از برنامه‌نویسی چندپردازنده‌ای به‌صورت «حافظه اشتراکی» (Shared-memory) روی C++، C، فرترن و درمعماری‌های مختلفی از جمله پلتفرم‌های ویندوز و یونیکس پشتیبانی کند. البته، تولیدکنندگان کامپایلر برای زبان‌های دیگر از جمله جاوا نیز امکان نوشتن برنامه با رابط OpenMP رافراهم کرده‌اند.

تاریخچه
در اوایل دهه 90 تعدادی از فروشندگان ماشین‌های حافظه اشتراکی یک هدف مشترک را مشخص ساختند که تولید یک زبان برنامه نویسی‌ بر ‌اساس فرترن بود، هدف از ساخت این زبان عبارت بود از:
1- کاربر بتواند در آن برنامه عادی سریال فرترن را نوشته و سپس با استفاده از directive‌ها مشخص سازد که چه حلقه‌ای باید موازی پیاده‌سازی شود.
2- موازی‌سازی حلقه در میان پردازنده‌های SMP وظیفه کامپایلر بوده و باید به‌صورت خودکار انجام شود.


پس از آن،  پیاده­سازی‌های اولیه‌ای از این هدف توسط شرکت‌های مختلف ارائه شد، اما عدم همگرایی آن‌ها و ضعف‌های تکنیکی باعث شد محبوبیت چندانی کسب نکنند. نخستین شرح ویژگی (‌specification‌) برای OpenMP که مخصوص فرترن 1 بود، در سال ۱۹۹۷ عرضه شد. یک سال بعد آن‌ها شرح ویژگی (‌specification) مخصوص ++C را نیز ارا‌ئه دادند. نسخه دوم فرترنی در سال ۲۰۰۰ و نسخه دوم ++C دو سال بعد از آن عرضه شدند. بعد از آن جهش بزرگی در روند توسعه OpenMP  ایجاد شد و به‌این ترتیب، در سال ۲۰۰۵ نسخه 2/5  OpenMP برای ++C و فرترن با هم عرضه شد. نسخه سوم نیز که هم‌اکنون مورد استفاده قرار می‌گیرد، در می ‌سال ۲۰۰۸ عرضه شد. OpenMP در‌ واقع یک پیاده‌سازی از multi-threading یا چند‌رشته‌ای است که در آن یک رشته ‌پردازشی  به‌عنوان master انتخاب شده و دیگر رشته‌های پردازشی زیردستی را fork می‌کند (شکل زیر).

 

سیستم Fork-Join مورد استفاده در OpenMP

در کد برنامه آن قسمتی که باید به‌ صورت موازی اجرا شود توسط دستورات خاص مشخص می‌شود. سپس با توجه به خواسته‌های برنامه وظایف رشته‌های‌ پردازشی  معلوم شده و هر کدام به یک پردازنده نسبت داده می‌شوند. پس از پایان کار رشته‌های‌ پردازشی به‌صورت موازی، عمل Join انجام شده و دوباره کنترل کار به master thread برمی‌گردد.

برند OpenMP در اختیار یک سازمان غیرانتفاعی (‌non-profit) با نام OpenMP ARB است که روی شرح ویژگی‌ها و تولید و تأیید نسخه‌های جدیدتر آن‌ها نظارت دارد. همچنین ARB به جمع آوری بودجه و برگزاری کنفرانس‌ها، کارگاه‌ها و رویدادهای مختلف دیگر برای کمک به پیشرفت OpenMP می‌پردازد. ARB مجموعه‌ای متشکل از اعضای ثابت و کمکی است. اعضای ثابت در‌واقع آن‌هایی هستند که نسبت به OpenMP و تولید محصولات مرتبط با آن علاقه و اهداف بلندمدتی دارند. در مقابل اعضای کمکی فقط به خود استاندارد علاقه‌مند بوده و برنامه‌ای برای ایجاد یا فروش محصولات OpenMP ندارند. هم‌اکنون در فهرست اعضای ثابت آن شرکت‌های مطرحی مانند AMD، اچ پی، آی بی ام، اینتل، اوراکل ، تگزاس اینسترومنتز و مایکروسافت به‌همراه نمایندگانشان دیده می‌شوند.

 

مقدمات
پیش از شروع در نظر داشته باشید که کدهایی که در این مقاله آورده شده‌اند، به زبان C هستند، اما به راحتی می‌توان ساختار کلی را برای زبان‌های دیگر نیز بسط داد. از مزایای اصلی OpenMP می‌توان به شباهت فراوان کد موازی برنامه و کد سریال آن اشاره کرد. در واقع، با استفاده از OpenMP کد سریال شما با اضافه شدن چند خط directive می‌تواند به یک کد موازی تبدیل شود. همچنین از مزایای دیگر OpenMP می‌توان از سادگی، استاندارد بودن، تطابق با پلتفرم‌های مختلف و محبوبیت ویژه در میان جامعه برنامه‌‌نویسان یاد کرد.

باید توجه داشت، OpenMP تضمین نمی‌کند که از حافظه اشتراکی استفاده بهینه خواهد کرد. همچنین مواردی مانند وابستگی داده‌ها،  race conditions یا deadlock‌ ها باید توسط خود برنامه­نویس در کد برنامه کنترل شود و OpenMP عموماً نمی‌تواند کاری درباره آن‌ها انجام دهد. همزمان­سازی ورودی و خروجی هنگام دسترسی موازی و چک کردن ترتیب اجرای کد برنامه نیز از جمله وظایف برنامه نویس است و از عهده OpenMP خارج است. به ‌این ترتیب، برنامه‌نویس باید ساختار کد و الگوریتم خود را کاملاً کنترل کرده و اطمینان حاصل کند که موارد ذکر شده در اجرای برنامه رخ نخواهد داد. استاندارد OpenMP بر‌اساس مدل برنامه­نویسی explicit بنا شده است که به برنامه‌نویس کنترل کاملی روی موازی‌سازی می‌دهد و سعی می‌کند تا جای ممکن از اعمال خودکار موازی­سازی جلوگیری کند. مواردی از قبیل رشته‌های‌ پردازشی داینامیک یا موازی تودر‌تو (nested parallelism) نیز در بعضی از پیاده‌سازی‌های OpenMP وجود دارد، اما در تمام پیاده‌سازی‌ها پشتیبانی نمی‌شود.

 

تشریح ساختار OpenMP

فرض کنید، یک حلقه بزرگ با محاسبات سنگین در اختیار دارید. بیشترین استفاده برنامه‌نویسی موازی در چنین حالت‌هایی به‌دست می‌آید. شما می‌توانید این حلقه را بین پردازنده‌های مختلف شکسته و به‌این ترتیب زمان اجرا را کاهش دهید. برای انجام این کار با OpenMP باید با دستورات مختلف آن آشنا باشید. این معرفی دستورها را در سه بخش به انجام خواهیم رساند:

1- directive های کامپایلر
2- روتین‌های runtime کتابخانه‌ای
3- متغیرهای محیطی

Directive‌ های کامپایلر
فرمت اصلی directive‌ های کامپایلر در++C به این صورت است‌:


#pragma omp   directive-name   [clauses]   newline

به عنوان مثال:

 #pragma omp parallel default(shared) private(i,j)

در صورتی که خط تعریف directive طولانی شد می‌توان با قراردادن یک backslash(\) به خط بعدی رفت.

ساخت یک بخش موازی
یک بخش موازی قطعه‌ای از کد است که توسط چندین رشته‌پردازشی  اجرا خواهد شد. ساختار کلی ایجاد یک بخش موازی مشابه فهرست1 است‌:

#pragma omp parallel [clause ...] newline
                     if (scalar_expression)
                     private (list)
                     shared (list)
                     default (shared | none)
                     firstprivate (list)
                     reduction (operator: list)
                     copyin (list)
                     num_threads (integer-expression)

 
   structured_block

 فهرست 1   

وقتی یک رشته‌پردازشی  به این خط می‌رسد، گروهی از رشته‌های‌ پردازشی را ایجاد کرده و خود master گروه می‌شود. master به رشته‌ای در درون گروه گفته می‌شود که thread number‌ اش صفر است. بعد از این نقطه وارد بخش موازی برنامه می‌شویم و به‌این ترتیب کد، بین رشته‌های مختلف پخش خواهد شد. دستور if در ساختار directive می‌تواند شامل یک شرط باشد که در صورت درست بودن آن برنامه به‌صورت موازی و در‌صورت برقرار نبودنش اجرای سریال برنامه ادامه می‌یابد. در فهرست2 یک مثال از ایجاد بخش موازی را مشاهده می‌کنید:


#include
#include
int main ()  {
    int num_threads, thread_id;
    #pragma omp parallel private(thread_id)
    {
        thread_id = omp_get_thread_num();
         printf(“Hello World - thread : %d\n”, thread_id);

        if (thread_id == 0)
        {
            num_threads = omp_get_num_threads();
             printf(“Number of threads : %d\n”, num_threads);
        }
    }
    return 0;{

 

فهرست 2

 ساختارهای work-sharing

یک ساختار work-sharing  اجرای کد یک بخش بسته و مشخص را بین اعضای تیمی از رشته‌های پردازشی که با آن بخش مواجه می‌شوند، تقسیم می‌کند.

 در واقع، ساختارهای work-sharing یا تقسیم کار، رشته‌پردازشی  جدیدی را ایجاد نمی‌کنند. آن‌ها تنها می‌توانند یک بخش از کد را بین رشته‌های‌ پردازشی مختلف پخش کرده و آن را به‌صورت موازی جلو برند. به طور کلی سه نوع ساختار تقسیم کار وجود دارد‌:

1-    Do/For

2-    Section

3-    Single

1- DO/for که برای تقسیم یک حلقه میان گروهی از رشته‌های‌پردازشی  به‌کار می‌رود و مصداقی از data parallelism است.

 

#pragma omp for [clause ...]  newline
                schedule (type [,chunk])
                ordered
                private (list)
                firstprivate (list)
                lastprivate (list)
                shared (list)
                reduction (operator: list)
                collapse (n)
                nowait

   for_loop

فهرست 3   

schedule مشخص می‌کند که تقسیم حلقه میان رشته‌های پردازشی  به چه صورت انجام می‌پذیرد. این تقسیم بندی می‌تواند به ‌صورت استاتیک، داینامیک، GUIDED یا RUNTIME صورت گیرد. در صورتی که NO WAIT/nowait به صورت صریح ذکر ‌شود،  رشته‌های‌ پردازشی  در پایان حلقه منتظر نمی‌مانند تا synchronize شوند. ORDERED نشان می‌دهد که حلقه باید به‌همان ترتیبی که در حالت سریال اجرا می‌شود، اجرا گردد. البته این به‌معنای غیرموازی بودن کار نیست. در‌واقع قرار گرفتن ORDERED در بدنه for نشان می‌دهد که ما یک بلاک ordered در درون کد برنامه داریم. به‌عنوان مثال به فهرست 4 توجه کنید‌:

 

#include
#include

int main( )
{
    int i;
    #pragma omp parallel
    {
        #pragma omp for ordered
        for (i = 0 ; i < 25 ; i++){
        #pragma omp ordered
            printf(“i=%d\n”, i);
        }
    }

}

فهرست 4

با مشاهده خروجی این کد می‌بینیم که تمام اعداد 0 تا 24 پشت سرهم در خروجی ظاهر می‌شوند، در حالی‌که اگر عبارت ordered نبود، ترتیب ظاهر شدن اعداد می‌توانست کاملاً درهم باشد. COLLAPSE یکی دیگر از عبارت‌هایی است که از نسخه 3 به OpenMP اضافه شده است. این عبارت یک آرگومان ورودی دریافت می‌کند. به‌عنوان مثال (‌COLLASPE(2 بیان می‌کند که حلقه‌های تو در تو تا عمق دو نیز شامل موازی سازی می‌شوند. به عنوان مثال به فهرست 5 دقت کنید‌:

#pragma omp parallel for
    for (int i = 0; i < 10; i++)
        for (int j = 0; j < 10; j++)   

           printf(“i=%d, j=%d, thread = %d\n”, i, j, omp_get_thread_num());
    }

فهرست 5

پس از اجرای این کد مشاهده می‌کنید که برای یک i خاص تمام ترکیب‌های آن با j به یک رشته‌پردازشی  می‌رود. یعنی به‌عنوان مثال i=1 , j=0  در اختیار   رشته‌پردازشی  شماره یک و i=1 , j=1 نیز در اختیار همان رشته ‌پردازشی خواهد بود. حال اگر در تعریف ساختار تقسیم کار for عبارت (‌COLLASPE(2 را نیز اضافه کنید، چنین ترتیبی به‌هم خواهد خورد. یعنی ممکن است i=1,j=0 به دست یک رشته‌پردازشی  و i=1 , j=1  به دست رشته ‌پردازشی  دیگر بیفتد. با استفاده از collapse می‌توان حلقه‌های درونی را نیز به‌صورت موازی تقسیم کرد.

 

 

 


2-  SECTIONS کارها را به بخش‌های جداگانه و مجزا قسمت کرده و سپس هر کدام را به یک رشته‌ پردازشی  جداگانه می‌دهد و مصداقی از functional parallelism است. فرمت کلی SECTIONS مشابه فهرست‌‌‌6 است‌:


#pragma omp sections [clause ...]  newline
                     private (list)
                     firstprivate (list)
                     lastprivate (list)
                     reduction (operator: list)
                     nowait
  {
  #pragma omp section   newline
     structured_block
  #pragma omp section   newline
     structured_block
 }


فهرست 6

با استفاده از این دستور بلاک‌هایی از کد ایجاد می‌شوند که می‌توانند میان رشته‌های‌ پردازشی مختلف تقسیم شوند. به‌این معنا که هر section توسط یکی از رشته‌های‌ پردازشی گروه اجرا می‌شود. ممکن است section‌ های مختلف توسط یک رشته ‌پردازشی اجرا شود، اما این تنها در حالتی ممکن است که section قبلی بتواند به سرعت اجرا شده و البته پیاده‌سازی برنامه نیز اجازه انجام این کار را بدهد. فهرست7 مثالی از کاربرد section است‌:


#include
#define N 20

main (){
    int i;
    int a[N], b[N], c[N], d[N];

    for (i=0; i < N; i++) {
        a[i] = i * 3;
        b[i] = i + 5;
    }

#pragma omp parallel shared(a,b,c,d) private(i)
    {
#pragma omp sections nowait
        {
#pragma omp section
            for (i=0; i < N; i++)
                c[i] = a[i] + b[i];

#pragma omp section
            for (i=0; i < N; i++)
                d[i] = a[i] * b[i];
        }

    }

}

فهرست 7

در این جا ضرب داخلی و جمع دو آرایه در دو رشته ‌پردازشی جداگانه انجام می‌شود.

3- SINGLE بیان می‌کند این قسمت از کد باید تنها توسط یک رشته ‌پردازشی در گروه اجرا شود و بقیه رشته‌های‌ پردازشی از روی آن عبور کنند. شاید بیشترین استفاده این directive برای مواردی از قبیل عملیات I/O باشد که رشته ‌پردازشی  safe  نیستند. فرمت کلی single مشابه فهرست8 است.


#pragma omp single [clause ...]  newline
                   private (list)
                   firstprivate (list)
                   nowait
                   
     structured_block

 

 

فهرست 8    

به‌طور پیش فرض رشته‌های‌ پردازشی که بلاک کد را اجرا نمی‌کنند، در پایان بلاک منتظر می‌شوند تا کار رشته ‌پردازشی داخل single directive تمام شود. اما می‌توان با گذاشتن عبارت nowait این موضوع را ملغی کرد.

 

 

 

 

 

همگام‌سازی در OpenMP

اعمال همگام‌سازی بین رشته‌های‌ پردازشی مختلف یکی از مهم‌ترین بخش‌های برنامه‌نویسی هم‌روند است. OpenMP نیز ساختار‌های مختلفی را برای کنترل تعامل رشته‌های‌ پردازشی بر سر منابع مشترک، فراهم می‌آورد. در این بخش به معرفی بعضی از این ساختارها می‌پردازیم.

Master Directive: این عبارت مشخص کننده منطقه‌ای از کد است که تنها باید به وسیله master thread اجرا شود. تمام رشته‌های‌ پردازشی دیگر باید از روی این بخش از کد عبور کنند(فهرست9).


#pragma omp master  newline
  structured_block

فهرست 9  

Critical Directive: این عبارت بخشی از کد را مشخص می‌کند که تنها باید به‌وسیله یکی از رشته‌های‌ پردازشی در هر زمان اجرا شود. به‌بیان دیگر دو رشته ‌پردازشی نمی‌توانند با هم در این بخش از کد حضور داشته باشند (فهرست10).


#pragma omp critical [ name ]  newline

   structured_block

فهرست 10  

اگر یک رشته‌پردازشی داخل یک منطقه critical حضور داشته باشد و رشته ‌پردازشی دیگری سعی کند به آن وارد شود، باید منتظر بماند تا  رشته ‌پردازشی در حال اجرا، کارش به اتمام رسیده و از critical section خارج شود. با استفاده از نام برای بخش‌های critical می‌توان چندین بخش critical را در یک برنامه ایجاد کرد. با بخش‌هایی که نام نداشته باشند یا بخش‌هایی که نام یکسان داشته باشند، مانند یک بخش برخورد می‌شود. فهرست11 مثالی از ایجاد بخش‌های critical را نشان می‌دهد‌:

 

 


#include
#include

main()
{
    int x;
    x = 0;
    int i;

#pragma omp parallel shared(x) private(i)
 {
#pragma omp critical
{
        printf(“%d\n”,x);
        for(i=0;i<25;i++)
            x++;
}
}
}

فهرست 11

با اجرای این برنامه در خروجی عدد 0 و 25 را مشاهده می‌کنیم که نشان می‌دهد، ابتدا یکی از رشته‌های‌ پردازشی وارد critical section شده و حلقه for را کامل طی می‌کند و سپس رشته ‌پردازشی  دیگر وارد این بخش می‌شود (اجرا برای دو رشته‌ پردازشی‌).

 

BARRIER Directive: این عبارت تمام رشته‌های‌ پردازشی داخل یک گروه را synchronize می‌کند. در ‌واقع وقتی یک رشته ‌پردازشی  به یک  barrier directive می‌رسد، منتظر می‌شود تا تمام رشته‌های‌ پردازشی دیگر نیز به این نقطه برسند. سپس بعد از رسیدن همه رشته‌های‌ پردازشی اجرای برنامه دوباره جریان می‌یابد.

 

 

 

#pragma omp barrier  newline

تمام رشته‌های‌ پردازشی  عضو یک گروه باید بخش barrier را اجرا کنند.

 ATOMIC Directive : این عبارت بیان کننده آن است که یک بخش خاص از حافظه باید به صورت اتمیک به روز شود و به این ترتیب نمی‌توان اجازه داد، چندین رشته‌ پردازشی  به‌صورت همزمان روی آن بنویسند. در واقع با به کار بردن این عبارت یک critical section کوچک ساخته‌ایم(فهرست12).


#pragma omp atomic  newline
   statement_expression

فهرست 12  

دستوری که در ادامه atomic directive می‌آید، فقط باید یک عبارت تنها و یک خطی باشد. به‌این ترتیب، در صورتی که نیاز است عملیات متعددی به‌صورت اتمیک اجرا شود، باید از critical section استفاده کرد.

 

 

 

 

 

 

 

 

 

 

Data Scope Attribute Clauses

در این بخش به بررسی دقیق تر عبارت‌هایی می‌پردازیم که در تعریف directive ها مشاهده کردید. عبارت‌هایی از قبیل private، first private، reduction و‌... . درک صحیح حوزه‌های داده (Data Scope) و نحوه استفاده از عبارت‌های مختلف در یک برنامه OpenMP برای برنامه‌نویس ضروری است. با توجه به این‌که OpenMP بر‌اساس مدل برنامه‌نویسی حافظه مشترک بنا شده است، بیشتر متغیرها به صورت پیش‌فرض اشتراکی تلقی می‌شوند. با این حال، در موارد بسیاری از جمله متغیری که در بدنه حلقه for به کار می‌رود، ما به متغیرهایی خصوصی نیاز داریم که میان  رشته‌های‌ پردازشی  به اشتراک گذاشته نشوند. در این بخش معنا و مفهوم بعضی از این عبارت ها را بیان کنیم. عبارت reduction نیز در مثال‌های پایان مقاله توضیح داده شده است.

 PRIVATE: عبارت private متغیرهایی را مشخص می‌کند که برای هر رشته‌پردازشی  به صورت شخصی به کار می‌روند. عبارت به نسبت مشابه threadprivate است که مانند private برای هر رشته‌پردازشی مقدار مخصوص خود را دارد. اما تفاوت اصلی آن با private در این است که فقط مخصوص یک بلاک خاص نیست و در تمام مدت فعالیت یک  رشته‌پردازشی مقدارش consistent بوده و باقی می‌ماند. درفهرست13 می‌توانید تفاوت این دو عبارت را مشاهده کنید.


#include
#include

int  a,b,c;

#pragma omp threadprivate(a)

main ()  {

    printf(“1st Parallel Region:\n”);
    c=15;
#pragma omp parallel private(b,c)
    {
        int tid= omp_get_thread_num();
        a=10*tid;
        b=5*tid;
        c=2*tid;
        printf(“a b c =%d %d %d\n”,a,b,c);
    }

    printf(“************************************\n”);
    printf(“Master thread doing serial work here\n”);
    printf(“************************************\n”);

    printf(“2nd Parallel Region:\n”);
#pragma omp parallel private(b)
    {
        int tid=omp_get_thread_num();
        c+=(tid+2);
        b=tid;
        printf(“a b c =%d %d %d\n”,a,b,c);
    }

}

فهرست 13

این برنامه مثال بسیار خوبی از نحوه عملکرد متغیرها با scope‌های مختلف است. خروجی برنامه باید چیزی شبیه فهرست 14 باشد (برای اجرا با دو رشته ‌پردازشی‌).


1st Parallel Region:
a b c =0 0 0
a b c =10 5 2
************************************
Master thread doing serial work here
************************************
2nd Parallel Region:
a b c =10 1 18
a b c =0 0 20

فهرست 14

برای هر متغیر روند تغییر مقادیر را دنبال می‌کنیم. ابتدا متغیر a را در نظر بگیرید. در منطقه موازی شماره یک مقدار این متغیر برای رشته ‌پردازشی  شماره صفر، 0 و برای رشته ‌پردازشی شماره یک، 10 می‌شود. با مشاهده خروجی منطقه موازی دوم مشاهده می‌کنیم که مقادیر a برای رشته‌های‌ پردازشی  در این بخش نیز باقی مانده‌اند و در حقیقت، مقادیرشان را حفظ کرده‌اند. این دقیقاً همان کاری است که متغیرهای threadprivate انجام می‌دهند. حال متغیر b را در نظر بگیرید. این متغیر برای رشته‌های‌ شماره صفر و یک در بخش اول به ترتیب مقادیر 0 و 5 را اختیار کرده است. در بخش دوم نیز این متغیر به عنوان private تعریف شده است. پس برای هر  رشته ‌پردازشی مقدار مخصوص به خود را دارد که برابر شماره رشته‌ پردازشی  است.

متغیر c نیز وضعیت جالبی دارد. این متغیر در منطقه موازی اول به‌صورت خصوصی تعریف شده است بنابر‌این، برای رشته‌های‌ پردازشی  مختلف مقادیر مختلف نیز گرفته است. پس از بیرون آمدن از منطقه موازی شماره یک مقدار c برابر 15 می‌شود. زیرا قبل از اجرای این بخش موازی مقدار آن این عدد بوده است و مقادیر تعریف شده به‌صورت private ، پابرجا نیستند و بعد از اتمام بلوک موازی از بین می‌روند. پس در ابتدای منطقه موازی مقدار c برابر 15 است. با ورود به منطقه موازی چون در مورد c چیزی ذکر نشده است، این متغیر shared محسوب می‌شود. پس مقدار نهایی آن برابر 15+2+1+2=20 می‌شود.

همچنین دو عبارت مشابه به private دیگر نیز وجود دارد: firstprivate و lastprivate. عبارت firstprivate ، رفتار private را با مقداردهی خودکار متغیرهای تعریف شده در آن تلفیق می‌کند. یعنی متغیرهای تعریف شده در بدنه این دستور هنگام ورود به ساختار تقسیم کار، با توجه به نوع‌ خود یک مقدار اولیه می‌گیرند. عبارت lastprivate نیز رفتار private را با کپی کردن آخرین اجرای حلقه یا آخرین section ترکیب می‌کند. به‌عنوان مثال، فرض کنید متغیر a توسط دستور (lastprivate(a مشخص شده است. آخرین عضو یک گروه از رشته‌های‌ پردازشی  که آخرین iteration حلقه را اجرا می‌کند یا رشته‌های‌ پردازشی  که آخرین section را در ساختار کلی section ها اجرا می‌کند، مقداری را برای متغیر a در اختیار دارد. این مقدار پس از پایان بخش موازی کد، به a اصلی نسبت داده می‌شود. یعنی آخرین مقداری که a در بخش موازی داشته، پس از پایان بخش موازی به a نسبت داده می‌شود.

SHARED: عبارت shared مشخص‌کننده متغیرهایی است که باید در میان تمام رشته‌های‌ پردازشی به اشتراک گذاشته شود. این متغیرها در محلی از حافظه نگه‌داری می‌شوند که تمامی رشته‌های‌ پردازشی  می‌توانند به آن دسترسی داشته باشند و در آن بنویسند یا از آن بخوانند. برنامه نویس وظیفه دارد تا با استفاده از مکانیزم های مختلفی از جمله ساختن critical section‌ها اطمینان حاصل کند که رشته‌های‌ پردازشی  به‌درستی از این متغیرها استفاده می‌کنند. برای مشخص کردن یک scope پیش فرض برای تمام متغیرها می‌توان از عبارت (‌default(shared | none استفاده کرد.

 

 

 

روتین های run-time کتابخانه‌ای

استاندارد API OpenMP ‌هایی را برای فراخوانی‌های کتابخانه‌ای فراهم می‌آورد. این روتین‌ها به‌طور عموم در چهار گروه خلاصه می‌شوند‌:

1- پرس و جو درباره تعداد رشته‌پردازشی‌/ پردازنده ها‌، تنظیم رشته‌های‌ پردازشی مورد استفاده
2- روتین‌های قفل کردن عمومی (سمافورها)
3- روتین‌های مربوط به زمان
4- روتین‌های تنظیم متغیرهای محیطی و موارد مربوط به آن‌ها


در این‌جا به معرفی تعدادی از این روتین‌ها می‌پردازیم. توجه داشته باشید که تمام این روتین‌ها در زبان C++/C با حروف کوچک هستند‌:

OMP_SET_NUM_THREADS: برای تنظیم تعداد رشته‌های‌ پردازشی که در بخش بعدی موازی به‌کار خواهند رفت.

مثال‌:

 omp_set_num_threads(10)

 توجه داشته باشید، این روتین تنها می‌تواند از درون بخش سریال کد فراخوانی شود. این فراخوانی نسبت به متغیر محیطی OMP_NUM_THREADS اولویت دارد و در‌صورتی که جفت آن‌ها تنظیم شده باشند مقدار این فراخوانی مورد توجه قرار می‌گیرد.

OMP_GET_NUM_THREADS: این روتین تعداد  رشته‌های‌ پردازشی  را بر می‌گرداند که هم اکنون در داخل بخش موازی‌ای که این فراخوانی از درون آن انجام می‌شوند، در حال فعالیت هستند. به‌یقین در‌صورتی که این فراخوانی از درون بخش سریال کد یا بخش nested ای از کد موازی که سریال مانده است انجام شود، عدد یک را بر می‌گرداند.


OMP_GET_THREAD_NUM: شماره رشته‌های‌ پردازشی  از گروه، که این فراخوانی از درون آن انجام می‌شود. این عدد می‌تواند بین صفر تا OMP_GET_NUM_THREADS-1 باشد. عدد master thread نیز همواره صفر است.


OMP_GET_NUM_PROCS: تعداد پردازنده‌هایی که در اختیار برنامه قرار دارد.

OMP_IN_PARALLE: مشخص می‌کند این بخش از کد که در حال اجرا است موازی می‌باشد یا خیر. در صورت موازی بودن عددی غیر صفر و در غیر این صورت صفر باز می‌گرداند.


OMP_SET_NESTED: برای فعال‌کردن یا غیر فعال کردن موازی‌سازی تو در تو. اگر به یک مقدار غیرصفر مقدار‌‌دهی شود، موازی‌سازی تو در تو فعال شده و در غیر این صورت غیرفعال می‌شود. به‌صورت پیش فرض موازی تو در تو غیرفعال است. روتین OMP_GET_NESTED نیز برای مشخص کردن این‌که موازی تودرتو فعال است یا خیر به‌کار می‌رود.

OMP_INIT_LOCK: برای ساخت یک قفل جدید که با یک متغیر قفل در ارتباط است، به‌کار می‌رود مثال‌:

void omp_init_lock(omp_lock_t *lock)
void omp_init_nest_lock(omp_nest_lock_t *lock)

حالت پیش فرض قفل‌های ساخته شده، unlocked است. همچنین روتین OMP_DESTROY_LOCK برای جدا کردن یک قفل از یک متغیر قفل به کار می‌رود.

OMP_SET_LOCK: این روتین باعث می‌شود اجرای یک   رشته‌پردازشی  تا زمانی که این قفل آزاد باشد، متوقف شود. نحوه فراخوانی به این صورت است‌:

void omp_set_lock(omp_lock_t *lock)
void omp_set_nest__lock(omp_nest_lock_t *lock)


همچنین روتین OMP_UNSET_LOCK برای آزاد کردن یک قفل در رشته‌پردازشی  در حال اجرا به‌کار می‌رود. در حقیقت، قفلی که توسط این متد آزاد می‌شود. می‌تواند باعث شود اجرای یک  رشته‌ پردازشی  که منتظر آزاد شدن این قفل بوده اس

/ 0 نظر / 12 بازدید