这里需要注意的是在使用 Thread.Start(para) 方法传递参数时,被调用方法的参数类型必须是 object 类型。如果你觉得上述两种方法都不好,那么你还可以使用 ParameterizedThreadStart 委托,只需把前面行一段代码中的 Thread thread = new Thread(PrintNumber);
修改为 Thread thread = new Thread(new ParameterizedThreadStart(PrintNumber));
即可。同样利用 ParameterizedThreadStart 委托也需要把被调用方法参数的类型定为 object 类型。
我个人建议大家在调用带参数的方法是使用匿名方法的方式调用,因为如果方法参数存在多个参数是这样调用更加便捷。当然了在遇到方法带有多个参数时你也可以使用自定义类的方式,但是这种方法并不被微软所推荐,而且这种方法代码量较大,为了调用多参数方法而去定义一个类,可以说是相当的鸡肋。
四、lock
当多个线程同时访问同一个对象时,会出现数据不正确的问题,下面我们先通过一个代码看一下这种情况。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using static System.Console; namespace ThreadLock { class Program { static void Main(string[] args) { CountOperating countOperating = new CountOperating(); Thread thread1 = new Thread(() => update(countOperating)); Thread thread2 = new Thread(() => update(countOperating)); thread1.Start(); thread2.Start(); thread1.Join(); thread2.Join(); WriteLine(countOperating.count); Read(); } static void update(CountOperating countOperating) { for (int i = 0; i < 10000; i++) { countOperating.Add(); countOperating.Subtraction(); } } } class CountOperating { public int count { get; set; } public void Add() { count++; } public void Subtraction() { count--; } } }
上述代码我们希望最后输出的结果是 0 ,但是在代码运行后发现输出结果大部分情况并不是 0 ,这时因为我的创建的 CountOperating 类并非线程安全的了类,当多个线程同时调用同一个 CountOperating 实例时,有很大的可能出现如下情况。首先线程1将 count 值加1,这时第二个线程也获取到 count 的值,此时值已经变为1,再次加1后值变为了2,这时第一个线程再次获取到 count 值值为2,第一个线程在获取到值后准备进行减运算,但是第二个线程也获取到了 count 值值也是2,接着第一个线程执行了减操作,此时得到的值是1,然后第二个线程也同样进行了减操作,此时的值依然是1,也就是说我们只执行了一次减法操作,两次加法操作。为了防止这种情况的发生,我们就需要将我们创建的类修改为线程安全的类,也就是说当一个线程调用 CountOperating 实例时其他线程只能等待。因此我们在这里引入了 lock ,lock 关键字可确保当一个线程位于代码的临界区时,另一个线程不会进入该临界区。 如果其他线程尝试进入锁定的代码,则它将一直等待,直到该对象被释放。lock 关键字在块的开始处调用 Enter,而在块的结尾处调用 Exit。 ThreadInterruptedException 引发,如果 Interrupt 中断等待输入 lock 语句的线程。通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。根据前面所说的我们将 CountOperating 代码修改如下即可:
class CountOperating { readonly object obj = new object(); public int count { get; set; } public void Add() { lock (obj) { count++; } } public void Subtraction() { lock (obj) { count--; } } }
五、Monitor
当我们使用 lock 关键字来锁定一个对象时,其他需要访问该对象的线程会处于阻塞状态,需要等到这个对象解锁后才能进行下一步操作,但是这会出现严重的性能问题和死锁的问题,性能问题相关的解决方式我会在后面的文章讲解,这一小节主要是讲解死锁的解决方案。所谓死锁举个例子来说就是线程A锁定了对象A,线程B锁定了对象B,线程A需要对象B释放后才能释放对象A,但是线程B要等到对象A释放后才能释放对象B。这样对象A和对象B永远不会被释放,线程A和线程B就永远在等待。为了解决这个问题微软为我们提供了一个解决方案,利用 Monitor 类来避免死锁,它通过获取和释放排它锁的方式实现多线程的同步问题。实际上在 .NET 中 lock 关键字时 Monitor 类用例的语法糖,lock 是对 Monitor 的 Enter 和 Exit 的一个封装,因此 Monitor 类的 Enter() 和 Exit() 方法的组合使用可以用 lock 关键字替代。Monitor 类除了具有 lock 功能还有以下功能:
- TryEnter() :解决长期死等的问题,如果一个并发经常发生,并且持续时间很长,使用TryEnter,可以有效防止死锁或者长时间 的等待。
- Wait() : 释放对象上的锁,以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。脉冲信号用于通知等待线程有关对象状态的更改。
- Pulse() / PulseAll() : 向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态。
上面这三个功能我将会在后续的文章中逐步讲解,下面我们先看一下 Monitor 类的基本用法:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace ThreadMonitor { class Program { static void Main(string[] args) { object lock1 = new object(); object lock2 = new object(); Thread thread = new Thread(() => Lock(lock1, lock2)); Thread thread2 = new Thread(() => Lock(lock1, lock2)); thread.Start(); lock(lock2) { Thread.Sleep(2000); if(Monitor.TryEnter(lock1,5000)) { Console.WriteLine("获取了被锁定的对象"); } else { Console.WriteLine("超时了"); } } thread2.Start(); lock (lock2) { Thread.Sleep(1000); lock (lock1) { Console.WriteLine("获取了被锁定的而对象"); } } } static void Lock(object lock1,object lock2) { lock(lock1) { Thread.Sleep(1000); lock (lock2); } } } }
上述代码中的 Lock 方法先锁定了第一个 lock 对象然后一秒钟后有锁定了第二个 lock 对象。值后在 Main 方法中创建了两个线程都调用 Lock 方法,然后通过两种方式锁定第一个 lock 和第二个 lock ,第一种方法我们使用 Monitor.TryEnter 来锁定对象,并设置了超时时间,一旦超时将会输出 “超时了”,但是第中方式我们利用 lock 来锁定对象,这样就创建了一个死锁。
六、线程异常处理
线程也是代码,因此也会出现异常,大部分开发人员的习惯是直接向上抛出异常,这种做法在普通的代码中并不错,向上抛出异常让方法的调用方去处理这个异常,但是在线程中这种做法就是错误的,因为抛出的异常无法在线程之外被检测的,因此我们必须在线程中将异常处理掉,也就是说在异常中必须使用 try…catch 语句块来捕获和处理异常。
七、源码下载
https://github.com/Thomas-Zhu/Multithreading/tree/master/no2/NoTwo
了”,但是第中方式我们利用 lock 来锁定对象,这样就创建了一个死锁。