مضى على الشبكة و يوم من العطاء.

"الإشارات" SEMSPHORE و دورها في مزامنة المعلومات مع أمثلة بللغة ال c

ibadaibada is verified member.

./عضو جديد
>:: v1p ::<

السمعة:

بسم الله الرحمن الرحيم

السلام عليكم و رحمة الله تعالى و بركاته


في هذا الموضوع سنشرح الإشارات

فما هي الإشارات ؟

الإشارات هي في العادة متغير عددي يستخدم لتنظيم حركة العمليات (processes) أو الخيوط (threads) لإستخدام مورد

قيمة المتغير العددي و دلالاته :
صفر (0): تعني أن المورد غير متاح، والعمليات الأخرى يجب أن تنتظر.
عدد موجب: تعني أن المورد متاح ويمكن للعمليات الوصول إليه.


بحيث تحرص على عدم حصول أي من المشاكل الأتية :
حالات التنافس (Race condition) : في هذه الحالة يمكننا تخيل خيطان يريدان قرءاة نفس العدد و زيادة مثلا ب 10 إذا كانت القيمة المبدأية هي 0 في النتيجة المنطقية ستكون 20 لكن إذا دخل الخيط أول مع الخيط الثاني في نفس الوقت كلاهما سيجد القيمة 0 فعندما يزيدان لها في نفس الوقت سنجد النتيجة النهائية 10 و هي مختلفة عن النتيجة المنطقية
مثال :
C:
#include <stdio.h>
#include <pthread.h>

int x = 0;

void *first(void *arg) {
    for(int i = 0 ; i< 20000 ;i++){
        x++ ;
    }
}

void *second(void *arg) {
for(int i = 0 ;  i< 20000; i++){
    x++ ;
}
}

int main() {
    pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, first, NULL);
    pthread_create(&thread2, NULL, second, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    printf("The result of x: %d\n", x);

    return 0;
}
النتيجة :
Bash:
┌──(kali㉿kali)-[~/Desktop]
└─$ ./a.out          
The result of x: 30594
                                                                                                                                             
┌──(kali㉿kali)-[~/Desktop]
└─$ ./a.out
The result of x: 30328
                                                                                                                                             
┌──(kali㉿kali)-[~/Desktop]
└─$ ./a.out
The result of x: 31793
                                                                                                                                             
┌──(kali㉿kali)-[~/Desktop]
└─$ ./a.out
The result of x: 22877
                                                                                                                                             
┌──(kali㉿kali)-[~/Desktop]
└─$ ./a.out
The result of x: 28198

أرقام غريبة
الحل : إيقاف أحد الخيوط حتى يكمل أخر عمله تم نجعله يستئنف عمليته
مشاكل التزامن (Synchronization Issues): فد يحتاج خيط مثلا لينتظر أن يقوم خيط أخر بتجهيز شئ كنتيجة أو معلومة مثلا ليقوم هو بإستخدامها المشكلة تكمن في تحمس الخيط أول و إنطلاقه قبل أن تجهز النتيجة

التنافس على الموارد (Resource Contention) : في هذه الحالة تحاول العديد من الخيوط الوصول لمعلومة و التغيير فيها مما يؤدي إلي تلف البيانات مثلا محاولتها للكتابة في ملف واحد بدون مزامنة سيؤدي لتلف المعلومات

مشاكل أخرى مشهورة : عشاء الفلاسفة , المنتج و المستهلك


في هذه المقالة سنرمز ل المنطقة الحرجة (Critical Section) ب المورد فما هي المنطقة الحرجة
المنطقة الحرجة (Critical Section) :
هي الجزء من البرنامج الذي يتطلب وصولًا حصريًا لمورد مشترك بين عدة خيوط (threads) أو عمليات (processes).
و سنرمز للعميات و الخيوط بمصطلح واحد و هو الخيوط
أنواع الإشارات : هنالك نوعين و هما :
الإشارة الثنائية (Binary Semaphore):أي قيمته متغيرة بين 0 و الواحد و هذا يدل على أن المورد يمكن الوصول إليه بواسطة خيط واحد في أن واحد.

الإشارة العدّدية (Counting Semaphore): أي أن قيمته يمكن أن تكون أكبر من الواحد و هذا يدل على أن المورد يمكن أن يتم تغييره من قبل عدة خيوط أو عمليات في أن واحد.

