דף הבית > תכנות > From lock to abstract locker

From lock to abstract locker

כמו שמשתמע מהכותרת של הפוסט הזה, לא חייבים לנעול דווקא עם lock (כלומר נעילה שמבוססת על Monitor).
יש נעילות נוספות (למשל, Spin-Lock), ולכן אני מציע כאן אבסטרקציה לנעילה.

במילים אחרות, במקום הקוד הזה:

lock(locker)
{
    // do something
}

אני מציע משהו כזה:

locker.Enter();
// do something
locker.Exit();

אלא שזה קצת מעצבן, ולכן אפשר לשכלל את זה אם נשתמש ב IDisposable וניצור משהו יותר נחמד עם ה using שהוא סוג של מאקרו בדוט נט:

using(locker.EnterLock())
{
    // do something
}

אז בפוסט הזה יש:

  • קצת אבסטרקציה
  • קצת מוטיבציה
  • קוד בסיס
  • 2 ירושות
  • וקצת אינטגרציה 🙂

קצת מוטיבציה

מתי זה שימושי?
-בעיקר אם נרצה לכתוב class שיכול להיות שימושי גם בסביבה שהיא single-threaded וגם בסביבה שהיא multi-threaded.

קצת קוד בסיס

ניגש לקוד עצמו. הנה ה base class עם תוספת פנימית בשביל ה using הנכסף:

using System;

public abstract class LockerBase
{
    public abstract void Enter();
    public abstract void Exit();

    public class DisposableLocker : IDisposable
    {
        private readonly LockerBase locker;

        public DisposableLocker(LockerBase locker)
        {
            this.locker = locker;
        }

        public void Dispose()
        {
            locker.Exit();
        }
    }

    public DisposableLocker EnterLock()
    {
        Enter();
        return new DisposableLocker(this);
    }
}

המתודות Enter ו Exit הן אבסטרקטיות, וכל מי שירצה לרשת מה base class הזה יצטרך לממש אותן.
אגב, אני לא מת על ירושה (לא בתכנות, לפחות), אבל כאן זה נראה יותר מתאים, כי יש איזה state לשמור עליו.

ירושה – המימוש ה"רגיל"

בואו נראה למשל, את המימוש המתבקש לנעילה "שגרתית":

using System.Threading;

public class MonitorLocker : LockerBase
{
    private readonly object syncRoot;

    public MonitorLocker(object syncObject)
    {
        syncRoot = syncObject;
    }

    public MonitorLocker() : this(new object())
    {
    }

    public override void Enter()
    {
        Monitor.Enter(syncRoot);
    }

    public override void Exit()
    {
        Monitor.Exit(syncRoot);
    }        
}

בקונסטרקטור נשים אובייקט שתפקידו להיות המשאב המשותף שעליו מבוססים השיתוף והנעילה.

ירושה – המימוש הריק

ועכשיו לקוד קצת אחר: class שלמעשה לא נועל. מעין dummy class שרק שומר על הפונקציונליות כלפי חוץ:

public class DummyLocker : LockerBase
{
    public override void Enter()
    {
    }

    public override void Exit()
    {
    }
}

בעקרון ה class הזה לא ישבור קומפילציה, אבל הוא לא באמת נועל כלום. זה ה dummy שלנו למקרה שמריצים את הקוד בסביבה שהיא single-threaded.

קצת אינטגרציה

אחרי כל הקוד הזה, נראה שימוש לאבסטרקציה הזו. נניח שיש לנו class בשם Counter. כל מה שיש שם זה מתודה בשם Hit ופרופרטי בשם Current.
וכל מה שצריך לקרות זה שכל קריאה ל Hit מעלה ב 1 את הערך של Current (שערכו ההתחלתי הוא 0).
אנחנו יודעים מראש שיהיו מצבים שבהם ה class הזה יהיה שימושי בסביבה שהיא single-threaded וכן בסביבה שהיא multi-threaded. לכן נשתמש בנעילה האבסטרקטית:

public class Counter
{
    private readonly LockerBase locker;
    private int counter = 0;

    public Counter(LockerBase locker)
    {
        this.locker = locker;
    }

    public void Hit()
    {
        using (locker.EnterLock())
        {
            ++counter;
        }

    }

    public int Current
    {
        get
        {
            using (locker.EnterLock())
            {
                return counter;
            }
        }
    }
}

הקוד די פשוט: כל התייחסות ל counter הפנימי "עטופה" בנעילה, אבל אנחנו לא יודעים מה יהיה המנעול שאיתו נעבוד.
המנעול עצמו יכנס ל class דרך הקונסטרקטור (ולזה קוראים Dependency Injection).
כך, למשל, ניצור instance של Counter לסביבה שהיא single-threaded (כלומר ללא צורך בנעילות):

Counter myCounter = new Counter(new DummyLocker());

אבל, כדי לא לעצבן את המתכנת שישתמש ב class הזה, אולי כדאי שנוסיף overload עם ה dummy-locker שלנו כברירת מחדל לקונסטרקטור ריק:

public Counter() : this(new DummyLocker())
{
}

ואז השימוש יהיה קל יותר:

Counter myCounter = new Counter();

נוכל גם להוסיף מתודה סטאטית שתפקידה ליצור instance שהוא thread-safe באופן הבא:

public static Counter Synchronized()
{
    return new Counter(new MonitorLocker());
}

והקריאה:

Counter myCounter = Counter.Synchronized();

סיכום

מה שהצעתי כאן זה אבסטרקציה למנגנון הנעילה, תוך שימוש במאקרו של using כדי לכתוב קוד שמתנהג קרוב ככל האפשר ל lock המוכר.
האבסטרקציה הזו שימושית אם יודעים מראש שיש איזה class שיהיה שימושי גם בסביבה שהיא multi-threaded וגם בסביבה שהיא single-threaded.
עם זאת, יש דרכים נוספות להשיג את התוצאה הזו.
למשל, לכתוב interface ל Counter וליצור decorator שהוא יעטוף כל פעולה בנעילה, או dynamic proxy שיעשה את זה באופן אוטומטי. אולי אני ארחיב על זה בהזדמנות.
בכל מקרה, מה שאנחנו משיגים כאן זו הפרדה של הלוגיקה של הקוד עצמו מהלוגיקה של הנעילה, וזה נחמד לכשעצמו, וגם נותן לנו יתרון משמעותי אם נרצה לכתוב unit testing לקוד הזה.

ניתן להוריד את הקוד של הפוסט הזה מהלינק הזה.

  1. 6 ינואר, 2010 מתוך 21:32 | #1

    למרות שהרעיון יפה, אני לא חושב שיש לאבסטרקטציה הזאת הרבה טעם משום שהעלות של תפיסת נעילה (ללא contentions) הוא מינימלי, כך שהוספת עוד שכבת אבסטרקציה לקוד נראית מעט מיותרת (גורמת לקוד להראות מסובך יותר ללא צורך).
    ובנוגע ל-SpinLock, מעטים הם המקרים שבאמת מצדיקים שימוש בו. בדרך כלל בשימושי "High Level" תקבל ביצועים טובים יותר בשימוש עם אובייקטי קרנל (שעליהם מתבסס Monitor).

  2. 6 ינואר, 2010 מתוך 23:18 | #2

    @לירן, אני מסכים איתך שהאבסטרקציה כאן מורידה במשהו מהקריאות של הקוד.
    למעשה אני חושב כבר על הפוסט שיסביר את הרעיון של נעילה באמצעות decorator, שזה נראה לי כרגע פתרון קצת יותר אלגנטי.
    צ'ירס!

  1. אין הפניות עדיין.

Quantcast