总目录
前言
在日常开发中,多线程是避不开的痛,用吧,总容易出问题,不用吧,程序性能又会及其慢,如何做才能避开线程的坑,更好的使用多线程呢?本文将会全面的介绍C#多线程的知识,让你对多线程有更深的认识。
一、多线程以及与之相关概念
1.基本概念
1)进程
狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
简单理解:当一个程序运行的时候,他就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。
2)线程
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel
thread),而把用户线程(user thread)称为线程。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。
但同一进程中的多个线程有各自的调用栈(call stack)、寄存器环境(register context)、线程本地存储(thread-local storage)。
通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。
简单理解:一个进程包含多个线程,线程是进程中一个单一顺序的控制流。
3)多线程
(1)概念
是指从软件或者硬件上实现多个线程并发执行的技术。
(2)原理
实现多线程是采用一种并发执行机制,其原理:简单地说就是把一个处理器划分为若干个短的时间片,每个时间片依次轮流
执行处理各个应用程序,由于一个时间片很短,相对于一个应用程序来说,就好像是处理器在为自己单独服务一样,从而达到多个应用程序在同时进行的效果。无论是过去还是现在,世界上大多数计算机仍然采用的是冯·诺依曼结构,这种结构的特点就是顺序处理,一个处理器在同个时刻只能处理一件事情。Windows95/NT采用一种全新的任务调度策略,它把一个进程划分为多个线程,每个线程轮流占用CPU的运算时间,操作系统不断地把线程挂起、唤醒、再挂起、再唤程,如此反复,由于现在CPU的速度比较快,给人的感觉是多个线程在同时执行,就好像有多个CPU存在于计算机中一样。
即是说,如果把CPU比作一个员工的话,他自始至终就是同一时刻只能处理一件事情,而为了实现同时处理多个任务的时候,他将按照一定的调度干一会儿任务A,然后干一会儿任务B,在干一会儿任务C,由于他干事情的速度很快,给人的感觉就是多个任务在同时进行。
(3)优点(资源换性能)
1、提高CPU的利用率
2、提高程序运行效率
3、可以加快应用程序的响应,对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作置于一个新的线程,就可以避免这种尴尬的情况
(4)缺点
1、如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换
2、更多的线程需要更多的内存空间
3、线程虽好,但是控制不好,将会导致很多的Bug,因此要小心使用
4、线程的中止需要考虑其对程序运行的影响
5、多个线程是共享进程资源的,因此需要解决竞用共享资源的问题
如果有多个线程同时运行,而且它们试图访问相同的资源,就会遇到竞用共享资源的问题。
举个例子来说,两个线程都需要将信息同时发送给一台打印机。为解决这个问题,对那些可共享的资源来说(比如打印机),它们在使用期间必须进入锁定状态。所以一个线程可将资源锁定,在完成了它的任务后,再解开(释放)这个锁,使其他线程可以接着使用同样的资源
(5)多线程适用场景
1、当主线程执行耗时的任务时,会导致用户界面卡死,导致用户体验差,这种可另开一个线程处理耗时任务,保证主线程正常运行
2、在一些等待的任务实现上如用户输入、文件读写和网络收发数据(请求接口)等
3、利用多线程拆分复杂运算,提高计算速度
(6)多线程不适用场景
单线程就能很好解决的时候,就不要为了适用多线程而使用多线程
(7)故事案例理解
1、如果将工厂比作系统,生产线比作进程,那么员工就是线程,
2、 假定这家特殊的工厂营收是有上限(就像CPU等硬件设备的处理上限)
3 、生产线上有不同的员工做着不同的工序(这就像多线程同步完成多个任务一样)
4 、同一个工序(比如用扳手拧螺丝),可能多个员工完成(表示多个线程也可能执行同一个任务)
5 、现在 生产线上拧螺丝的扳手只有一把(平常这个工序不太忙),但是有2-3员工要用,那么需要用的人,就必须等到上一个人使用完才行(表示线程需要处理一些共享资源竞用的问题)
6 、后面生产线接了个大单,生产线给每个工序都增加了员工并且开始加班,刚开始少量增加还可以,越到后面员工越多,给工厂加大了管理的成本以及工资开销,由于营收有上限,导致不久后工厂就撑不住了,那么只有裁员或者减少每个员工的工资,导致每个员工干活的积极性也降低了,加上不断加班,工作效率也降低了。(表示多线程过多就会占用过多的系统资源,导致程序性能降低)
2.同步、异步
1)同步方法
线性执行,从上往下依次执行,同步方法执行慢,消耗的计算机资源少。
案例:同步方法
public static void SyncMethod(string str)
{
Console.WriteLine($"--------同步方法{str}--------");
for (int i = 0; i < 10; i++)
{
Thread.Sleep(200);
Console.WriteLine($"同步:{i}");
}
}
//Main中调用
SyncMethod("一");
SyncMethod("二");
Console.WriteLine("--------主线程--------");
Console.ReadLine();
结果输出:按照调用顺序和业务顺序依次输出
2)异步方法
线程和线程之间,不再线型执行,多个线程总的耗时少,执行快,消耗的计算机资源多,各线程执行是无序的。
案例:(这里主要理解同步异步,先不纠结线程的使用,后面就会讲到)
//异步方法
public static void AsyncMethod(string str)
{
Console.WriteLine($"--------异步方法{str}--------");
Task.Run(()=> {
for (int i = 0; i < 10; i++)
{
Thread.Sleep(200);
Console.WriteLine($"异步{str}:{i}");
}
});
}
//Main中调用
SyncMethod("一");
SyncMethod("二");
Console.WriteLine("--------主线程--------");
Console.ReadLine();
输出结果:并没有按照调用顺序输出结果,而是异步交错的输出结果
以上案例是为了加深对同步和异步方法的理解
二、Thread
Thread是.Net最早的多线程处理方式,它出现在.Net1.0时代,虽然现在已逐渐被微软所抛弃,微软强烈推荐使用Task,但从多线程完整性的角度上来说,我们有必要了解下早期多线程的是怎么处理的,以便体会.Net体系中多线程处理方式的进化。
关于线程调试工具WinDbg的使用,感兴趣的可参考:官方文档,WinDbg介绍
1.线程的使用
1)创建并开启线程
//【1】创建一个线程
Thread thread= new Thread(new ThreadStart(()=>
{
Console.WriteLine("创建一个线程!");
}));
//【2】开启线程
thread.Start();
//【简写】
new Thread(()=> { Console.WriteLine("创建一个线程!"); }).Start();
由上可知创建需要使用Thread,然后调用Start()方法启动
public delegate void ThreadStart();
另外ThreadStart的本质是委托,因此可以直接使用lambda表达式替换,因此就有了简写的方式
2)线程的属性设置&方法调用
(1)设置前台后台线程
前台线程:界面关闭,线程会等待执行完才结束
后台线程:界面关闭,线程也就随之消失
Thread thread = new Thread(() => { Console.WriteLine("创建一个线程!"); });
thread.IsBackground = true;
(2)调用方法,设置线程的停止等待
Thread thread = new Thread(() => { Console.WriteLine("创建一个线程!"); });
thread.Start();
#挂起/暂停一个线程,已弃用
thread.Suspend();
#恢复挂机的线程,已弃用
thread.Resume();
#停止线程,通过抛异常的方式停止
thread.Abort();
#中断线程
thread.Interrupt();
#重启 停止的线程
Thread.ResetAbort();
if (thread.ThreadState!=ThreadState.Stopped)
{
#休息100ms,【此时间内不消耗计算机资源】
Thread.Sleep(100);
}
#让主线程等待,直到当前thread线程执行完毕
thread.Join();
//让主线程等100ms,100ms后不管当前线程是否执行完毕,都继续执行后续的操作
thread.Join(100);
(3)设置线程的优先级
Thread thread1 = new Thread(() => { Console.WriteLine("创建一个线程1!"); });
Thread thread2 = new Thread(() => { Console.WriteLine("创建一个线程2!"); });
thread1.Priority = ThreadPriority.Highest;
thread2.Priority = ThreadPriority.Lowest;
thread1.Start();
thread2.Start();
通过 ThreadPriority设置线程的优先级,设置好后,系统会根据设置的优先级来执行
线程执行完就会变为dead状态 不会占用系统资源
2.WinForm中跨线程操作主线程UI
1)案例
实现:时间一个按钮,让一个Label像时钟一样展示时间
(1)子线程中访问UI线程并修改UI
(2)使用控件的Invoke方法跨线程操作
private void button1_Click(object sender, EventArgs e)
{
Thread thread = new Thread(() => {
while (true)
{
this.lb_time.Invoke(new Action(()=>
{
this.lb_time.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}));
Thread.Sleep(800);
}
});
thread.IsBackground = true;
thread.Start();
}
通过以上代码即可实现跨线程访问UI而不会卡界面
2)Control.Invoke和Control.BeginInvoke
Control.Invoke (Delegate method) :
在拥有此控件的基础窗口句柄的线程上执行指定的委托。
Control.BeginInvoke(Delegate method) :
在创建控件的基础句柄所在线程上异步执行指定委托。
(1) 为啥上面案例中使用Invoke后不报错?
Control的Invoke和BeginInvoke都执行在主线程即UI线程上
上面的案例之所以可以做到不报错,因为子线程中通过使用控件的Invoke方法将UI上的改动委托给UI主线程自己去操作,就不涉及到跨线程的问题了。
(2)Control.Invoke和Control.BeginInvoke的区别
通过上图更够更深刻的明白,Invoke就是在UI线程上同步执行一段代码,
BeginInvoke就是UI线程上异步执行一段代码
(3)Invoke(Delegate method) 解析
有Invoke中的Delegate可知需要传入一个委托类型,由于Delegate 本身是abstract 禁止实例化的,因此只能传入其派生类,那么所有的委托类型都可以传入,但是不能直接传入一个lambda表达式,虽然任何 Lambda 表达式都可以转换为委托类型。
如:
private void Form2_Load(object sender, EventArgs e)
{
this.lb_time.Text = "1";
//直接使用lambda会报错
//this.lb_time.BeginInvoke(()=> { this.lb_time.Text = "2"; });
//可以使用,Action派生自Delegate
this.lb_time.BeginInvoke(new Action(()=> { this.lb_time.Text += "2"; }));
//可以使用,ThreadStart派生自Delegate
this.lb_time.BeginInvoke(new ThreadStart(()=> { this.lb_time.Text += "3"; }));
//可以使用,CustomDelegate和所有自定义delegate都派生自Delegate
this.lb_time.BeginInvoke(new CustomDelegate(()=> { this.lb_time.Text += "4"; }));
}
public delegate void CustomDelegate();
//这些委托的本质是类,但是lambda表达式的本质是匿名方法,
//虽有语法糖可将匿名方法直接赋予一个委托,但委托类型和lambda表达式两者本质上是不同的
3.其他知识点(应用场景不多,可略过)
1) Thread的扩展使用
(1) 实现多个委托在中多线程按照顺序执行
static void Main(string[] args)
{
Action action = new Action(()=> { Console.WriteLine("执行方法1"); });
Func<string> func = () =>
{
Console.WriteLine("执行方法2");
return "我是方法2返回的msg";
};
CallBackThread(action,func);
Console.ReadLine();
}
public static void CallBackThread(Action action,Func<string> func)
{
Thread thread = new Thread(()=> {
action.Invoke();
string msg= func.Invoke();
Console.WriteLine($"输出消息:{msg}");
});
thread.Start();
}
结果:按照线程中的调用顺序去执行
(2) 实现获取多线程委托的结果
获取子线程中的值并且返回,需要用到Join,主线程会等待,不推荐这种做法,还是抱着学习的态度了解一下
(1)初始实现
public static long GetUserID()
{
long result = 0;
Thread thread = new Thread(()=> {
result = DoSomething();//假定这里是访问接口获取用户Id是个很耗时的操作
});
thread.Start();
thread.Join();//这里回卡住UI,因为主线程在等待
return result;
}
//模拟耗时操作
public static long DoSomething()
{
long iResult = 0;
for (int i = 0; i < 1_000_000_000; i++)
{
iResult += i;
}
return iResult;
}
(2)包装实现
//获取子线程中值,并返回
public static Func<T> CallBackFunc<T>(Func<T> func)
{
T t = default(T);
Thread thread = new Thread(()=> { t = func.Invoke(); });
thread.Start();
return new Func<T>(() =>
{
thread.Join();//等待thread执行完成;
return t;
});
}
传入一个有返回值的并且耗时的委托,然后返回一个有返回值的委托
static void Main(string[] args)
{
Func<long> func = () =>
{
return DoSomething();
};
//这一步不会阻塞界面
Func<long> func1 = CallBackFunc<long>(func);
Console.WriteLine("子线程准备开启");
long iResult = func1.Invoke();
Console.WriteLine($"子线程完成:{iResult}");
Console.ReadLine();
}
2)数据槽,内存栅栏
这块儿内容的应用场景更少,感兴趣的可以,推荐查看数据槽,内存栅栏
三、ThreadPool
线程池是用来保存线程的一个容器,在程序创建线程来执行任务的时候线程池才会初始化一个线程,线程在执行完毕之后并不会被销毁,而是被挂起等待下一个任务的到来被激活执行任务,当线程池里的线程不够用的时候会新实例化一个线程,来执行,线程池里的线程会被反复利用。
1.使用须知
1、线程池中的所有线程都是后台线程。如果进程的所有前台线程都结束了,所有的后台线程 就会停止。不能把入池的线程改为前台线程。
2、不能给入池的线程设置优先级或名称。
3、入池的线程只能用于时间较短的任务。如果线程要一直运行(如Word的拼写检查器线程), 就应使用Thread类创建一个线程.
4、线程池里的线程会被反复利用,不需要程序员对线程的数量管控,提高性能,防止滥用
2.创建并开启一个线程
static void Main(string[] args)
{
ThreadPoolTest();
Console.WriteLine("主线程");
Console.ReadLine();
}
public static void ThreadPoolTest()
{
//ThreadPool使用QueueUserWorkItem开启一个线程
//QueueUserWorkItem方法,将方法排入队列以便执行。 此方法在 有线程池线程 变得可用时执行。
//QueueUserWorkItem(WaitCallback callBack)
//public delegate void WaitCallback(object state);
//由此可知WaitCallback是一个有object类型参数且无返回值的委托。
//QueueUserWorkItem(WaitCallback callBack, object state)
//state即WaitCallback中需要的参数, 不推荐这么使用,存在拆箱装箱的转换问题,影响性能。
//即state的值会传入CallBack这个委托中,供使用
ThreadPool.QueueUserWorkItem((obj) => { Console.WriteLine("张三"); });
ThreadPool.QueueUserWorkItem((obj) => { Console.WriteLine(obj); }, "我是鲤籽鲲");
}
3.ThreadPool的属性和方法
int maxWorkerThreads, minWorkerThreads, maxCompletionPortThreads, minCompletionPortThreads;
//获取线程中的工作线程数的最大值,和线程池中异步I/O线程的数目
ThreadPool.GetMaxThreads(out maxWorkerThreads,out maxCompletionPortThreads);
//获取线程中的工作线程数的最小值,和线程池中异步I/O线程的数目
ThreadPool.GetMinThreads(out minWorkerThreads, out minCompletionPortThreads);
//设置当前进程的最大/最大 工作线程数和IO线程数,不推荐使用,配置不当,影响性能
ThreadPool.SetMaxThreads(15,12);
ThreadPool.SetMinThreads(5,5);
Console.WriteLine($"当前进程--最大工作线程数:{maxWorkerThreads}最大的IO线程数:{maxCompletionPortThreads}");
Console.WriteLine($"当前进程--最小工作线程数:{minWorkerThreads}最小的IO线程数:{minCompletionPortThreads}");
4.实例(比较Thread 和ThreadPool)
public static void ThreadPoolTest()
{
for (int i = 1; i <=10; i++)
{
ThreadPool.QueueUserWorkItem((obj)=>
{
Console.WriteLine($"ThreadPool子线程Id:{Thread.CurrentThread.ManagedThreadId}");
DoSomething();
});
Thread.Sleep(100);
}
}
public static void ThreadTest()
{
for (int i = 1; i <= 10; i++)
{
new Thread(()=>
{
Console.WriteLine($"Thread子线程Id:{Thread.CurrentThread.ManagedThreadId}");
DoSomething();
}).Start();
Thread.Sleep(100);
}
}
Main调用输出结果:
通过输出结果的ThreadId可知,Thread会按照你的业务安排需要多少线程开启多少线程,而ThreadPool,在开启一定数量的线程后,如果发现现有的线程数量足以应对,那么就不会新开启多余的线程。
5 System.Threading.Timer
是一种简单的、轻量级计时器,它使用回调方法而不是使用事件,并由线程池线程提供支持。
可以使用System.Threading.Timer做一些桌面应用显示时钟等小功能
(1) Timer 主要使用的的构造函数
1 public Timer(TimerCallback callback, object state, int dueTime, int period);
2 public Timer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period);
参数 | 意义 |
---|---|
callback | 一个委托,表示要执行的方法 |
state | 表示回调方法callback中,要使用的对象或null |
dueTime | 表示实例后,多久后启动计时器 |
period | 表示计时间隔 |
使用Timer 需要注意:一定要声明成全局变量以保持对Timer的引用,否则会被垃圾回收
(2)实例:
//首先声明Timer变量,一定要声明称全局变量以保持对Timer的引用,不要在方法内部申明局部变量,
//否则会被垃圾回收~
private System.Threading.Timer timer;
//实例化
timer = new System.Threading.Timer((obj)=>
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
},null,0,1000);
timer.Dispose();//作用:注销(停止)计时器
//还可以通过使用Change方法停止计时器,把 dueTime 参数置为-1就可以停止定时器。
四、Task【重点】
Task是微软在.Net 4.0时代推出来的,也是微软极力推荐的一种多线程的处理方式,Task看起来像一个Thread,实际上,它是在ThreadPool的基础上进行的封装,Task的控制和扩展性很强,在线程的延续、阻塞、取消、超时等方面远胜于Thread和ThreadPool。
1.Task开启线程的方式
public static void TaskTest()
{
//【1】new Task().Start()
Task task1 = new Task(() => { Console.WriteLine("Start开启一个子线程"); });
task1.Start();
//【1】实例化Task的一个重载,可传入委托需要用到的参数
Task task1_1 = new Task((inStr) => { Console.WriteLine($"Start开启一个子线程,入参{inStr}"); },"入参AAA");
task1_1.Start();
//通过AsyncState 获取创建子线程时委托传入的参数值
string result = task1_1.AsyncState.ToString();
Console.WriteLine($"输出委托的传入参数:{result}");
//【2】Task.Run()
Task task2 = Task.Run(() => { Console.WriteLine("Run开启一个子线程"); });
//【2】Task.Run<TResult>表示一个可以返回值的异步操作
Task<long> task2_1 = Task.Run<long>(() =>
{
return DoSomethingLongTime();
});
long rst = task2_1.Result;//获取子线程返回的值
//【3】Task.Factory.StartNew
Task task3 = Task.Factory.StartNew(() => { Console.WriteLine("Factory.StartNew开启一个子线程"); });
//【3】Task.Factory.StartNew<TResult>
Task<long> task3_1 = Task.Factory.StartNew<long>(() =>
{
return DoSomethingLongTime();
});
long rst2 = task3_1.Result;
//另外还可以
TaskFactory task3_2=new TaskFactory();
task3_2.StartNew(()=> { Console.WriteLine("TaskFactory开启一个子线程");});
//【4】new Task().RunSynchronously()同步执行,上述三种均是异步
Task task4 = new Task(() => { Console.WriteLine("Factory.StartNew开启一个子线程111"); });
task4.RunSynchronously();
Task task4_1 = new Task(() => { Console.WriteLine("Factory.StartNew开启一个子线程222"); });
task4_1.RunSynchronously();
Task task4_2 = new Task(() => { Console.WriteLine("Factory.StartNew开启一个子线程333"); });
task4_2.RunSynchronously();
Task task4_3 = new Task(() => { Console.WriteLine("Factory.StartNew开启一个子线程444"); });
task4_3.RunSynchronously();
//以上线程中的方法将会依次执行,不再异步执行
//【4】new Task().RunSynchronously()同步执行且带返回值的Task,(就当没开子线程)
Task<long> task4_4 = new Task<long>(() =>
{
return DoSomethingLongTime();
});
task4_4.RunSynchronously();
long result = task4_4.Result;
}
//耗时操作
public static long DoSomethingLongTime()
{
long iResult = 0;
for (int i = 0; i < 1_000_000_000; i++)
{
iResult += i;
}
return iResult;
}
new Task().Start()开启还有一个重载的方法public void Start(TaskScheduler scheduler);
可以这样用:new Task().Start(TaskScheduler.FromCurrentSynchronizationContext())
,还没搞明白这个用法
2.Task在WinForm中跨线程操作UI
同Thread是一样的使用invoke
private void button_Click(object sender, EventArgs e)
{
Task task = new Task(() =>
{
//耗时操作
long res = DoSomething();
while (true)
{
this.lb_time.Invoke(new Action(() =>
{
this.lb_time.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}));
Thread.Sleep(100);
}
});
task.Start();
}
public long DoSomething()
{
long iResult = 0;
for (int i = 0; i < 1_000_000_000; i++)
{
iResult += i;
}
return iResult;
}
3.Task线程等待
(1)task.Wait()
task.Wait()是Task实例方法,使用时会阻塞主线程,因为Wait()会让task内部的任务执行完后,主线程才往后执行。
static void Main(string[] args)
{
Console.WriteLine("----主线程--Start-----");
TaskTest();
Console.WriteLine("----主线程--End-----");
Console.ReadLine();
}
public static void TaskTest()
{
Task task1 = new Task(()=>
{
DoSomethingLongTime();
});
task1.Start();
//【1】内部执行完方可往下执行
task1.Wait();
//【2】指定等待的毫秒数,到指定毫秒后,不管是否执行完,都往下执行,相当于说好我要卡你主线程多久
task1.Wait(1000);
//作用同【2】,不过使用TimeSpan可以指定时分秒等更多的单位,更加的灵活
task1.Wait(TimeSpan.FromMilliseconds(1000));
}
//耗时任务
public static void DoSomethingLongTime()
{
long iResult = 0;
for (int i = 0; i < 1_000_000_000; i++)
{
iResult += i;
}
Console.WriteLine("耗时任务完成!");
//return iResult;
}
从输出结果可知,如果没有使用wait,那么主线程将会正常执行,使用后主线程将会等待task中的任务执行完,才执行自己的内容。
(2)Task.WaitAny()&Task.WaitAll()
均是Task静态方法,存在多个子线程的时候,
当使用WaitAny可实现【任意一个】task完成后,就可以往后执行,同样会卡主线程。比如上位机查询报警点,任意一个报警点报警都必须触发报警。
当使用WaitAll的时候,就需要等到taskList中的所有任务完成才会往后执行,会卡主线程。对应案例的表现就是所有的报警点报警才会触发报警
static void Main(string[] args)
{
Console.WriteLine("----主线程--Start-----");
TaskTest();
Console.WriteLine("----主线程--End-----");
Console.ReadLine();
}
public static void TaskTest()
{
List<Task> taskList = new List<Task>();
Task task1 = Task.Run(() => { GetAlarmInfo("站点1"); });
Task task2 = Task.Run(() => { GetAlarmInfo("站点2"); });
Task task3 = Task.Run(() => { GetAlarmInfo("站点3"); });
taskList.Add(task1);
taskList.Add(task2);
taskList.Add(task3);
//Task.WaitAny需要传入一个Task的数组
Task.WaitAny(taskList.ToArray());
Console.WriteLine("有报警点查询到报警信息,需要报警");
//WaitAll等待所有的任务完成,才往后执行
//Task.WaitAll(taskList.ToArray());
//Console.WriteLine("所有报警点都查询到报警信息,紧急报警");
}
public static void GetAlarmInfo(string name)
{
Console.WriteLine($"Start--开始查询报警【{name}】的数据");
DoSomethingLongTime();//模拟查询任务
Console.WriteLine($"End--查询到报警【{name}】的数据");
}
public static void DoSomethingLongTime()
{
long iResult = 0;
for (int i = 0; i < 1_000_000_000; i++)
{
iResult += i;
}
}
输出结果:
(3)Task.WhenAny()&Task.WhenAll()&ContinueWith()
1、Task.WhenAny()&Task.WhenAll()均是Task静态方法
2、Task.WhenAny()+ContinueWith() 可以实现,当传入的多个线程中任意一个线程执行完成后,继续执行执行ContinueWith中的任务(不卡主线程,因为新开了一个线程)
3、Task.WhenAll()+ContinueWith() 可以实现,当传入的多个线程都执行完成后,继续执行执行ContinueWith中的任务(不卡主线程,因为新开了一个线程)
4、案例
static void Main(string[] args)
{
Console.WriteLine("----主线程--Start-----");
//TaskTest(1);
TaskTest(2);
Console.WriteLine("----主线程--End-----");
Console.ReadLine();
}
public static void TaskTest(int flag)
{
List<Task> taskList = new List<Task>();
TaskFactory factory = new TaskFactory();
taskList.Add(factory.StartNew(() => { GetAlarmInfo("站点1"); }));
taskList.Add(factory.StartNew(() => { GetAlarmInfo("站点2"); }));
taskList.Add(factory.StartNew(() => { GetAlarmInfo("站点3"); }));
if (flag == 1)
{
//当传入的多个线程中任意一个线程执行完成后,继续执行执行ContinueWith中的任务(不卡主线程,因为新开了一个线程)
Task.WhenAny(taskList.ToArray()).ContinueWith((continuationAction) =>
{
Console.WriteLine($"【{flag}】-开始紧急报警");
DoSomethingLongTime();
Console.WriteLine($"【{flag}】-紧急报警完毕");
});
}
else
{
//当传入的多个线程都执行完成后,继续执行执行ContinueWith中的任务(不卡主线程,因为新开了一个线程)
Task.WhenAll(taskList.ToArray()).ContinueWith((continuationAction) =>
{
Console.WriteLine($"【{flag}】-开始紧急报警");
DoSomethingLongTime();
Console.WriteLine($"【{flag}】-紧急报警完毕");
});
}
}
public static void GetAlarmInfo(string name)
{
Console.WriteLine($"Start--开始查询报警【{name}】的数据");
DoSomethingLongTime();//模拟查询任务
Console.WriteLine($"End--查询到报警【{name}】的数据");
}
public static void DoSomethingLongTime()
{
long iResult = 0;
for (int i = 0; i < 1_000_000_000; i++)
{
iResult += i;
}
}
输出结果:效果就是都不卡主线程
5、分析WhenAny,WhenAll,ContinueWith
分析项 | 结果 |
---|---|
Wait与When | Wait的就是需要阻塞主线程的,使用Wait就是子线程和主线程说:“你要等我干完我的事情,你再干”。使用When就是子线程和主线程说:“你干你的事情,不用管我,我什么时候还不知道,我自己处理自己的事情” |
WaitAny与WhenAny | 使用WaitAny就是有多个子线程和主线程说:“你先等着,我们中任何一个事情干完了,你再开始做你的事情”。使用WhenAny就是多个子线程和主线程说:“你干你的事情,不用管我们,我们中有人干完事情,我们自己安排后续的事情” |
WaitAll与WhenAll | 使用WaitAll就是子线程和主线程说:“你需要等我们都把事情干完了,你再开始做你的事情”。使用WhenAny就是和主线程说:“你干你的事情,不用管我,我们自己做完事情自己安排” |
ContinueWith | 使用ContinueWith就是子线程安排后续自己要做的事情 |
#表示提供的任务之一已完成的任务。 返回任务的结果是完成的任务。
public static Task<Task> WhenAny(params Task[] tasks)
#返回结果:表示所有提供的任务的完成情况的任务。
public static Task WhenAll(params Task[] tasks);
#ContinueWith返回的一个新的延续 System.Threading.Tasks.Task
##入参:在运行时,委托将作为一个参数传递给完成的任务
public Task ContinueWith(Action<Task<TResult>> continuationAction);
Task.WhenAny(taskList.ToArray()).ContinueWith()
在完成任务列表中的一个任务的任务后面使用ContinueWith再延续一个任务
Task.WhenAll(taskList.ToArray()).ContinueWith()
在完成所有任务的任务后面使用ContinueWith再延续一个任务
(4)ContinueWhenAny()&ContinueWhenAll()
1、ContinueWhenAny 的作用等于 WhenAny+ContinueWith
2、ContinueWhenAll 的作用等于 WhenAll+ContinueWith
if (flag == 1)
{
//当传入的多个线程中任意一个线程执行完成后,继续执行执行ContinueWith中的任务(不卡主线程,因为新开了一个线程)
Task.Factory.ContinueWhenAny(taskList.ToArray(), ((continuationAction) =>
{
Console.WriteLine($"【{flag}】-开始紧急报警");
DoSomethingLongTime();
Console.WriteLine($"【{flag}】-紧急报警完毕");
}));
}
else
{
//当传入的多个线程都执行完成后,继续执行执行ContinueWith中的任务(不卡主线程,因为新开了一个线程)
factory.ContinueWhenAll(taskList.ToArray(),((continuationAction) =>
{
Console.WriteLine($"【{flag}】-开始紧急报警");
DoSomethingLongTime();
Console.WriteLine($"【{flag}】-紧急报警完毕");
}));
}
只需将上面讲WhenAll和WhenAny中的两行代码进行替换即可
3 、可通过Task.Factory.ContinueWhenAny()静态方法调用
也可通过factory.ContinueWhenAll()实例方法调用
public Task ContinueWhenAny(Task[] tasks, Action<Task> continuationAction);
public Task ContinueWhenAll(Task[] tasks, Action<Task[]> continuationAction);
第一个参数均是传入一个任务列表,第二个参数表示将要延续执行的委托,
委托的参数则是第一个参数:任务列表
4.线程返回值
(1)主线程中获取返回值,会阻塞主线程
private void TaskMethod1()
{
//这样子在主线程内获取返回值,会导致主线程阻塞
Task<long> task2_1 = Task.Run<long>(() =>
{
return DoSomethingLongTime1();
});
long rst = task2_1.Result;//获取子线程返回的值
}
(2)使用ContinueWith获取返回值,并做后续操作,不会阻塞线程,并且还可以连环使用返回值
private void TaskMethod2()
{
//使用ContinueWith会另外开一个线程不会卡顿
Task<long> task2_1 = Task.Run<long>(() =>
{
//Console.WriteLine(task2_1.Result.ToString());
//第一个任务的返回值,是无法在自己内部直接获取的
return DoSomethingLongTime1();
});
task2_1.ContinueWith(t =>
{
// 这里的 t 是一个带返回值的Task
// 通过 t.Result 可以取出上一个任务的返回值,然后再行使用
Console.WriteLine(t.Result.ToString());
return 2 + t.Result;
}).ContinueWith(s=>
{
//这里没有return ,系统自动识别是没有返回值的Task
Console.WriteLine(s.Result.ToString());
}) ;
}
(3)使用ContinueWhenAny和ContinueWhenAny获取返回值并作不同的操作,不会阻塞主线程
private void TaskMethod3()
{
List<Task<long>> tasks = new List<Task<long>>();
TaskFactory factory = new TaskFactory();
tasks.Add(factory.StartNew<long>(() => { return DoSomethingLongTime2(1); }));
tasks.Add(factory.StartNew<long>(() => { return DoSomethingLongTime2(2); }));
tasks.Add(factory.StartNew<long>(() => { return DoSomethingLongTime2(3); }));
tasks.Add(factory.StartNew<long>(() => { return DoSomethingLongTime2(4); }));
//在使用ContinueWhenAny获取第一个返回的数据
factory.ContinueWhenAny(tasks.ToArray(), (t) =>
{
Console.WriteLine($"第一个返回的结果:{t.Result}");
});
//使用ContinueWhenAll执行完所有任务后,找出最大值,最小值和结算总和
factory.ContinueWhenAll(tasks.ToArray(),(t)=>
{
long sumNum = 0;
long maxNum = t.Max(x => x.Result);
Console.WriteLine($"返回的最大值:{maxNum}");
long minNum = t.Min(x => x.Result);
Console.WriteLine($"返回的最小值:{minNum}");
long sum = t.Sum(x => x.Result);
Console.WriteLine($"返回的总和方式1:{sum}");
foreach (var item in t)
{
sumNum += item.Result;
};
Console.WriteLine($"返回的总和方式2:{sumNum}");
});
}
private long DoSomethingLongTime2(int id)
{
Console.WriteLine($"Start--耗时任务【{id}】开始");
long iResult = 0;
for (int i = 0; i < 1_000_000_000; i++)
{
iResult += i;
}
iResult += id;//这里只是为了后面对比大小使用
Console.WriteLine($"End--耗时任务【{id}】结束");
return iResult;
}
输出结果:
从以上的案例就可Task在任务的延续上是完胜Thread和ThreadPool
5.TaskCreationOptions&TaskContinuationOptions枚举类
(1)TaskCreationOptions
主要是可以指定任务的运行时的相关设置,创建任务时即可设置,一般默认足够使用,感兴趣的可以自行敲代码尝试一下
private void TaskMethod4()
{
Task task = new Task(() =>
{
//【None默认情况】默认情况下,子任务(即由外部任务创建的内部任务)将独立于其父任务执行,异步运行
Task task1 = new Task(() =>
{
DoSomethingLongTime2(1);
}, TaskCreationOptions.None);
task1.Start();
//【AttachedToParent】指定将任务附加到任务层次结构中的某个父级,将父任务和子任务同步
Task task2 = new Task(() =>
{
DoSomethingLongTime2(2);
}, TaskCreationOptions.AttachedToParent);
task2.Start();
//【LongRunning】指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件
//提示,过度订阅可能是合理的
Task task3 = new Task(() =>
{
DoSomethingLongTime2(3);
}, TaskCreationOptions.LongRunning);
task3.Start();
//防止环境计划程序被视为已创建任务的当前计划程序。
//这意味着像 StartNew 或 ContinueWith 创建任务的执行操作将被视为
//System.Threading.Tasks.TaskScheduler.Default当前计划程序。
Task task4 = new Task(() =>
{
DoSomethingLongTime2(4);
}, TaskCreationOptions.HideScheduler);
task4.Start();
//以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,
//而较晚安排运行的任务将更可能较晚运行。
Task task5 = new Task(() =>
{
DoSomethingLongTime2(5);
}, TaskCreationOptions.PreferFairness);
task4.Start();
DoSomethingLongTime2(0);
//【DenyChildAttach】指定任何尝试作为附加的子任务都无法附加到父任务,会改成作为分离的子任务执行
}, TaskCreationOptions.DenyChildAttach);
task.Start();
//task.Wait();
}
(2)TaskContinuationOptions
1 通过设置TaskContinuationOptions影响ContinueWith传入的任务的运行方式
2 【None】默认是在前面的任务完成后以异步方式运行, 如果延续为子任务,则会将其创建为分离的嵌套任务
3 其余枚举值感兴趣的可以自行尝试了解
private void TaskMethod5()
{
Task task1 = new Task(() =>
{
DoSomethingLongTime2(1);
});
//在前面的任务完成后以异步方式运行, 如果延续为子任务,则会将其创建为分离的嵌套任务。
task1.ContinueWith((t) =>
{
Console.WriteLine("TaskContinuationOptions.None");
}, TaskContinuationOptions.None).ContinueWith((s) =>
{
Console.WriteLine("TaskContinuationOptions.None");
}, TaskContinuationOptions.ExecuteSynchronously);//指定应同步执行延续任务。
task1.Start();
}
6.延迟执行
由上可知,使用Sleep是会阻塞主线程的,而是用Delay则不会阻塞主线程,因为他是另开一个线程在达到指定时间后异步执行。
7.多线程异常捕获
//【1--直接在Task外包一层捕获异常,这样是无法捕获到异常的】
private void TaskExceptionTest1()
{
try
{
throw new Exception("自定义异常");
}
catch (Exception)
{
Debug.WriteLine("自定义异常已处理");//自定义异常的可以轻松捕获
}
try
{
//对于多线程,在线程外面使用try,catch 无法捕获到异常
Task task = Task.Run(() =>
{
throw new Exception("Task内部异常");
});
}
catch (Exception ex)
{
}
}
//【2 --在线程内部获取异常,这个就是正常使用】
public static void TaskExceptionTest2()
{
Task task = new Task(() =>
{
//直接在线程内部捕获异常,同样很容易,
//【不仅捕获到异常还处理了异常】
try
{
throw new Exception("Task内部异常");
}
catch (Exception ex)
{
Console.WriteLine($"线程发生异常-异常信息:{ex.Message}");
}
});
task.Start();
}
//【3--同样Task外层包一层try catch,同时使用线程等待Task.WaitAll】
//这样会捕获到多线程内部所有的异常
//多线程专用的异常AggregateException,另外可通过InnerExceptions获取异常集合
//在多个子线程的情况下:与在线程内部捕获异常相比,这样更加方便,但是编译的时候会报错
//内部使用try catch代码会显得臃肿一点,但是简单,看具体情况选择使用
public static void TaskExceptionTest3()
{
try
{
Task task1 = Task.Run(() =>
{
throw new Exception("task1内部异常");
});
Task task2 = Task.Run(() =>
{
throw new Exception("task2内部异常");
});
//线程等待
Task.WaitAll(task1,task2);
}
catch (AggregateException aex)
{
foreach (var exception in aex.InnerExceptions)
{
Debug.WriteLine($"线程发生异常-异常信息:{exception.Message}");
}
}
catch (Exception ex)
{
}
}
8.取消线程
1)使用变量的方式,取消线程
一般取消流程是:设置一个变量来控制任务是否停止,例如设置一个变量IsCancel ,然后线程轮询查看IsCancel ,如果IsCancel 为true就取消线程,代码如下:
private bool IsCancel = false;
//点击按钮开启一个线程
private void button8_Click(object sender, EventArgs e)
{
int index = 0;
Task.Run(()=>
{
while (!IsCancel)
{
Debug.WriteLine($"线程运行中,第{index + 1}次执行");
index++;
Thread.Sleep(1000);
}
});
}
//点击按钮,更改变量值,从而实现取消线程
private void button9_Click(object sender, EventArgs e)
{
IsCancel = true;
}
2)使用CancellationTokenSource取消线程
通过专门的类 CancellationTokenSource 来取消线程,上面案例,如果同样使用CancellationTokenSource 取消线程,则代码如下:
//【1】实例化CancellationTokenSource
CancellationTokenSource source = new CancellationTokenSource();
private void button8_Click(object sender, EventArgs e)
{
int index = 0;
Task.Run(()=>
{
//【2】使用IsCancellationRequested属性值,默认为false
while (!source.IsCancellationRequested)
{
Debug.WriteLine($"线程运行中,第{index + 1}次执行");
index++;
Thread.Sleep(1000);
}
});
}
private void button9_Click(object sender, EventArgs e)
{
//【3】使用CancellationTokenSource中的Cancel方法
//该方法会将IsCancellationRequested变为false
source.Cancel();
//【4】使用CancelAfter可以在指定时间后,取消线程,等待过程中不会阻塞主线程
source.CancelAfter(1000);//在1s后取消线程,这1s不会阻塞主线程
}
3)source.Token.Register
由上看来好像区别不太大,但是CancellationTokenSource 的功能不止于此,使用Token属性可以注册一个委托,
这样可以实现,取消后想做点什么事情。代码如下:
//【1】实例化CancellationTokenSource
CancellationTokenSource source = new CancellationTokenSource();
private void button8_Click(object sender, EventArgs e)
{
int index = 0;
//【2】注册一个取消时需要调用的委托
source.Token.Register(()=>
{
Debug.WriteLine("通知:线程已经取消了,我要去干点别的什么了!");
});
Task.Run(()=>
{
//【3】使用IsCancellationRequested属性值,默认为false
while (!source.IsCancellationRequested)
{
Debug.WriteLine($"线程运行中,第{index + 1}次执行");
index++;
Thread.Sleep(1000);
}
},source.Token);//【4】将token 传入Task
}
private void button9_Click(object sender, EventArgs e)
{
source.Cancel();
}
结果如下:
4)CreateLinkedTokenSource组合取消
利用CreateLinkedTokenSource构建CancellationTokenSource的组合体,其中任何一个体取消,则组合体就取消,代码如下:
private void button8_Click(object sender, EventArgs e)
{
TestCancellationTokenSource("1-Cancel");
TestCancellationTokenSource("2-Cancel");
TestCancellationTokenSource("3-Cancel");
}
private void TestCancellationTokenSource(string name)
{
CancellationTokenSource source1 = new CancellationTokenSource();
CancellationTokenSource source2 = new CancellationTokenSource();
CancellationTokenSource source3 = new CancellationTokenSource();
CancellationTokenSource combineSource = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token, source3.Token);
//【2】取消后观察,各CancellationTokenSource的IsCancellationRequested的变化
if (name.Equals("1-Cancel")) source1.Cancel();
else if (name.Equals("2-Cancel")) source2.Cancel();
else source3.Cancel();
//【3】输出各个IsCancellationRequested的值
Debug.WriteLine($"【{name}】source1.IsCancellationRequested={source1.IsCancellationRequested}");
Debug.WriteLine($"【{name}】source2.IsCancellationRequested={source2.IsCancellationRequested}");
Debug.WriteLine($"【{name}】source3.IsCancellationRequested={source3.IsCancellationRequested}");
Debug.WriteLine($"【{name}】combineSource.IsCancellationRequested={combineSource.IsCancellationRequested}");
Debug.WriteLine($"------------------------------------------------------------------------------------------");
}
输出结果如下:
由此可知,无论组合中哪一个状态发生改变,组合的那个CancellationTokenSource状态一定会随之发生改变
5)监控取消
使用Cancel()之后,调用ThrowIfCancellationRequested就会抛异常,不取消,不会抛异常
9.解决中间变量问题
//{
// for (int i = 0; i < 5; i++)
// {
// Debug.WriteLine($"ThreadID={Thread.CurrentThread.ManagedThreadId.ToString("00")}_i={i}");
// }
//}
//【方式1】:Task开启线程的时候,延迟开启,在循环的时候,不会阻塞主线程,
// 循环很快,线程执行业务逻辑的时候,循环已经结束了,i已经变成5
//{
// for (int i = 0; i < 5; i++)
// {
// Task.Run(() =>
// {
// Debug.WriteLine($"ThreadID={Thread.CurrentThread.ManagedThreadId.ToString("00")}_i={i}");
// });
// }
//}
//【方式2】:在每次的循环的时候赋予变量一个值,这里i每次输出的结果还会是5,但是k每个的结果都会不同,
//因为每次运行到Task.Run的时候就会新申请一个线程,线程内k的值就是申请的那一刻i复制给K的值,而申请和启动不是同时进行的
//运行流程: i循环 的过程中,不断复赋值给k,当程序走到Task.Run,,线程获得了k的值,但是还没有完成线程启动,
//等待i已经循环完的时候,程序可能才完成了5个线程的申请并开始启动,因此每个线程获得k值都是不一样的
{
for (int i = 0; i < 5; i++)
{
int k = i;
Task.Run(() =>
{
Debug.WriteLine($"ThreadID:{Thread.CurrentThread.ManagedThreadId}\ti={i}_k={ k}");
});
}
}
10. Task.CompletedTask 和Task.FromResult<T>
当我们有一个返回Task的方法的时候,通常我们会像下面这样去写:
private Task Do()
{
return Task.Run(() =>
{
Console.WriteLine("Task Run");
});
}
但是为了减少了Task.Run调用的系统开销,就有的优化方法,具体如下:
public Task Do1()
{
Console.WriteLine("return Task.CompletedTask");
return Task.CompletedTask;
}
效果同上一样,而且没必要每次都是用Task.Run
对于无返回值的可以使用Task.CompletedTask,但是对于有返回值的Task 如何处理呢?
原始方法:
private Task<string> DoString()
{
return Task<string>.Run(() =>
{
Console.WriteLine("return Task<string>");
return "Task<String>";
});
}
优化方法:
public Task<string> DoString1()
{
Console.WriteLine();
return Task.FromResult<string>("Task<String>");
}
通过使用Task.FromResult<T>方法我们就可以返回有返回值的Task.
五、Parallel
1.Parallel介绍
Parallel类是对线程的抽象,提供数据与任务的并行性。
Parallel.For和Parallel.ForEach方法在每次迭代的时候调用相同的代码,而Parallel.Invoke()方法允许同时调用不同的方法。
Parallel.ForEach()方法用于数据的并行性,Parallel.Invoke()方法用于任务的并行性。
2.Parallel.Invoke()
尽可能并行执行提供的每个操作,需要传入一个委托数组。
public static void Invoke(params Action[] actions)
案例代码:
static void Main(string[] args)
{
Console.WriteLine("----主线程--Start-----");
ParallelTest();
Console.WriteLine("----主线程--End-----");
Console.ReadLine();
}
private static void ParallelTest()
{
var actions = new Action[]
{
new Action (()=>{ActionTest("action1"); }),
()=>{ActionTest("action2"); },//简化
()=>{ ActionTest("action3"); },
};
Console.WriteLine("ParallelTest ---Start");
Parallel.Invoke(actions);//并行执行
Console.WriteLine("ParallelTest ---End");
}
static void ActionTest(string name)
{
Console.WriteLine($"thread:{Thread.CurrentThread.ManagedThreadId}, value:{name}");
}
运行结果:
(1)由上面案例的运行结果可知:
- 使用Parallel.Invoke是会阻塞主线程的,与Task.WaitAll()效果类似,只是默认就拥有这个特性了,那么随之而来就是灵活性的缺失。
- 使用Parallel.Invoke会并行的执行多个委托,在此过程中会创建多个子线程
(2)把Parallel包在一个Task里面实现不卡主线程,如:
Task.Run(()=>
{
Parallel.Invoke(actions);
});
(3)使用Invoke的重载方法,传入ParallelOptions参数,限制开启的线程数量,可以做到不影响线程池的线程数量又能控制当前执行所用的线程数量
public static void Invoke(ParallelOptions parallelOptions, params Action[] actions);
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = 2;//设置最大并发数
Parallel.Invoke(options,actions);
3.Parallel.For()
Parallel.For()方法类似于C#的for循环语句,循环多次执行一个任务,可以并行运行迭代,但迭代的顺序并没指定。主要用于处理针对数组元素的并行操作(数据的并行)
static void Main(string[] args)
{
int[] nums = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
Parallel.For(0, nums.Length, (i) =>
{
Console.WriteLine($"索引:{i},数组元素:{nums[i]},线程ID:{Thread.CurrentThread.ManagedThreadId});
});
Console.ReadKey();
}
4.Parallel.ForEach()
主要用于处理泛型集合元素的并行操作(数据的并行)
public void Test()
{
List<int> nums = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
Parallel.ForEach(nums, (item) =>
{
Console.WriteLine($"输出元素:{item}、线程ID:{Thread.CurrentThread.ManagedThreadId}" );
});
Console.ReadKey();
}
总结
以上就是本文要讲的内容,希望通过以上的介绍,可以让你对线程有更深的理解,在后续使用线程的使用能够更加得心应手。如有不对之处,还请不吝批评指正。
参考:
百度百科
多线程
C# Task和async/await详解
c# Parallel类的使用