一、使用线程的理由
1、可以使用线程将代码同其他代码隔离,提高应用程序的可靠性。
2、可以使用线程来简化编码。
3、可以使用线程来实现并发执行。
二、基本知识
1、进程与线程:进程作为操作系统执行程序的基本单位,拥有应用程序的资源,进程包含线程,进程的资源被线程共享,线程不拥有资源。
2、前台线程和后台线程:通过Thread类新建线程默认为前台线程。当所有前台线程关闭时,所有的后台线程也会被直接终止,不会抛出异常。
3、挂起(Suspend)和唤醒(Resume):由于线程的执行顺序和程序的执行情况不可预知,所以使用挂起和唤醒容易发生死锁的情况,在实际应用中应该尽量少用。
4、阻塞线程:Join,阻塞调用线程,直到该线程终止。
5、终止线程:Abort:抛出 ThreadAbortException 异常让线程终止,终止后的线程不可唤醒。Interrupt:抛出 ThreadInterruptException 异常让线程终止,通过捕获异常可以继续执行。
6、线程优先级:AboveNormal BelowNormal Highest Lowest Normal,默认为Normal。
三、线程的使用
线程函数通过委托传递,可以不带参数,也可以带参数(只能有一个参数),可以用一个类或结构体封装参数。
四、线程池
由于线程的创建和销毁需要耗费一定的开销,过多的使用线程会造成内存资源的浪费,出于对性能的考虑,于是引入了线程池的概念。
线程池维护一个请求队列,线程池的代码从队列提取任务,然后委派给线程池的一个线程执行,线程执行完不会被立即销毁,这样既可以在后台执行任务,又可以减少线程创建和销毁所带来的开销。
线程池线程默认为后台线程(IsBackground)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ConsoleApplication1 { class Program { static void Main( string [] args) { //无参数 Thread t1 = new Thread( new ThreadStart(GetName1)); t1.IsBackground = true ; t1.Start(); //多个参数,使用类内部调用,多个参数还可以使用 struct 结构 UserInfo userInfo = new UserInfo() { Name = "jack" , Address = "ShangHai" }; Thread t2 = new Thread( new ThreadStart(userInfo.GetUserInfo)); t2.IsBackground = true ; t2.Start(); //单个参数,使用线程调用,参数须是object类型 Thread t3 = new Thread( new ParameterizedThreadStart(GetName2)); t3.IsBackground = true ; t3.Start( "rolly" ); //加入到线程池队列中,多个参数,使用 类 或者 struct 结构 ThreadPool.QueueUserWorkItem(GetUserInfo, userInfo); Console.ReadLine(); } //无参数1 public static void GetName1() { Console.WriteLine( "无参数1:My name is Jay!" ); } //有一个参数,参数须是object类型 public static void GetName2( object Name) { Console.WriteLine( "有参数,使用线程调用:My name is " + Name.ToString()); } //加入到线程池中,多个参数,使用 类 或者 struct 结构,参数须是object类型 public static void GetUserInfo( object obj) { //强转 UserInfo model = (UserInfo)obj; Console.WriteLine( "多个参数,使用线程调用:My name is " + model.Name + ", Address: " + model.Address); } } public class UserInfo { public string Name { get ; set ; } public string Address { get ; set ; } public void GetUserInfo() { Console.WriteLine( "多个参数 类内部自己调用:My name is " + Name + ", Address: " + Address); } } } |
五、Task类
使用ThreadPool的QueueUserWorkItem()方法发起一次异步的线程执行很简单,但是该方法最大的问题是没有一个内建的机制让你知道操作什么时候完成,有没有一个内建的机制在操作完成后获得一个返回值。为此,可以使用System.Threading.Tasks中的Task类。
构造一个Task<TResult>对象,并为泛型TResult参数传递一个操作的返回类型。
// 执行一个无返回值的任务 Task.Run(() => { Console.WriteLine("runing ..."); });
// 执行一个返回 int 类型结果的任务 var res1 = Task.Run<int>(() => { return 483; });
// 声明一个任务,仅声明,不执行 Task t = new Task(() => { Console.WriteLine("声明"); });
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | namespace Test { class Program { static void Main( string [] args) { Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 1000); t.Start(); t.Wait(); Console.WriteLine(t.Result); Console.ReadKey(); } private static Int32 Sum(Int32 n) { Int32 sum = 0; for (; n > 0; --n) checked { sum += n;} //结果太大,抛出异常 return sum; } } } |
一个任务完成后,它可以启动另一个任务,下面重写了前面的代码,不阻塞任何线程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApplication1 { public class TaskTest { public static void Test() { IntTest it = new IntTest() { n = 1000, m = 500 }; Task<Int32> t = new Task<Int32>(n => SumTest(it), 1000); t.Start(); //等待此线程结束 //t.Wait(); //线程结束后,自动开始下一个任务 t.ContinueWith(task => Test2(t.Result)); } private static Int32 SumTest(IntTest it) { return it.n + it.m; } private static void Test2( int data) { Console.WriteLine( "test data:" + data); } } public class IntTest { public int n { get ; set ; } public int m { get ; set ; } } } |
六、委托异步执行
委托的异步调用:BeginInvoke() 和 EndInvoke()
在C#中使用线程的方法很多,使用委托的BeginInvoke和EndInvoke方法就是其中之一。
BeginInvoke方法可以使用线程异步地执行委托所指向的方法。
然后通过EndInvoke方法获得方法的返回值(EndInvoke方法的返回值就是被调用方法的返回值),或是确定方法已经被成功调用。
我们可以通过四种方法从EndInvoke方法来获得返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | namespace Test { public delegate string MyDelegate( object data); class Program { static void Main( string [] args) { MyDelegate mydelegate = new MyDelegate(TestMethod); IAsyncResult result = mydelegate.BeginInvoke( "Thread Param" , TestCallback, "Callback Param" ); //异步执行完成 string resultstr = mydelegate.EndInvoke(result); } //线程函数 public static string TestMethod( object data) { string datastr = data as string ; return datastr; } //异步回调函数 public static void TestCallback(IAsyncResult data) { Console.WriteLine(data.AsyncState); } } } |
当使用BeginInvoke异步调用方法时,如果方法未执行完,EndInvoke方法就会一直阻塞,直到被调用的方法执行完毕。
七、线程同步
1)原子操作(Interlocked):所有方法都是执行一次原子读取或一次写入操作。
2)lock()语句:避免锁定public类型,否则实例将超出代码控制的范围,定义private对象来锁定。
3)Monitor实现线程同步
通过Monitor.Enter() 和 Monitor.Exit()实现排它锁的获取和释放,获取之后独占资源,不允许其他线程访问。
还有一个TryEnter方法,请求不到资源时不会阻塞等待,可以设置超时时间,获取不到直接返回false。
4)ReaderWriterLock
当对资源操作读多写少的时候,为了提高资源的利用率,让读操作锁为共享锁,多个线程可以并发读取资源,而写操作为独占锁,只允许一个线程操作。
5)事件(Event)类实现同步
事件类有两种状态,终止状态和非终止状态,终止状态时调用WaitOne可以请求成功,通过Set将时间状态设置为终止状态。
1)AutoResetEvent(自动重置事件)
2)ManualResetEvent(手动重置事件)
6)信号量(Semaphore)
信号量是由内核对象维护的int变量,为0时,线程阻塞,大于0时解除阻塞,当一个信号量上的等待线程解除阻塞后,信号量计数+1。
线程通过WaitOne将信号量减1,通过Release将信号量加1,使用很简单。
7)互斥体(Mutex)
独占资源,用法与Semaphore相似。
8)跨进程间的同步
通过设置同步对象的名称就可以实现系统级的同步,不同应用程序通过同步对象的名称识别不同同步对象。
来源:http://www.cnblogs.com/luxiaoxun/p/3280146.html
多线程访问公共资源,可以加锁,但最好不要用,容易出问题,再找找其他方法。
Task使用:https://www.cnblogs.com/pengstone/archive/2012/12/23/2830238.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】