MCP კლუბის 23.04.2010 - ის დეველოპერულ შეხვედრასთან დაკავშირებით მინდოდა განმეხილა Multithreading - ის საკითხები ცოცხალი მაგალითებით. როგორც გვახსოვს Multithreading - ი მოიცავს 3 ძირითად საპრობლემო საკითხს:
  1. DeadLock - გამოუვალი მდგომარეობა
  2. Race Condition - შეჯიბრის პირობა
  3. Starvation - შიმშილობა
განვიხილოთ თითოული მათგანი უფრო ღრმად დეტალებში



DeadLock - გამოუვალი მდგომარეობა

მაგალითად გვაქვს ორი სტატიკური ცვლადი a და b, რომლებზეც აპირებს მუშაობას ორი Thread - ი ThreadProc1() და ThreadProc2()
ორივე ThreadProc ცვლადებზე მუშაობის დაწყების წინ შეეცდება ჯერ დაირეზერვოს (ანუ დაბლოკოს) ცვლადი, Monitor.TryEnter - ით, სხვა Thread - თვის და შემდეგ დაიწყოს მასზე მუშაობა, ხოლო თუ ცვლადი უკვე დარეზერვებული აღმოჩნდა სხვა პროცესის მიერ, მუშაობა შეწყდება და TryEnter დააბრუნებს false - ს შემდეგი ორი მაგალითის მიხედვით გამოჩნდება თუ როგორ შეიძლება დაირეზერვოს ორმა Thread - მა თითოული ცვლადი ისე რომ ერთმანეთს ხელი შეუშალონ და ვერასდროს შეასრულონ თავიანთი საქმე, ანუ შექმნან DeadLock - გამოუვალი მდგომარეობა და როგორ უნდა დაიწეროს კოდი ისე, რომ ავიცილოთ მსგავსი სიტუაცია ან როგორ უნდა დაიწეროს კოდი სწორად.

შეცდომაა Laughing


public static class Program
{
    public static Random random = new Random();
    public static object a = new object();
    public static object b = new object();

    public static void ThreadProc1()
    {
      for (; ; )
      {
        if (Monitor.TryEnter(a, 1000) && Monitor.TryEnter(b, 1000))
        {
          Thread.Sleep(random.Next(500));

          Monitor.Exit(b);
          Monitor.Exit(a);
        }
        else
        {
          Console.WriteLine("BRUTAL NINJA KILLS THREAD 1");
        }
      }
    }

    public static void ThreadProc2()
    {
      for (; ; )
      {
        if (Monitor.TryEnter(b, 1000) && Monitor.TryEnter(a, 1000))
        {
          Thread.Sleep(random.Next(500));

          Monitor.Exit(a);
          Monitor.Exit(b);
        }
        else
        {
          Console.WriteLine("BRUTAL NINJA KILLS THREAD 2");
        }
      }
    }

    public static void Main(string[] args)
    {
      (new Thread(ThreadProc1)).Start();
      (new Thread(ThreadProc2)).Start();

      Console.WriteLine("Press <Enter> to exit.");
      Console.ReadLine();
    }
}

რადგან ThreadProc1() - ის Monitor.TryEnter(a, 1000) დაბლოკავს a - ს, ამ დროს ThreadProc2() - ის Monitor.TryEnter(b, 1000) დაბლოკავს b - ს, რის შემდეგაც შემოწმდებას ThreadProc1() - ის if - ის მეორე პირობა TryEnter(b, 1000) რაც დააბრუნებს false - ს და ThreadProc2() - ის if - ის მეორე პირობა TryEnter(a, 1000) რაც აგრეთვე დააბრუნებს false - ს. შედეგი ორივე პროცესის if პირობა ყოველთვის გადავა else - ში.
შედეგი DeadLock - ი:
DeadLock

გამოსავალი?

სწორია Laughing


public static class Program
{
    public static Random random = new Random();
    public static object a = new object();
    public static object b = new object();

    public static void ThreadProc1()
    {
      for (; ; )
      {
        if (Monitor.TryEnter(a, 1000) && Monitor.TryEnter(b, 1000))
        {
          Thread.Sleep(random.Next(500));

          Monitor.Exit(b);
          Monitor.Exit(a);
        }
        else
        {
          Console.WriteLine("BRUTAL NINJA KILLS THREAD 1");
        }
      }
    }

