<一>Thread 线程基础
一、新建一个控制台,创建线程,并启动线程(start)
Thread t = new Thread(PrintfNum); t.Start(); PrintfNum(); static void PrintfNum() { Console.WriteLine("启动"); for (int num = 0; num < 10; num++) { Console.WriteLine(num); } }
上述方法启动了一个子线程去运行PrintfNum方法,主程序本身也执行了一次PrintfNum。来执行看看结果,两个启动一起显示了,说明子线程是和主线程是同时运行的。
二、线程等待(Join)
Thread t = new Thread(PrintfNumWait); t.Start(); t.Join(); PrintfNum(); static void PrintfNumWait() { Console.WriteLine("启动延迟方法"); Thread.Sleep(2000); }
当主线程的后续操作需要等待线程执行完成后再运行时,就应该进行等待,如上述方法,等待线程2秒后,再进行打印数字。
三、线程终止(abort),线程等待(Suspend),线程继续(Resume) 已经被弃用。
一个线程在终止时会强制中断线程的执行,不管方法是否执行完了,并且还会释放这个线程所持有的所有的锁对象。这一现象会被其它因为请求锁而阻塞的线程看到,使他们继续向下执行。这就会造成数据的不一致。
一个线程被挂起时不会破换对象和强制释放锁,相反它会一直保持对锁的占有,如果不使用Resume方法唤醒,就会造成死锁。
四、检测线程状态(ThreadState)
Thread t = new Thread(PrintfNumWait); t.Start(); for (int i = 0; i < 5; i++) { Thread.Sleep(1000); Console.WriteLine(t.ThreadState.ToString()); } static void PrintfNumWait() { Console.WriteLine("启动延迟方法"); Thread.Sleep(2000); }
五、线程优先级:CPU核心大部分时间在运行高优先级的线程,只留给剩下的线程很少的时间来运行。所以最高优先级的线程通常会计算更多的迭代
Thread t = new Thread(PrintfNumWait); t.Priority = ThreadPriority.AboveNormal; t.Start();
Lowest, 最低级别
BelowNormal, 低于一般
Normal, 一般线程
AboveNormal, 高于一般
Highest 最高级别
六、前后、后台线程
默认情况下,显式创建的线程是前台线程。通过手动的设置thread对象的IsBackground 属性为ture来创建一个后台线程。进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。
一个重要注意事项是如果程序定义了一个不会完成的前台线程,主程序并不会正常结束。
Thread t = new Thread(PrintfNumWait); t.Start(); Thread t1 = new Thread(PrintfNum); t1.IsBackground = true; t1.Start();
七、向线程传递参数
线程的参数传递由多种方式,常用的有全局变量,start()传参,另一种是lambda表达式。lambda表达式时一个闭包,解析时会解析成一个类,利用类的构造函数接收参数,然后用类全局变量来提供给线程使用。
Thread t = new Thread(PrintfNum); TestModel m=new TestModel (); m.count = 2; m.name = "test"; t.Start(m); Thread t2 = new Thread(() => PrintfNum(m)); t2.Start(); static void PrintfNum(object model) { TestModel m = (TestModel)model; for (int i = 0; i < m.count; i++) { Console.WriteLine(m.name+i); } } public class TestModel { public int count { get; set; } public string name { get; set; } }
八、锁(lock)
当多个线程同时访问一个资源时,会产生资源竞争。导致数据在多线程环境中经常出现数据返回错误。
compute c=new compute (); c.sleep = 3500; Thread t1 = new Thread(() => PrintfNum(c)); c.sleep = 2200; Thread t2 = new Thread(() => PrintfNum(c)); c.sleep = 1600; Thread t3 = new Thread(() => PrintfNum(c)); t1.Start(); t2.Start(); t3.Start(); void PrintfNum(compute model) { for (int i = 0; i < 10; i++) { model.Couter(); Console.WriteLine(model.num); } } class compute { public int sleep { get; set; } public int num { get; set; } public void Couter() { num++; Thread.Sleep(sleep); num--; } }
如上代码,一个线程执行一次printfNum 最终的打印结果应该都是0.但由于中间执行了等待后,由于num是共享的,会被其他线程修改,导致打印出来的数据有很多错误的。
为了确保不会发生以上情形,必须保证当有线程操作compute对象时,所有其他线程必须等待直到当前线程完成操作。我们可以使用lock 关键字来实现这种行为。如果锁定了一个对象,需要访问该对象的所有其他线程则会处于阻塞状态,并等待直到该对象解除锁定。在for循环添加lock,如下,就能保证每次打印的都是0了。
object ss = new object(); void PrintfNum(compute model) { for (int i = 0; i < 10; i++) { lock (ss) { model.Couter(); Console.WriteLine(model.num); } } }
九、使用monitor类锁定资源
由于lock容易产生死锁,那么先来了解一下什么叫死锁。
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下代码:
object locker1 = new object(); object locker2 = new object(); new Thread(()=>Printf(locker1, locker2)).Start(); lock (locker2) { lock (locker1) { } } void Printf(object A,object B) { lock (A) { Thread.Sleep(1000); lock (B) { } } }
来看看lock的源代码如下,先enter一个资源进行锁定,然后加个try进行异常判定,最后解锁资源。如果在try里面被阻塞死了,那么这个资源就无法走到finally里面了
Monitor.Enter(this); try { } finally { Monitor.Exit(this); }
所以lock实际上还是使用的Monitor,而monitor有一个tryenter的方法。它支持接收一个超时时间。这样就可以避免死锁了。
if (Monitor.TryEnter(this, 1000)) { //......业务 Monitor.Exit(this); }
十、异常处理
多线程只能在线程方法里面进行处理,异常不可以跨线程捕获,也就是说异常不能抛出到主线程获取。