לדלג לתוכן

10.4 מודל הזכרון תרגול

תרגול - מודל הזכרון - memory model and volatile

תרגול 1 - volatile ואופטימיזציות

  1. כתבו את הקוד הבא ללא volatile:
    int flag = 0;
    
    void *waiter(void *arg) {
        while (flag == 0) { }
        printf("flag changed!\n");
        return NULL;
    }
    
  2. ב-thread אחר, הוסיפו sleep(1) ואז flag = 1.
  3. קמפלו עם -O2 והריצו. מה קורה? (רמז: ייתכן שהתוכנית תתקע)
  4. הוסיפו volatile למשתנה flag וקמפלו שוב. מה קורה עכשיו?
  5. הסבירו את ההבדל: מה הקומפיילר עשה עם האופטימיזציה?

תרגול 2 - מונה אטומי

  1. כתבו תוכנית עם מונה גלובלי int counter = 0.
  2. צרו 4 threads, כל אחד מגדיל את המונה 1,000,000 פעמים (counter++).
  3. הדפיסו את הערך הסופי. האם הוא 4,000,000? הריצו כמה פעמים ובדקו.
  4. החליפו את int counter ב-atomic_int counter ושנו את counter++ ל-atomic_fetch_add(&counter, 1, memory_order_relaxed).
  5. הריצו שוב. האם עכשיו התוצאה תמיד 4,000,000?
  6. הסבירו למה memory_order_relaxed מספיק כאן (רמז: אנחנו לא מסתמכים על סדר מול משתנים אחרים).

תרגול 3 - producer/consumer עם acquire/release

  1. ממשו דפוס producer/consumer:
  2. משתנה גלובלי int data.
  3. משתנה אטומי atomic_int ready = 0.
  4. thread היצרן: מכין את data (שם ערך), ואז עושה atomic_store עם memory_order_release ל-ready.
  5. thread הצרכן: ממתין בלולאה עד ש-ready הוא 1 (עם atomic_load ו-memory_order_acquire), ואז קורא את data.
  6. ודאו שהצרכן תמיד קורא את הערך הנכון.
  7. נסו להחליף את acquire/release ב-memory_order_relaxed. האם עדיין מובטח שזה יעבוד? הסבירו (תיאורטית, גם אם בx86 זה "עובד").
  8. שנו את הדוגמה כך שהיצרן שולח 10 ערכים, אחד אחרי השני. הצרכן קורא את כולם ומדפיס.

תרגול 4 - ניסוי מחסום קומפיילר

  1. כתבו את הקוד הבא:
    int x = 0, y = 0;
    
    void set_values(void) {
        x = 1;
        y = 2;
    }
    
  2. קמפלו עם gcc -O2 -S כדי לראות את האסמבלי שנוצר. בדקו את סדר הכתיבות.
  3. הוסיפו מחסום קומפיילר בין שתי הכתיבות:
    void set_values(void) {
        x = 1;
        asm volatile("" ::: "memory");
        y = 2;
    }
    
  4. קמפלו שוב עם -O2 -S ובדקו את האסמבלי. האם הסדר נשמר?
  5. הסבירו מה מחסום הקומפיילר עושה ולמה הוא לא מספיק לסינכרון בין threads (בארכיטקטורות חלשות כמו ARM).

תרגול 5 - תור lock-free

  1. ממשו תור SPSC (single-producer single-consumer) פשוט עם מערך מעגלי:
  2. הגדירו struct עם מערך int buffer[64], ו-atomic_int head, atomic_int tail.
  3. ממשו int queue_push(queue_t *q, int val) - מחזיר 0 בהצלחה, -1 אם מלא.
  4. ממשו int queue_pop(queue_t *q, int *val) - מחזיר 0 בהצלחה, -1 אם ריק.
  5. השתמשו ב-acquire/release כדי להבטיח סדר.
  6. צרו thread יצרן שדוחף מספרים 0-999 לתור, ו-thread צרכן שקורא אותם ומדפיס.
  7. ודאו שכל המספרים מגיעים בסדר הנכון וללא אבדן.
  8. מה קורה אם התור מתמלא? איך הthread היצרן צריך להתנהג?