العمليات الأساسية للإشارات :
p : تأخد s ك مدخل تستخدم هذه الدالة لإنتظار الإشارة أي تقوم بتحقق من العدد s إذا وجدته 0 تقوم بإنتظار أما إذا وجدته أكبر من الصفر فإنها تقوم بإنقاص واحد لقيمة s إستدعائها يدل على أن العملية تريد إستخدام مورد معين
من هذا الوصف يمكننا برمجتها
الكود :
C:
void  p(int* s){
    while(*s ==0 ){
        //لا تفعل شئ
    }
    *s -= 1;
}
v : تأخد s ك مدخل لها تستخدم هذه الدالة لتحرير إشارة أخرى أي أنها تقوم برفع قيمة s بواحد إستدعائها يدل أن العملية أكملت إستخدامها لموردا ما و من هذا الوصف يمكن برمجتها
الكود :
C:
void v(int*s ){
    *s =+1
}

قد يتسائل أحدكم بإستخدام هذه الدوال مالذي سيمنع من حصول المشكلة التي ناقشنها من قبل مشكلة " حالات التنافس " في الحقيقة لا شئ سيمنع لأن هذا الكود هو مثال لكيفية عمل الدوال التي سنقوم بإستخدامها و التي تمت برمجتها من قبل
, هذه الدوال ستمنع هذه المشكلة لأن نصها البرمجي عبارة عن عمليات ذرية أو كمية


ماهي العمليات الذرية :
في سياق البرمجة، الكود الذري (Atomic Code) يشير إلى العمليات أو الأوامر التي تُنفذ بشكل كامل وغير قابل للتقسيم , دون أن يتم مقاطعتها أو تغيير حالتها خلال التنفيذ. يُستخدم الكود الذري لضمان التزامن بين العمليات المتعددة في بيئات متعددة الخيوط أو العمليات

من هذه الدوال التي تم تعريفها مسبقا من قبل النظام :
C:
      #include <semaphore.h>

       int sem_init(sem_t *sem, int pshared, unsigned int value);
        //تأخد ثلاتة مدخلات
        //لأول هو المؤشر على إشارة
        // الثاني هو يمكن أن يكون قيمتان 0 أ و 1 , 0 :يعني أنه سيكون مشاركا بين الخيوط أما 1 فيعني أنه سيكون مشاركا بين العمليات
        //القيمة أولية و هي تعتمد على المشكل التي نريد حلها
        //للمزيد من المعلومات أنظر لصفحة : https://man7.org/linux/man-pages/man3/sem_init.3.html




// البقية لديها مدخل واحد وهو مؤشر على الإشارة
int sem_wait(sem_t *sem);

        int sem_post(sem_t *sem);

        int sem_destroy(sem_t *sem);


بإستخدام هذه الدوال سنحل المشكلة التي قابلتنا في الأول

الكود :

C:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem;
int x = 0;

void *first(void *arg) {
    for(int i = 0 ; i< 20000 ;i++){
    sem_wait(&sem) ;
        x++ ;
    sem_post(&sem );
    }
}

void *second(void *arg) {
for(int i = 0 ;  i< 20000; i++){
        sem_wait(&sem) ;
        x++ ;
    sem_post(&sem );
}
}

