一、多线程
1、线程概念
线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
概要:
1、线程是轻量级的进程
2、线程没有独立的地址空间(内存空间)
3、线程是由进程创建的(寄生在进程)
4、一个进程可以拥有多个线程-->这就是我们常说的多线程编程
5、线程有几种状态:
a、新建状态(new)
b、就绪状态(Runnable)
c、运行状态(Running)
d、阻塞状态(Blocked)
e、死亡状态(Dead)
当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。或通过线程锁来进行线程同步。
实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
2、异步编程
在很多时候,我们在进程中使用单一线程从头到尾地执行程序,这种简单模式会导致性能和用户体验另人难以接受。
如果程序调用某个方法,等待其执行全部处理后才能继续执行,我们称其为同步的。
我们在编程语言的流程中添加了异步控制的部分,这部分的编程可以称之为异步编程。
3、学习网址
一个总结帖,也是这篇整理的大部分出处:
https://www.cnblogs.com/wyt007/p/9486752.html
4、线程锁lock
1)说说lock到底锁谁?
https://blog.csdn.net/weixin_34344403/article/details/85686895
2)死锁
https://www.cnblogs.com/liuqiyun/p/9118382.html
3)分解
实际上lock关键字是Monitor类用例的一个语法糖。如果我们分解使用了lock关键字的代码,将会看到它如下面代码片段所示:
bool acquiredLock = false;
try
{
Monitor.Enter(lockObject, ref acquiredLock);
}
finally
{
if (acquiredLock)
{
Monitor.Exit(lockObject);
}
}
我们可以直接使用Monitor类。其拥有TryEnter方法,该方法接受一个超时参数。
如果在我们能够获取被lock保护的资源之前,超时参数过期,则该方法会返回 false.
5、线程间传递参数
1)跨线程控制控件:
https://blog.csdn.net/xz_life/article/details/8446129
2)主线程和子线程如何实现互相传递数据(三种方式):
https://www.cnblogs.com/eve612/p/14342087.html
https://www.cnblogs.com/stemon/p/4214906.html
https://blog.csdn.net/shuaihj/article/details/41316731
一般使用lambda表达式。
这可能会导致几个问题。例如,如果在多个lambda表达式中使用相同的变量,它们会共享该变量值。
如下代码:
int i = 10;
var t1 = new Thread(() => PrintNumber(i));
i = 20;
var t2 = new Thread(() => PrintNumber(i));
t1.Start();
t2.Start();
两个线程都会打印出20,因为t1在线程启动之前变量已经被修改为20了。
多线程与委托密不可分,详细委托相关知识见本文章末尾。
6、线程越多越好吗?
线程不是越多越好,还得看内存能不能带动(抢占资源)。
并且线程创建和销毁都要消耗很多时间的,如果创建消耗的时候大于执行任务的时候,就没有必要多线程了。
同时cpu频繁在各个线程之间切换影响性能(开销大)。
7、线程池
当我们需要频繁的创建线程时,我们要考虑到通过线程池统一管理线程资源,避免不可控风险以及额外的开销。
归纳起来说,线程池的作用包括:
实现任务线程队列缓存策略和拒绝机制
实现某些与实践相关的功能,如定时执行,周期执行等(比如列车指定时间运行)
隔离线程环境,比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大。因此,通过配置独立的线程池,将较慢的交易服务与搜索服务个离开,避免个服务线程互相影响
利用线程池管理并服用线程,控制最大并发数(手动创建线程很难得到保证)
8、前台线程和后台线程
当主程序启动时定义了两个不同的线程。通过手动设置IsBackground属性为ture来创建后台线程。
通过配置来实现前台线程会比后台线程先完成。然后运行程序。前台线程完成后,程序结束并且后台线程被终结。
这是前台线程与后台线程的主要区别:
进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。
一个重要注意事项是如果程序定义了一个不会完成的前台线程,主程序并不会正常结束。
9、终止线程
1)调用Abort方法终止线程的原理:给线程注入ThreadAbortException方法,导致线程被终结。
隐患:
1、该异常可以在任何时刻发生并可能彻底摧毁应用程序。
2、使用该技术也不一定总能终止线程。目标线程可以通过处理该异常并调用Thread.ResetAbort方法来拒绝被终止。
因此并不推荐使用Abort方法来关闭线程。可优先使用一些其他方法。
2)中止线程的方法
(1)判断标志位。
https://blog.csdn.net/zhu2695/article/details/53464757
(2)提供一个CancellationToken方法来取消线程的执行。这个我没有试过,马一下。
https://www.tnblog.net/aojiancc2/article/details/58
https://blog.csdn.net/yangwohenmai1/article/details/90404497?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-7&spm=1001.2101.3001.4242
10、处理异常
(1)如果在线程外添加异常捕获代码:
try
{
t = new Thread(BadFaultyThread);
t.Start();
}
catch (Exception ex)
{
Console.WriteLine("We won't get here!");
}
则异常不会被包裹启动线程的try/catch代码块捕获到。
所以如果直接使用线程,一般来说不要在线程中抛出异常,而是在线程代码(BadFaultyThread)中使用try/catch代码块。
(2)跨线程调用控件时关闭窗体时如何避免出现异常:Cannot access a disposed object
https://www.cnblogs.com/beilinyu/p/3569123.html
二、委托和事件
1、委托(Delegate)
(1)概述
委托是一个类,它使得可以将方法当作另一个方法的参数来进行传递
将方法动态地赋给参数,可以避免在程序中大量使用 if else (switch)语句,同时使得程序具有更好的可扩展性。
委托的三种调用示例(同步调用、异步调用、异步回调):
https://www.cnblogs.com/linybo/p/10126686.html
○ 触发委托有2种方式: 委托实例.Invoke(参数列表),委托实例(参数列表);
回调函数简述:https://blog.csdn.net/sajiazaici/article/details/78702144
最主要的两个作用:封装、异步
(2)delegate和Invoke的区别:
delegate是委托,本身不能解决跨线程访问控件的问题,直接调用委托还是会报错。
Invoke指定用主线程中的控件去调用这个委托,相当于主线程来执行这个函数。
2、事件
(1)概念
事件基于委托模型。
委托模型遵循观察者设计模式,使订阅者能够向提供方注册并接收相关通知。
事件是用委托实现的,是对委托的额外封装,其本质上是一种特殊的委托。
事件分为两个阶段:
委托的实例化(对应事件订阅)
委托的调用(对应事件触发)
(2)原理
1)引发事件
事件是由对象发送的用于发出操作信号的消息。
该操作可能是由用户交互引起,例如单击按钮;
也可能是由某些其他程序的逻辑导致,例如更改属性值。
引发事件的对象称为“事件发送方”。
事件发送方推送事件发生的通知。事件发送方不知道哪个对象或方法将接收(处理)它引发的事件。
事件接收器接收该通知并定义对它的响应。
事件通常是事件发送方的成员。
例如 Click 事件是 Button 类的成员;
PropertyChanged 事件是实现 INotifyPropertyChanged 接口的类的成员。
为了引发事件,可以在 C# 中添加一个标记为 protected 和 virtual 的方法。
将此方法命名为 OnEventName;例如,OnDataReceived。
此方法应接受一个指定事件数据对象(EventArgs 类型或派生类型)的参数。
您提供此方法以允许派生类重写引发事件的逻辑。
1 public event EventHandler ThresholdReached;//声明名为 ThresholdReached 事件 2 3 protected virtual void OnThresholdReached(EventArgs e) 4 { 5 EventHandler handler = ThresholdReached; 6 handler?.Invoke(this, e);//该事件与 EventHandler 委托相关联并且由 OnThresholdReached 方法引发。 7 }
在事件上下文中,委托是事件源和处理事件的代码之间的媒介(或类似指针的机制)。
.NET提供了 EventHandler 和 EventHandler<TEventArgs> 委托来支持大部分事件场景。
使用 EventHandler 委托处理不包含事件数据的所有事件。
使用 EventHandler<TEventArgs> 委托处理包含事件相关数据的事件。
这些委托没有返回类型值,并且接受两个参数(事件源的对象和事件数据的对象)。
在 EventHandler 和 EventHandler<TEventArgs> 委托不可用的场景下,可以定义一个委托。
//声明 ThresholdReachedEventHandler 委托 public delegate void ThresholdReachedEventHandler(object sender, ThresholdReachedEventArgs e);
与事件相关的数据可以通过事件数据类提供。
.NET 遵循所有事件数据类以 EventArgs 结尾的命名模式。
例如,SerialDataReceivedEventArgs 类是 SerialPort.DataReceived 事件的事件数据类。
通过查看事件的委托来确定哪个事件数据类与事件相关联。
例如,SerialDataReceivedEventHandler 委托包含 SerialDataReceivedEventArgs 类作为它的一个参数。
2)响应事件
若要响应事件,需要在事件接收器中定义一个事件处理程序方法。
此方法必须与您正处理的事件的委托签名匹配。
static void Main() { var c = new Counter(); c.ThresholdReached += c_ThresholdReached;//事件处理程序方法 } static void c_ThresholdReached(object sender, EventArgs e) { Console.WriteLine("The threshold was reached."); }
出处:
https://docs.microsoft.com/zh-cn/dotnet/standard/events/
3、委托和事件的联系和区别
○ 委托就是一个类,也可以实例化,通过委托的构造函数来把方法赋值给委托实例。事件不需要实例化;
○ 事件可以看作是一个委托类型的变量;
○ 通过+=为事件注册多个委托实例或多个方法;通过-=为事件注销多个委托实例或多个方法;
○ EventHandler就是一个委托。
https://www.cnblogs.com/darrenji/p/3967381.html
简述委托与事件的区别:
https://blog.csdn.net/lrfleroy/article/details/88780590
4、回调函数
https://blog.csdn.net/weixin_33994429/article/details/86069163