    public static void ThreadProc2()
    {
      for (; ; )
      {
        
        if (Monitor.TryEnter(a, 1000) && Monitor.TryEnter(b, 1000))
        {
          Thread.Sleep(random.Next(500));

          Monitor.Exit(a);
          Monitor.Exit(b);
        }
        else
        {
          Console.WriteLine("BRUTAL NINJA KILLS THREAD 2");
        }
      }
    }
   
    public static void Main(string[] args)
    {
      (new Thread(ThreadProc1)).Start();
      (new Thread(ThreadProc2)).Start();

      Console.WriteLine("Press <Enter> to exit.");
      Console.ReadLine();
    }
}

Race Condition - შეჯიბრის პირობა

public static class Program
{
    private static long result;

    public static void ThreadProc1()
    {
      for (long i = 0; i < 10000000; i++)
      {
        // NON-ATOMIC OPERATION
        result += i;
      }
    }

    public static void ThreadProc2()
    {
      for (long i = 0; i < 10000000; i++)
      {
        // NON-ATOMIC OPERATION
        result += i;
      }
    }

    public static void Main(string[] args)
    {
      for (int i = 0; i < 10; i++)
      {
        Thread thread1 = new Thread(ThreadProc1);
        Thread thread2 = new Thread(ThreadProc2);

        result = 0;

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine("Try: {0}; Result: {1}", i, result);
      }
    }
}
თითოული 10 ცდის შედეგი იქნება განსხვავებული რადგან result ცვლადში მნიშვნელობათა ჩაწერა პრაქტიკულად "მოსწრებაზეა".

Starvation - შიმშილობა

ლოკალური ცვლადების გამოყენება vs გლობალური ცვლადების გამოყენება. მიმდევრობითი პროცესის ჯამი vs Thread - ებით დაჯამება.
public static class Program
{
    //
    // CLOSE IM MEMORY variables
    //
    private const long max = 200000000;

    private static long result1;
    private static long result2;

    public static void SlowThreadProc1()
    {
      for (long i = 0; i < max; i++)
      {
        result1 += i;
      }
    }

    public static void SlowThreadProc2()
    {
      for (long i = 0; i < max; i++)
      {
        result2 += i;
      }
    }

    public static void FastThreadProc1()
    {
      long temp = 0;

      for (long i = 0; i < max; i++)
      {
        temp += i;
      }

      result1 = temp;
    }

    public static void FastThreadProc2()
    {
      long temp = 0;

      for (long i = 0; i < max; i++)
      {
        temp += i;
      }

      result2 = temp;
    }

    public static void Main(string[] args)
    {
      Stopwatch watch = new Stopwatch();

      watch.Start();

      SlowThreadProc1();
      SlowThreadProc2();

      watch.Stop();

      Console.WriteLine(
        "Base Time: {0}; Result: {1}",
        watch.ElapsedMilliseconds.ToString().PadLeft(5),
        result1 + result2);

      Thread slowThread1 = new Thread(SlowThreadProc1);
      Thread slowThread2 = new Thread(SlowThreadProc2);

      result1 = 0;
      result2 = 0;

      watch.Reset();
      watch.Start();

      slowThread1.Start();
      slowThread2.Start();

      slowThread1.Join();
      slowThread2.Join();

      watch.Stop();

      Console.WriteLine(
        "Slow Time: {0}; Result: {1}",
        watch.ElapsedMilliseconds.ToString().PadLeft(5),
        result1 + result2);

      Thread fastThread1 = new Thread(FastThreadProc1);
      Thread fastThread2 = new Thread(FastThreadProc2);

      result1 = 0;
      result2 = 0;

      watch.Reset();
      watch.Start();

      fastThread1.Start();
      fastThread2.Start();

      fastThread1.Join();
      fastThread2.Join();

      watch.Stop();

      Console.WriteLine(
        "Fast Time: {0}; Result: {1}",
        watch.ElapsedMilliseconds.ToString().PadLeft(5),
        result1 + result2);
    }
}


თითოული ზემოთ განხილული კოდისთვის თეორიული ცოდნის მისაღებად იხილეთ პრეზენტაცია და ვიდეომასალა
mcp.community.ge
Video on Vimeo

source კოდები მოგვაწოდა ბატონ რომან აკოფოვმა