Threads - continued

התחלנו קצת לדבר על Threading נמשיך לגעת בעוד כמה נקודות הקשורות...קודם כל עלינו לשים לב שאם כמה Threadים מנסים לגשת לאותו מידע, נוצר מצב מסוכן

static List<int> myNums;
        public static void AddNumber(int num)
        {
            if (!myNums.Contains(num))-->check
            {
                myNums.Add(num);-->add
            }
        }

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

  1. להפוך את כל הפונקציה ל Synchronized - משמע שסביבת הריצה לא תיתן ליותר מ Thread אחד לגשת לפונקציה במקביל
  2. לנעול את האובייקט הבעייתי, שבמקרה שלנו זה myNums
  3. להשתמש באמצעים קיצוניים יותר :)

בואו נראה איך ליישם 2 שיטות בדוגמה שלנו - כדי לישם את השיטה הראשונה פשוט נוסיף הוראה לפני הגדרת הפונקציה:

[MethodImpl(MethodImplOptions.Synchronized)]

כדי ליישם את השיטה השניה נוסיף שורה אחת בתוך הפונקציה:

static List<int> myNums;
        public static void AddNumber(int num)
        {
            lock(myNums)
            {
                if (!myNums.Contains(num))-->check
                {
                    myNums.Add(num);-->add
                }
            }
        }

וכל עוד אנחנו בתוך הקטע שנועל את האובייקט, אף Thread לא יוכל לגשת אליו

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

עדיפות:
ניתן לקבוע לכל Thread שאנו מתחילים את העדיפות שלו - איזה חלק הוא יקבל מהמעבד באופן יחסי

t.Priority = ThreadPriority.Normal;

לסיכום בואו נראה דוגמה של Crawler MultiThreaded , התוכנית שלנו רוצה להריץ עד 10 Threading במקביל, הם יגלשו באינטרנט, יאספו לינקים, כל אחד מדף אחד, ויפסיקו, ואז יתחיל Thread חדש, יקבל לינק אחד ממה שאספנו וימשיך לקרוא לינקים ממנו
סוגיות:

  • אני חייב להשתמש ב MultiThreading כי חבל להתעכב עד שכל שרת יענה לי, אם הוא יענה בכלל, גם חבל על זמן שבו אני מוצא את הלינקים בדף
  • אני רוצה שמספר ה Threading שרצים במקביל לא יהיה גדול מ-10 בגלל רוחב הפס לאינטרנט וכדי לא להעמיס על המעבד
  • אני רוצה להימנע ממצב ששני Threading יכניסו את אותו הלינק למאגר הלינקים ואז אני אבקר באותו לינק פעמיים סתם

יהיו לנו 2 מחלקות - הראשית שמריצה את ה Threadים, ומחלקה שתייצג את ה Thread והיא זאת שתפתח את דף האינטרנט, תקרא את הקישורים ותוסיף אותם לאוסף שימצא במחלקה הראשית שלנו

שם המחלקה הראשית Crawler
המחלקה תכיל משתנה סטטי מסוג int שיבדוק כמה טרדים כרגע פעילים

private static int threadsCount;

המחלקה גם תכיל רשימה סטטית של כל הלינקים החדשים

private static Queue<string> links;

המחלקה מכילה פונקציה שמאפשרת ל Thread שסיים להודיע שהוא סיים ובכך מוריד את מספר הThreadים הפעילים

[MethodImpl( MethodImplOptions.Synchronized)]
internal static void RemoveThread()
{
    threadsCount--;
}

ופונקציה שאליה יעבירו כל הThreadים את כל הלינקים שמצאו בדף כדי לעדכן את רשימת הלינקים החדשים שיש לנו

internal static void AddLinks(Queue<string> newlinks)
{
    lock (links)   
    {
        for (int i = 0; i < newlinks.Count; i++)
        {
            links.Enqueue(newlinks.Dequeue);
        }
    }
}

והלולאה העיקרית שמריצה את כל ה Threadים

private void Start()
    {
        links = new Queue<string>();
        links.Enqueue("http://www.google.com/search?q=csharp");
        run = true;
        while (run)
        {
            while (threadsCount >= 10)
            {
                Thread.Sleep(1000);
            }
            while (links.Count == 0)
            {
                Thread.Sleep(1000);
            }
            WorkerThread wt = new WorkerThread(links.Dequeue);
            Thread t = new Thread(new ThreadStart(wt.Run));
            threadsCount++;
            t.Start();
            
        }
    }

שימו לב שהלולאה בודקת את המשתנה run שהינו בוליאני, כך שאם נרצה לעצור צריך פשוט לשנות את הערך שלו.
בתוך הלולאה אנו קודם בודקים אם לא הגענו עדיין למספר המקסימלי של Threadים, אם כן אז עוצרים לשניה וכך עד שאיזה Thread יתפנה ונוכל להמשיך הלאה.
בשלב הבא אנו בודקים שיש לנו בכלל לינקים לתת לThreadים להריץ, אם לא אז ממתינים, אולי איזה Thread יסיים בינתיים וימלא לנו את מאגר הלינקים שוב.
אם הכל בסדר אפשר ליצור אובייקט חדש מהמחלקה WorkerThread, נותנים לו את הכתובת לבדוק, מגדילים את מספר הThreadים הפעילים ומריצים אותו

ונשארה לנו רק המחלקה WorkerThread שהמבנה שלה לא כל משנה בעצם

public class WorkerThread
{
    private string _url;
    public WorkerThread(string url)
    {
        this._url = url;
    }
    public void Run()
    {
        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(this._url);
        HttpWebResponse resp = req.GetRequestStream();
        if (resp.StatusCode == HttpStatusCode.OK)
        {
            string data = new StringReader(resp.GetResponseStream()).ReadToEnd();
            resp.Close();
            Queue<string> links = new Queue<string>();
            //do the formatting of the page to find links...
            Crawler.AddLinks(links);
        }
        Crawler.RemoveThread();
    }
}

המחלקה שומרת את הכתובת שהיא קיבלה מהמחלקה הראשית שלנו, ואז בפונקציה Run שגם מופעלת על ידי המחלקה הראשית היא מבצעת פניה לדף, מפרמטת אותו כדי לחפש את הלינקים שם, קוראת לפונקציה ססטית במחלקה הראשית כדי להוסיף את הלינקים שנמצאו, ובסוף גם מורידה את מספר הThreadים הפעילים במחלקה הראשית

טוען נתונים...
אהבתם?
המליצו לאחרים!

נהניתם? בעיות? הערות?
אני רוצה לשמוע!
rss feed