int main() {
    pthread_t thread1, thread2;
 

  sem_init(&sem , 0 , 1 ) ;
//القيمة أولية ل sem هي 1
    pthread_create(&thread1, NULL, first, NULL);
    pthread_create(&thread2, NULL, second, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    sem_destroy(&sem);
    printf("The result of x: %d\n", x);

    return 0;
}


النتيجة :
Bash:
┌──(kali㉿kali)-[~/Desktop]
└─$ ./a.out          
The result of x: 40000
                                                                                                                                             
┌──(kali㉿kali)-[~/Desktop]
└─$ ./a.out
The result of x: 40000
                                                                                                                                             
┌──(kali㉿kali)-[~/Desktop]
└─$ ./a.out
The result of x: 40000
                                                                                                                                             
┌──(kali㉿kali)-[~/Desktop]
└─$ ./a.out
The result of x: 40000
                                                                                                                                             
┌──(kali㉿kali)-[~/Desktop]
└─$ ./a.out
The result of x: 40000
                                                                                                                                             
┌──(kali㉿kali)-[~/Desktop]
└─$ ./a.out
The result of x: 40000
                                                                                                                                             
┌──(kali㉿kali)-[~/Desktop]
└─$ ./a.out
The result of x: 40000



كيف تم تنفيذ البرنامج :
كلا الخيطين تم تشغيلهما
نعرف أن العملية ذرية اي أنه واحد منها سيقوم بتشغيل دالة ال (sem_wait) p أولا لنفترض أن الخيط الأول هو الذي قام بها أولا :

الخيط الأول : يتحقق من قيمة ال s يجدها 1 لأننا قمنا بوضع قيمته أولية 1 لذلك لن ينتظر و سيقوم بإنقاص واحد للقيمة s لذلك s= 0
الخيط الثاني : يتحقق من قيمة s يجدها 0 فينتظر
الخيط الأول : يرفع من قيمة x لذلك x = 1تم ينادي v (sem_post) و التي سترفع من قيمة s لذلك s= 1
الخيط الثاني: سيجد أن s لم يعد يساوي الصفر فيخرج من الدالة p بعد إنقاص قيمة s فنجد s = 0 مجددا
الخيط الأول : ينادي P يجد s = 0 فينتظر

الخيط الثاني : يدخل لرفع قيمة x لذلك x =2 و ينادي v فنجد s = 1
.... لا ننسي أن كلاهما في كل دورة يتحققون من قيمة i




مثال أخر في هذا المثال سيحاول كل من الخيط أ و ب أن يكتبوا أرقاما بحيث يكتب الخيط أ أرقاما زوجية و الخيط ب أرقاما فردية بترتيب حتى ال 100 فنتيجة ستكون كالأتي 01234567891011...
سنقوم بكتابتها في ملف لنظهر المشكلة بوضوح
الكود بدون إشارات :
C:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <fcntl.h>
sem_t sem;
int fd ;
void *printodd(void *arg) {
    for(int i = 1 ; i<= 100 ;i+=2){

        dprintf(fd, "%d\n", i);
    //الكتابة في ملف
    }
}

void *printeven(void *arg) {
for(int i = 0 ;  i<=100; i+=2){

        dprintf(fd, "%d\n", i);
//الكتابة في ملف
}
}

int main() {
    pthread_t thread1, thread2;

     fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    //فتح ملف
    pthread_create(&thread1, NULL, printodd, NULL);
    pthread_create(&thread2, NULL, printeven, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);



    return 0;
}

النتيجة :
0
2
4
6
8
10
12
14
16
1
18
3
20
22
5
24
7
26
9
11
13
15
28
17
30
19
32
21
34
23
36
25
38
27
40
29
42
31
44
33
46
35
48
37
50
39
52
41
54
43
56
45
58
47
60
49
62
51
64
53
66
55
68
57
70
59
72
61
74
63
76
65
78
67
80
69
82
71
84
73
86
75
88
77
90
79
92
81
94
83
96
85
98
87
89
91
93
95
97
99
100

كلما تشغلون الكود ستتحصلون على نتيجة مختلفة ستكون في لأعلب خاطئة
طبعا هذه المشكلة قد طرحناها و هي مشكلة التزامن بحيث يحتاج الخيط الثاني الخيط الاول لإكمال مهمته
بإستخدام الإشارات :

الكود :

C:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <fcntl.h>
sem_t odd , even ;
int fd ;
void *printodd(void *arg) {
    for(int i = 1 ; i<= 100 ;i+=2){
    sem_wait(&odd) ;
        dprintf(fd, "%d\n", i);
    sem_post(&even);
    }
}

void *printeven(void *arg) {
for(int i = 0 ;  i<=100; i+=2){
        sem_wait(&even) ;
        dprintf(fd, "%d\n", i);
    sem_post(&odd );
}
}

int main() {
    pthread_t thread1, thread2;
    sem_init(&even , 0 , 1 ) ;
    sem_init(&odd , 0 , 0 ) ;
     fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    pthread_create(&thread1, NULL, printodd, NULL);
    pthread_create(&thread2, NULL, printeven, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    sem_destroy(&odd);
    sem_destroy(&even);


    return 0;
}
النتيجة طبعا ستكون النتيجة المنطقية و يمكنكم التأكد من ذلك
سؤال أخر :

سوف نقوم بتزامن الوصول إلى الملف LecRed بين عدة عمليات للقراء والكتّاب، مع مراعاة القواعد التالية:


الكتّاب يجب أن يكون لديهم وصول حصري إلى الملف.
القراء يمكنهم الوصول إلى الملف في نفس الوقت، ولكن فقط إذا لم يكن أي كاتب نشطًا.

C:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>

#define FILE_NAME "TpSemaphore"

sem_t r_c_sem, wr_sem;
int r_c = 0;

void* reader(void* arg) {
    int id = *((int *)arg);
    while (1) {
        sem_wait(&r_c_sem);
        r_c++;
        if (r_c == 1) {
            sem_wait(&wr_sem);
        }
        sem_post(&r_c_sem);

        printf("Reader %d is reading the file.\n", id);
        sleep(rand() % 5);

        sem_wait(&r_c_sem);
        r_c--;
        if (r_c == 0) {
            sem_post(&wr_sem);
        }
        sem_post(&r_c_sem);

        sleep(rand() % 5);
    }
    return NULL;
}

void* writer(void* arg) {
    int id = *((int *)arg);
    while (1) {
        sem_wait(&wr_sem);
        printf("Writer %d is writing to the file.\n", id);
        sleep(rand() % 5);
        sem_post(&wr_sem);
        sleep(rand() % 5);
    }
    return NULL;
}

int main() {
    pthread_t readers[5], writers[2];

    sem_init(&wr_sem, 0, 1);
    sem_init(&r_c_sem, 0, 1);

    for (int i = 0; i < 5; i++) {
        int *id = malloc(sizeof(int));
        *id = i + 1;
        pthread_create(&readers[i], NULL, reader, id);
    }

    for (int i = 0; i < 2; i++) {
        int *id = malloc(sizeof(int));
        *id = i + 1;
        pthread_create(&writers[i], NULL, writer, id);
    }

    for (int i = 0; i < 5; i++) {
        pthread_join(readers[i], NULL);
    }
    for (int i = 0; i < 2; i++) {
        pthread_join(writers[i], NULL);
    }

    sem_destroy(&r_c_sem);
    sem_destroy(&wr_sem);

    return 0;
}


الفقرة الثانية للسؤال هي أضف كود برمجي لجعل عدد محدود من قراء يدخلون للمنطقة الحرجة

لا يوجد حل حاول أنت


و السؤال الأهم مالفرق بين قفل الإغلاق (Mutex) و الإشارة ( semaphore ) :
بإختصار
يتيح قفل الإغلاق المورد لخيط واحد فقط، بينما الإشارة يمكنها السماح لعدد محدد من الخيوط بالوصول إلى المورد في نفس الوقت.

ربما قد نتطرق لشرح ال MUTEX في مقالة قادمة إن شاء الله.

و السلام عليكم و رحمة الله تعالي و بركاته .
 
التعديل الأخير:
بسم الله ما شاء الله لا قوة الا بالله
موضوع رائع وجبّار
بارك الله فيك اخي وجزاك الله كل خير
هذه المواضيع تفتح النفس للقراءة والتجربة والتعلم
ننتظر جديد ابداعاتك دائماً
تحياتي
 
  • Love
التفاعلات: ibada
بارك الله فيك لكن دوال pthread
pthread.h — Thread interfaces
pthread_cond_broadcast() — Broadcast a condition
pthread_cond_signal() — Signal a condition
pthread_cond_timedwait(), pthread_cond_timedwait64() — Wait on a condition variable

تفي بالغرض بدلاً من مكتبة semaphore اذا مهتم بهذي اللغه انصحك ب ibm docs بيفيدك كثيرا بكل تفاصيل اللغه

شكرا لك
 
  • Love
التفاعلات: ibada
بارك الله فيك لكن دوال pthread
pthread.h — Thread interfaces
pthread_cond_broadcast() — Broadcast a condition
pthread_cond_signal() — Signal a condition
pthread_cond_timedwait(), pthread_cond_timedwait64() — Wait on a condition variable

تفي بالغرض بدلاً من مكتبة semaphore اذا مهتم بهذي اللغه انصحك ب ibm docs بيفيدك كثيرا بكل تفاصيل اللغه

شكرا لك
شكرا على إضافتك لكن هل يمكنك إضافة مميزات هذه الدوال التي قد تقنعني و تقنع الفارئ بإستخدام هذه الدوال بدل الإشارات و بارك الله فيك.
 
شكرا على إضافتك لكن هل يمكنك إضافة مميزات هذه الدوال التي قد تقنعني و تقنع الفارئ بإستخدام هذه الدوال بدل الإشارات و بارك الله فيك.

دوال مكتبه pthread كثيره لكن اطلع على docs الخاص بشركة ibm هنا فقط اكتب اسم المكتبه بمحرك البحث او اكتب c language يعرض لك
لديهم مصدر مهم جداً لهذي اللغه وأيضاً للغات اخرى مع شرح المكتبات ودوالها .
 

آخر المشاركات

فانوس

رمضان
عودة
أعلى