• C#多线程之基础篇3


      在上一篇C#多线程之基础篇2中,我们主要讲述了确定线程的状态、线程优先级、前台线程和后台线程以及向线程传递参数的知识,在这一篇中我们将讲述如何使用C#的lock关键字锁定线程、使用Monitor锁定线程以及线程中的异常处理。

    九、使用C#的lock关键字锁定线程

    1、使用Visual Studio 2015创建一个新的控制台应用程序。

    2、双击打开“Program.cs”文件,然后修改为如下代码:

     1 using System;
     2 using System.Threading;
     3 using static System.Console;
     4 
     5 namespace Recipe09
     6 {
     7     abstract class CounterBase
     8     {
     9         public abstract void Increment();
    10         public abstract void Decrement();
    11     }
    12 
    13     class Counter : CounterBase
    14     {
    15         public int Count { get; private set; }
    16 
    17         public override void Increment()
    18         {
    19             Count++;
    20         }
    21 
    22         public override void Decrement()
    23         {
    24             Count--;
    25         }
    26     }
    27 
    28     class CounterWithLock : CounterBase
    29     {
    30         private readonly object syncRoot = new Object();
    31 
    32         public int Count { get; private set; }
    33 
    34         public override void Increment()
    35         {
    36             lock (syncRoot)
    37             {
    38                 Count++;
    39             }
    40         }
    41 
    42         public override void Decrement()
    43         {
    44             lock (syncRoot)
    45             {
    46                 Count--;
    47             }
    48         }
    49     }
    50 
    51     class Program
    52     {
    53         static void TestCounter(CounterBase c)
    54         {
    55             for (int i = 0; i < 100000; i++)
    56             {
    57                 c.Increment();
    58                 c.Decrement();
    59             }
    60         }
    61 
    62         static void Main(string[] args)
    63         {
    64             WriteLine("Incorrect counter");
    65             var c1 = new Counter();
    66             var t1 = new Thread(() => TestCounter(c1));
    67             var t2 = new Thread(() => TestCounter(c1));
    68             var t3 = new Thread(() => TestCounter(c1));
    69             t1.Start();
    70             t2.Start();
    71             t3.Start();
    72             t1.Join();
    73             t2.Join();
    74             t3.Join();
    75             WriteLine($"Total count: {c1.Count}");
    76 
    77             WriteLine("--------------------------");
    78 
    79             WriteLine("Correct counter");
    80             var c2 = new CounterWithLock();
    81             t1 = new Thread(() => TestCounter(c2));
    82             t2 = new Thread(() => TestCounter(c2));
    83             t3 = new Thread(() => TestCounter(c2));
    84             t1.Start();
    85             t2.Start();
    86             t3.Start();
    87             t1.Join();
    88             t2.Join();
    89             t3.Join();
    90             WriteLine($"Total count: {c2.Count}");
    91         }
    92     }
    93 }

    3、运行该控制台应用程序,运行效果(每次运行效果可能不同)如下图所示:

      在第65行代码处,我们创建了Counter类的一个对象,该类定义了一个简单的counter变量,该变量可以自增1和自减1。然后在第66~68行代码处,我们创建了三个线程,并利用lambda表达式将Counter对象传递给了“TestCounter”方法,这三个线程共享同一个counter变量,并且对这个变量进行自增和自减操作,这将导致结果的不正确。如果我们多次运行这个控制台程序,它将打印出不同的counter值,有可能是0,但大多数情况下不是。

      发生这种情况是因为Counter类是非线程安全的。我们假设第一个线程在第57行代码处执行完毕后,还没有执行第58行代码时,第二个线程也执行了第57行代码,这个时候counter的变量值自增了2次,然后,这两个线程同时执行了第58行处的代码,这会造成counter的变量只自减了1次,因此,造成了不正确的结果。

      为了确保不发生上述不正确的情况,我们必须保证在某一个线程访问counter变量时,另外所有的线程必须等待其执行完毕才能继续访问,我们可以使用lock关键字来完成这个功能。如果我们在某个线程中锁定一个对象,其他所有线程必须等到该线程解锁之后才能访问到这个对象,因此,可以避免上述情况的发生。但是要注意的是,使用这种方式会严重影响程序的性能。更好的方式我们将会在仙童同步中讲述。

    十、使用Monitor锁定线程

       在这一小节中,我们将描述一个多线程编程中的常见的一个问题:死锁。我们首先创建一个死锁的示例,然后使用Monitor避免死锁的发生。

    1、使用Visual Studio 2015创建一个新的控制台应用程序。

    2、双击打开“Program.cs”文件,编写代码如下:

     1 using System;
     2 using System.Threading;
     3 using static System.Console;
     4 using static System.Threading.Thread;
     5 
     6 namespace Recipe10
     7 {
     8     class Program
     9     {
    10         static void LockTooMuch(object lock1, object lock2)
    11         {
    12             lock (lock1)
    13             {
    14                 Sleep(1000);
    15                 lock (lock2)
    16                 {
    17                 }
    18             }
    19         }
    20 
    21         static void Main(string[] args)
    22         {
    23             object lock1 = new object();
    24             object lock2 = new object();
    25 
    26             new Thread(() => LockTooMuch(lock1, lock2)).Start();
    27 
    28             lock (lock2)
    29             {
    30                 WriteLine("This will be a deadlock!");
    31                 Sleep(1000);
    32                 lock (lock1)
    33                 {
    34                     WriteLine("Acquired a protected resource succesfully");
    35                 }
    36             }
    37         }
    38     }
    39 }

    3、运行该控制台应用程序,运行效果如下图所示:

      在上述结果中我们可以看到程序发生了死锁,程序一直结束不了。

      在第10~19行代码处,我们定义了一个名为“LockTooMuch”的方法,在该方法中我们锁定了第一个对象lock1,等待1秒钟后,希望锁定第二个对象lock2。

      在第26行代码处,我们创建了一个新的线程来执行“LockTooMuch”方法,然后立即执行第28行代码。

      在第28~32行代码处,我们在主线程中锁定了对象lock2,然后等待1秒钟后,希望锁定第一个对象lock1。

      在创建的新线程中我们锁定了对象lock1,等待1秒钟,希望锁定对象lock2,而这个时候对象lock2已经被主线程锁定,所以新建线程会等待对象lock2被主线程解锁。然而,在主线程中,我们锁定了对象lock2,等待1秒钟,希望锁定对象lock1,而这个时候对象lock1已经被创建的线程锁定,所以主线程会等待对象lock1被创建的线程解锁。当发生这种情况的时候,死锁就发生了,所以我们的控制台应用程序目前无法正常结束。

    4、要避免死锁的发生,我们可以使用“Monitor.TryEnter”方法来替换lock关键字,“Monitor.TryEnter”方法在请求不到资源时不会阻塞等待,可以设置超时时间,获取不到直接返回false。修改代码如下所示:

     1 using System;
     2 using System.Threading;
     3 using static System.Console;
     4 using static System.Threading.Thread;
     5 
     6 namespace Recipe10
     7 {
     8     class Program
     9     {
    10         static void LockTooMuch(object lock1, object lock2)
    11         {
    12             lock (lock1)
    13             {
    14                 Sleep(1000);
    15                 lock (lock2)
    16                 {
    17                 }
    18             }
    19         }
    20 
    21         static void Main(string[] args)
    22         {
    23             object lock1 = new object();
    24             object lock2 = new object();
    25 
    26             new Thread(() => LockTooMuch(lock1, lock2)).Start();
    27 
    28             lock (lock2)
    29             {
    30                 WriteLine("This will be a deadlock!");
    31                 Sleep(1000);
    32                 //lock (lock1)
    33                 //{
    34                 //    WriteLine("Acquired a protected resource succesfully");
    35                 //}
    36                 if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5)))
    37                 {
    38                     WriteLine("Acquired a protected resource succesfully");
    39                 }
    40                 else
    41                 {
    42                     WriteLine("Timeout acquiring a resource!");
    43                 }
    44             }
    45         }
    46     }
    47 }

    5、运行该控制台应用程序,运行效果如下图所示:

      此时,我们的控制台应用程序就避免了死锁的发生。

    十一、处理异常

       在这一小节中,我们讲述如何在线程中正确地处理异常。正确地将try/catch块放置在线程内部是非常重要的,因为在线程外部捕获线程内部的异常通常是不可能的。

    1、使用Visual Studio 2015创建一个新的控制台应用程序。

    2、双击打开“Program.cs”文件,修改代码如下所示:

     1 using System;
     2 using System.Threading;
     3 using static System.Console;
     4 using static System.Threading.Thread;
     5 
     6 namespace Recipe11
     7 {
     8     class Program
     9     {
    10         static void BadFaultyThread()
    11         {
    12             WriteLine("Starting a faulty thread...");
    13             Sleep(TimeSpan.FromSeconds(2));
    14             throw new Exception("Boom!");
    15         }
    16 
    17         static void FaultyThread()
    18         {
    19             try
    20             {
    21                 WriteLine("Starting a faulty thread...");
    22                 Sleep(TimeSpan.FromSeconds(1));
    23                 throw new Exception("Boom!");
    24             }
    25             catch(Exception ex)
    26             {
    27                 WriteLine($"Exception handled: {ex.Message}");
    28             }
    29         }
    30 
    31         static void Main(string[] args)
    32         {
    33             var t = new Thread(FaultyThread);
    34             t.Start();
    35             t.Join();
    36 
    37             try
    38             {
    39                 t = new Thread(BadFaultyThread);
    40                 t.Start();
    41             }
    42             catch (Exception ex)
    43             {
    44                 WriteLine(ex.Message);
    45                 WriteLine("We won't get here!");
    46             }
    47         }
    48     }
    49 }

    3、运行该控制台应用程序,运行效果如下图所示:

      在第10~15行代码处,我们定义了一个名为“BadFaultyThread”的方法,在该方法中抛出一个异常,并且没有使用try/catch块捕获该异常。

      在第17~29行代码处,我们定义了一个名为“FaultyThread”的方法,在该方法中也抛出一个异常,但是我们使用了try/catch块捕获了该异常。

      在第33~35行代码处,我们创建了一个线程,在该线程中执行了“FaultyThread”方法,我们可以看到在这个新创建的线程中,我们正确地捕获了在“FaultyThread”方法中抛出的异常。

      在第37~46行代码处,我们又新创建了一个线程,在该线程中执行了“BadFaultyThread”方法,并且在主线程中使用try/catch块来捕获在新创建的线程中抛出的异常,不幸的的是我们在主线程中无法捕获在新线程中抛出的异常。

      由此可以看到,在一个线程中捕获另一个线程中的异常通常是不可行的。

      至此,多线程(基础篇)我们就讲述到这儿,之后我们将讲述线程同步相关的知识,敬请期待!

      源码下载

  • 相关阅读:
    POJ 1182 食物链 +POJ 2492 A Bug's Life 种类并查集
    Codeforces 884C Bertown Subway dfs判环
    HDU 2102 A计划 bfs
    Codeforces Round #442 (Div. 2) 877E
    Codeforces Round #443 (Div. 2) B. Table Tennis
    Codeforces Round #442 (Div. 2) D. Olya and Energy Drinks
    HDU 1495 非常可乐 BFS/数论
    Codeforces Round #442 (Div. 2) B. Nikita and string DP
    51Nod 1102 面积最大的矩形 +1272 最大距离 单调栈
    HDU 1286 找新朋友
  • 原文地址:https://www.cnblogs.com/yonghuacui/p/6187701.html
Copyright © 2020-2023  润新知