参考:https://blog.csdn.net/qq_35040828/article/details/73123996
1. 基础语法
static void Main() { Thread t = new Thread(WriteY); t.Start(); } static void WriteY() { while (true) Console.Write("y"); }
2.线程安全
1)排他锁
解决方法 提供一个排他锁
static bool done; static object locker; static void Main() { new Thread(Go).Start(); Go(); } static void Go() { lock (locker) { if (!done) { Console.WriteLine("Done"); done = true; } } }
停止线程的方法 : 锁 、 或sleep 、或 用join方法等待另一个线程结束
2)暂停或者sleep
写在调用方法里 ?
Thread.Sleep(TimeSpan.FromSeconds(30));
3) joint方法 等待另一个线程结束:
Thread t = new Thread(Go); //Assume Go is some static method t.Start(); t.Join(); //Wait (block) until thread t ends
3.创建和开始使用多线程
线程用Thread类来创建, 通过ThreadStart委托来指明方法从哪里开始运行,下面是ThreadStart委托如何定义的:
public delegate void ThreadStart();
1)创建方法
下面是一个例子,使用了C#的语法创建TheadStart委托:
Thread t = new Thread (new ThreadStart(Go)); t.Start();
一个线程可以通过C#堆委托简短的语法更便利地创建出来:
Thread t = new Thread(Go);
//No need to explicitly use ThreadStart t.Start();
还有一种方法是使用匿名的方法来启动
Thread t = new Thread(delegate(){ Console.WriteLine ("Hello!"); }); t.Start();
线程有一个IsAlive属性,在调用Start()之后直到线程结束之前一直为true。一个线程一旦结束便不能重新开始了。
2)将数据传入ThreadStart中
threadStart 不接受参数
ParameterizedThreadStart 可以接收一个单独的object类型参数
public delegate void ParameterizedThreadStart (object obj);
例如:
static void Main() { Thread t = new Thread(go); t.start(true); //go(true) go(false); } static void go(object upperCase) { bool upper = (bool)upperCase; Console.writeLine(upper?“Hello”:"hello"); }
在整个例子中,编译器自动推断出ParameterizedThreadStart委托,因为Go方法接收一个单独的object参数,就像这样写:
Threadt = new Thread(new ParameterizedThreadStart (Go)); t.Start(true);
ParameterizedThreadStart的特性是在使用之前,我们必须对我们想要的类型(例如bool)进行装箱 操作,并且 只能接收一个参数,一个
static void Main() { Thread t = new Thread(delegate(){ WriteText ("Hello");}); t.Start(); } static void WriteText(string text) { Console.WriteLine (text); }
优点是目标方法可以接收任意数量的参数,并且没有装箱操作。
不过如果将一个外部变量放入匿名方法中
例如:
static void Main() { string text= "Before"; Thread t = new Thread(delegate() { WriteText (text); }); text= "After"; t.Start(); } static void WriteText(string text) { Console.WriteLine (text); }
当外部变量被后来的部分修改了值的时候,可能会透过外部变量进行无意的互动,外部变量最好被处理成只读的,除非添加锁。
另一个方法是将对象实例的方法而不是静态方法传入到线程中,对象实例的属性告诉线程要做什么:
class ThreadTest { bool upper; static void Main() { ThreadTest instance1 = new ThreadTest(); instance1.upper= true; Thread t = new Thread(instance1.Go); t.Start(); ThreadTest instance2 = new ThreadTest(); instance2.Go(); //主线程——运行 upper=false } void Go() { Console.WriteLine (upper ? "HELLO!" :"hello!"); }
4.命名线程
线程可以通过name属性进行 命名,线程的名字可以在任意时候进行设置,但是只能设置一次 ,崇明会引发异常。
程序的主线程也可以被命名,例:
static void Main() { Thread.CurrentThread.Name = "main"; Thread work = new Thread(go); work.Name = "worker"; work.Start(); go(); Console.ReadLine(); } static void go () { Console.WriteLine("Hello from " + Thread.CurrentThread.Name); }
5.前台和后台线程
线程默认为前台线程。
后台线程,当所有的前台线程结束之后,不维持程序的存活
IsBackground 属性控制它的前后台状态,例:
static void Main(string []args) { Thread worker = new Thread(delegate () { Console.ReadLine(); }); if (args.Length > 0) worker.IsBackground = true; worker.Start(); }
如果程序被调用的时候没有任何参数,工作线程为前台线程,并且将等待ReadLine语句来等待用户的触发回车,这期间,主线程退出,但是程序保持运行,因为一个前台线程仍然活着。
另一方面如果有参数传入Main(),工作线程被赋值为后台线程,当主线程结束程序立刻退出,终止了ReadLine。
后台线程终止 的这种方式 ,使任何最后的操作都 被规避了 ,这种方式是不太合适的,应该等后台工作线程完成后再结束程序 ,可以使用timeout(或大部分时候使用Threa.join)。
如何因为某种原因,某个线程无法完成,可以用试图终于它的方式,如果失败了再抛弃线程,允许它与进行一起消亡?
拥有一个后台工作线程是有益的,最直接的理由是:当结束程序的时候它总是可能有最后 的发言权,交互不会消亡的前台线程,保证程序的正常退出。抛弃一个前台工作线程是尤为危险的,尤其是对windowform程序 ,因为我程序直到主线程结束时才退出,但是进程依然运行着。在Windows任务管理器它将从应用程序栏消失不见,但却可以在进程栏找到它。除非用户找到并结束它,它将继续消耗资源,并可能阻止一个新的实例的运行从开始或影响它的特性。
对于程序失败退出的普遍原因就是存在“被忘记”的前台线程。
6.线程的优先级
线程的priority 属性确定了线程 相对于其他同一进程的活动的线程拥有多少执行时间,以下是级别:
enum ThreadPriority{ Lowest,BelowNormal,normal,AboveNormal,Highest }
只有有多个线程同时为活动时,优先级才有作用 。
设置 一个线程的优先级为高一些 ,并不意味着它能执行实时的工作,因为它受限于程序 的进程的级别。要执行实时的工作,必须 提升在System.Diagnostics 命名空间下Process的级别,像下面这样:
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
ProcessPriorityClass.High 其实是一个短暂缺口的过程中的最高优先级别:Realtime。设置进程级别到Realtime通知操作系统:你不想让你的进程被抢占了。如果你的程序进入一个偶然的死循环,可以预期,操作系统被锁住了,除了关机没有什么可以拯救你了!基于此,High大体上被认为最高的有用进程级别。
如果一个实时的程序有一个用户界面,提升进程的级别是不太好的,因为当用户界面UI过于复杂的时候,界面的更新耗费过多的CPU时间,拖慢了整台电脑。(虽然在写这篇文章的时候,在互联网电话程序Skype侥幸地这么做, 也许是因为它的界面相当简单吧。) 降低主线程的级别、提升进程的级别、确保实时线程不进行界面刷新,但这样并不能避免电脑越来越慢,因为操作系统仍会拨出过多的CPU给整个进程。最理想的方案是使实时工作和用户界面在不同的进程(拥有不同的优先级)运行,通过Remoting或共享内存方式进行通信,共享内存需要Win32 API中的 P/Invoking。(可以搜索看看CreateFileMapping 和 MapViewOfFile)
7.异常处理
任何线程创建范围内 try/catch/finally块,当线程开始执行便不再与其有任何关系。
static void Main() { try { new Thread(go).Start(); } catch (Exception ex) { //不会在这里得到异常 Console.WriteLine("Exception!"); throw; } } static void go() { throw null; }
这里try
/
catch
语句一点用也没有,新创建的线程将引发NullReferenceException异常。当你考虑到每个线程有独立的执行路径的时候,便知道这行为是有道理的
补救方法是在线程处理的方法中加入自己的异常处理:
public static void Main() { new Thread(Go).Start(); } static void Go() { try { throw null; //这个异常在下面会被捕捉到... } catch (Exception ex) { //记录异常日志,并且或通知另一个线程 我们发生错误 } }
从.NET 2.0开始,任何线程内的未处理的异常都将导致整个程序关闭,这意味着忽略异常不再是一个选项了。因此为了避免由未处理异常引起的程序崩溃,try/catch块需要出现在每个线程进入的方法内,至少要在产品程序中应该如此。对于经常使用“全局”异常处理的Windows Forms程序员来说,这可能有点麻烦,像下面这样:
using System; using System.Threading; using System.Windows.Forms; static class Program { static void Main() { Application.ThreadException+= HandleError; Application.Run (new MainForm()); } static void HandleError(object sender,ThreadExceptionEventArgs e) { 记录异常或者退出程序或者继续运行... } }
Application.ThreadException事件在异常被抛出时触发,以一个Windows信息(比如:键盘,鼠标活着 "paint" 等信息)的方式,简言之,一个Windows Forms程序的几乎所有代码。虽然这看起来很完美,它使人产生一种虚假的安全感——所有的异常都被中央异常处理捕捉到了。由工作线程抛出的异常便是一个没有被Application.ThreadException捕捉到的很好的例外。(在Main方法中的代码,包括构造器的形式,在Windows信息开始前先执行)
.NET framework为全局异常处理提供了一个更低级别的事件:AppDomain.UnhandledException,这个事件在任何类型的程序(有或没有用户界面)的任何线程有任何未处理的异常触发。尽管它提供了好的不得已的异常处理解决机制,但是这不意味着这能保证程序不崩溃,也不意味着能取消.NET异常对话框。
在产品程序中,明确地使用异常处理在所有线程进入的方法中是必要的,可以使用包装类和帮助类来分解工作来完成任务,比如使用BackgroundWorker类(在第三部分进行讨论)