תכנות מעשי: מה זה Buffer?

פוסט מבוא על אחד ממנגנוני התוכנה הבסיסיים והשימושיים ביותר בעבודה עם מיקרו-בקרים: הבאפר (Buffer).

מה זה באפר

מערכות שמעבדות מידע מעבירות כל הזמן נתונים ממקום למקום, ולעתים קרובות הצד שמספק את הנתונים והצד שמקבל אותם לא מתואמים ביניהם מבחינת קצב, מהירות או תזמון. כדי למנוע פספוסים ועיכובים, אנחנו נעזרים באיזשהו מאגר – הבאפר, או בעברית "חוצץ" – שצובר זמנית את הנתונים במקום אחסון כלשהו ומגשר על אי-ההתאמות האלה.

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

ולהמחשה מהכיוון ההפוך, נניח שמישהו צריך לקחת תרופה מסוימת פעם ביום למשך חודש. זה מאוד לא נוח (וגם לא תמיד אפשרי) לגשת לבית המרקחת כל בוקר בשביל המנה היומית, אז קונים אריזה של 30 מנות – זה הבאפר, כמובן – וממנה מוציאים את הכמות הדרושה בכל פעם.

הבאפרים של Serial

משתמשי ארדואינו עבדו מאז ומתמיד עם שני באפרים, בדרך כלל בלי להיות מודעים אליהם: הבאפר הנכנס והבאפר היוצא של Serial. הבייטים שנכנסים דרך RX חייבים להיקרא מייד: החשמל בחוטים לא מחכה לאף אחד, והסיכוי ש"נתפוס" אותו במקרה עם קריאה לפונקציה רגילה הוא אפסי. בפועל, כשחומרת המיקרו-בקר מזהה בייט נכנס היא מפעילה פונקציית פסיקה מיידית שמעתיקה את הבייט הזה לבאפר בזיכרון (התהליך הזה מוגדר בקוד בספריות הליבה של ארדואינו), ואילו הפונקציה המוכרת Serial.read שולפת בייטים מהבאפר הנ"ל.

שליחת בייטים ב-UART היא תהליך איטי (כאלפית שנייה לבייט יחיד, בקצב הנפוץ 9600 באוד), ולכן כתיבה ישירה של מחרוזת ארוכה עלולה "לתקוע" את המיקרו-בקר לזמן ממושך יחסית. בפועל, פונקציית הכתיבה Serial.println רק ממלאת באפר בזיכרון, ואילו חומרת ה-UART שולפת מהבאפר הזה בייטים לשליחה בקצב שמתאים לה.

Overflow ו-Underflow

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

ייתכן גם מצב שבו הצד הקורא ינסה "לשאוב" נתונים מבאפר ריק. זה נקרא Underflow (המונח העברי מופרך מדי, מצטער), וגם על זה כדאי לחשוב מראש ולהכין אמצעים להתמודדות. בארדואינו, למשל, אנחנו אמורים להיעזר בפונקציה Serial.available כדי לוודא שהבאפר לא ריק לפני שאנחנו קוראים ממנו נתונים.

מבנה הבאפר

באפרים מכילים בדרך כלל נתונים גולמיים ופשוטים (הרי אם היה לנו זמן לעבד אותם לפני האחסון, לא היינו צריכים באפר…) ולכן האחסון מתבצע כמעט תמיד במערך (array) של מספרים או תווים. אפרופו, לפעמים אנחנו דווקא כן רוצים לבצע עיבוד בסיסי מיידי על הנתונים הנכנסים, למשל כדי לזהות התחלה או סיום של אלמנטים. זוכרים את מערכות המענה הטלפוני שמבקשות "נא הקישו את מספר תעודת הזהות, ולסיום הקישו סולמית"? זה בדיוק העניין: רק כשמתקבלת אינדיקציה שמספר תעודת הזהות מוכן יש טעם לקרוא אותו מהבאפר ולחפש אותו במסד הנתונים של החברה.

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

בפועל, במערכות רבות הצד השולח הוא פחות צפוי, וייתכן שיגיעו נתונים חדשים בזמן שהמערכת עדיין קוראת ומעבדת את הישנים. נניח שיש באפר עם 100 מקומות, הגיעו 95 יחידות נתונים (שמורות באינדקסים 0-94) והמערכת הספיקה לעבד 80 מתוכן. כעת מגיע פרץ חדש עם 10 יחידות. איפה לשמור אותן? בסוף הבאפר כבר אין מספיק מקום, אבל המערכת עוד לא גמרה עם הנתונים הקודמים ולכן לא "שחררה" את המערך לכתיבה מ-0. מה עושים?

הפתרון המקובל הוא באפר טבעת (Ring buffer). מבחינת האחסון בזיכרון זהו עדיין מערך רגיל, אך אנחנו מוסיפים לו שני משתני עזר – אחד שמראה לאיזה אינדקס לכתוב עכשיו ואחד שמראה מאיזה אינדקס לקרוא עכשיו – וגם מגדירים שכאשר אחד המשתנים האלה עובר את האינדקס המקסימלי (בדוגמה מקודם, 99) הוא חוזר ל-0. מבחינת הלוגיקה של התוכנה, זה נותן לנו מעין מעגל או טבעת לאחסון רציף של נתונים, ללא גבולות. לדוגמה, כשהגיעו עשר יחידות הנתונים החדשות, אפשר פשוט לכתוב אותן לאינדקסים 95 עד 99 ובהמשך ישיר 0 עד 4. התלות בין הכתיבה והקריאה נשברת, וניצול הזיכרון יעיל הרבה יותר.

גם באפר טבעת יכול להגיע למצב של Overflow ושל Underflow, וצריך לשים לב לפרטים הקטנים ביישום שלו (כשהערכים של האינדקס לכתיבה והאינדקס לקריאה זהים, האם זה אומר שהבאפר ריק או מלא?) אף על פי כן הוא שימושי, חסכוני במשאבים ונפוץ מאוד.

באפרים בכל מקום

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

להרשמה
הודע לי על
2 תגובות
מהכי חדשה
מהכי ישנה לפי הצבעות
Inline Feedbacks
הראה את כל התגובות

תודה למדתי כמה דברים חדשים. איפה הבאפר יושב בזכרון של הבקר?