一周一话题之二(多线程学习总结)
本话题根据《细说多线程》学习整理,有部分内容根据自己理解添加补充。
-->目录导航
(1) 线程的引入:
进程是资源分配的最小单位,程序隔离的边界。
CPU的时间片轮转,在不同的时间段切换执行不同的进程,但是切换进程是比较耗时的;就引来了轻量级进程,也就是所谓的线程,一个进程中包括多个线程(代码流,其实也就是进程中同时跑的多个方法体)
进程与线程的关系
(2) 在进程入口执行的第一个线程被视为这个进程的主线程
(3) 在主线程中开启另一个线程
无参类:ThreadStart 、有参类:ParameterizedThreadStart,传入参数为基类Object类型
(4) 前后台线程
使用Thread.Start()启动的线程默认为前台线程,而系统必须等待所有前台线程运行结束后,应用程序域才会自动卸载。
将新启动的异步线程设置为后台线程,则主线程不必再等待异步线程的执行完毕而自行结束。
(5) Thread.Sleep()可将主线程挂起,但是系统无法预知异步线程的运行时间,用Sleep()来阻塞主线程不科学,这时我们用异步线程实例的Join()方法,就能保证主线程在异步线程运行结束后才结束。
(6) 避免使用Suspend()、Resume()来挂起或恢复线程,因为一旦某个线程占用了已有的资源,再使用Suspend()就会使线程长期处于挂起状态,当在其他线程调用这些资源的时候就会引起死锁
(7)终止线程
终止正在运行的线程使用Thread.Abort()方法,会引发ThreadAbortException异常;
想要恢复终止的线程,可以用catch进行捕获异常并调用Thread.ResetAbort进行取消终止线程
(8)ManualResetEvent
ManualResetEvent 允许线程通过发信号互相通信。 通常,此通信涉及一个线程在其他线程进行之前必须完成的任务。
微软解释:
当一个线程开始一个活动(此活动必须完成后,其他线程才能开始)时,它调用 Reset 以将 ManualResetEvent 置于非终止状态。 此线程可被视为控制 ManualResetEvent。 调用 ManualResetEvent 上的 WaitOne 的线程将阻止,并等待信号。 当控制线程完成活动时,它调用 Set 以发出等待线程可以继续进行的信号。 并释放所有等待线程。
一旦它被终止,ManualResetEvent 将保持终止状态,直到它被手动重置。 即对 WaitOne 的调用将立即返回。
可以通过将布尔值传递给构造函数来控制 ManualResetEvent 的初始状态,如果初始状态处于终止状态,为 true;否则为 false。
ManualResetEvent 也可以同 staticWaitAll 和 WaitAny 方法一起使用。
对于微软的解释我理解起来开始真的很困难,后来通过写Demo才理解了些许,下面是我的理解:ManualResetEvent 的终止状态(终止的是等待状态)指的是使得wait等待的线程释放开来;相反非终止状态(让等待状态生效)就是让使用WaitOne方法的线程开始进入阻塞而等待信号通知的状态。
(1)介绍
CLR线程池并不会在CLR初始化时立刻建立线程,而是在应用程序要创建线程来执行任务时,线程池才初始化一个线程。线程的初始化与其他的线程一样。在完成任务以后,该线程不会自行销毁,而是以挂起的状态返回到线程池。直到应用程序再次向线程池发出请求时,线程池里挂起的线程就会再度激活执行任务。这样既节省了建立线程所造成的性能损耗,也可以让多个任务反复重用同一线程,从而在应用程序生存期内节约大量开销。
-->线程池建立的线程默认为后台线程,且线程优先级为Normal。
-->获得正在线程池中正在投入使用的线程有多少:ThreadPool.GetAvailableThreads( out int workerThreads,out int completionPortThreads )
(2)两种线程
工作者线程(workerThreads):管理CLR内部对象的运作;I/O线程(completionPortThreads) :用于与外部系统交换信息
1. QueueUserWorkItem启动
WaitCallback委托指向一个带有Object参数的无返回值方法,ThreadPool中两个静态方法可直接启动工作者线程: ThreadPool.QueueUserWorkItem(WaitCallback)、ThreadPool.QueueUserWorkItem(WaitCallback,Object)
通过QueueUserWorkItem启动线程虽然方便,但是要求WaitCallback必须指向带有Object参数的无返回值方法,限制大,不够灵活。更加灵活的方式就是采用委托的方式。
(1) 委托类的重要方法
Void Invoke():当调用时,对应次委托内的所有方法都会被执行。
System.IAsyncResult BeginInvoke(System.AsyncCallback, System.Object):由此启动的线程都属于CLR线程池中的工作者线程
Void EndInvoke(System.IAsyncResult)
(2) BeginInvoke和EndInvoke实现异步线程
建立一个委托对象,通过IAsyncResult BeginInvoke(string name,AsyncCallback callback,object state) 异步调用委托方法,BeginInvoke 方法除最后的两个参数外,其它参数都是与方法参数相对应的。通过 BeginInvoke 方法将返回一个实现了 System.IAsyncResult 接口的对象,之后就可以利用EndInvoke(IAsyncResult ) 方法就可以结束异步操作,获取委托的运行结果
class Program { delegate string MyDelegate(string name); static void Main(string[] args) { ThreadMessage("Main Thread"); //建立委托 MyDelegate myDelegate = new MyDelegate(Hello); //异步调用委托,获取计算结果 IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null); //完成主线程其他工作 ............. //等待异步方法完成,调用EndInvoke(IAsyncResult)获取运行结果 string data=myDelegate.EndInvoke(result); Console.WriteLine(data); Console.ReadKey(); } static string Hello(string name) { ThreadMessage("Async Thread"); Thread.Sleep(2000); //虚拟异步工作 return "Hello " + name; } //显示当前线程 static void ThreadMessage(string data) { string message = string.Format("{0}\n ThreadId is:{1}", data,Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } }
(1) 上诉方式来实现异步线程,会导致主线程阻塞,只有异步线程执行完之后,主线程才能继续做操作,这样做显然不科学。
先研究一下IAsyncResult接口的成员:
object AsyncState {get;} //获取用户定义的对象,它限定或包含关于异步操作的信息
WailHandle AsyncWaitHandle {get;} //获取用于等待异步操作完成的 WaitHandle
bool CompletedSynchronously {get;} //获取异步操作是否同步完成的指示
bool IsCompleted {get;} //获取异步操作是否已完成的指示
(2) 利用IsCompleted属性判断异步操作是否执行完成,以轮询方式检测异步线程执行情况,可实现主线程在异步线程执行结束前进行一些操作
(3) 利用AsyncWaitHandle 的WaitOne(int timeout )方法可以实现同IsCompleted同样的操作,但是监视多个对象时就得用
WaitHandle的两个静态方法
WaitAny(waitHandle[], int):等待所有waitHandle完成后再返回一个bool值
WaitAll (waitHandle[] , int):等待其中一个waitHandle完成后就返回一个int,这个int是代表已完成waitHandle在waitHandle[]中的数组索引。
.NET为 IAsyncResult BeginInvoke(AsyncCallback , object)准备了一个回调函数。使用 AsyncCallback 就可以绑定一个方法作为回调函数,回调函数必须是带参数 IAsyncResult 且无返回值的方法: void AsycnCallbackMethod(IAsyncResult result) 。在BeginInvoke方法完成后,系统就会调用AsyncCallback所绑定的回调函数,最后回调函数中调用 XXX EndInvoke(IAsyncResult result) 就可以结束异步方法,它的返回值类型与委托的返回值一致。
如果想为回调函数传入参数,只需给object传值即可,注意IAsyncResult的的AsyncState
static void Completed(IAsyncResult result) { ThreadMessage("Async Completed"); //获取委托对象,调用EndInvoke方法获取运行结果 AsyncResult _result = (AsyncResult)result; MyDelegate myDelegate = (MyDelegate)_result.AsyncDelegate; string data = myDelegate.EndInvoke(_result); //获取Person对象 Person person = (Person)result.AsyncState; string message = person.Name + "'s age is " + person.Age.ToString(); Console.WriteLine(data+"\n"+message); }
I/O线程时.Net专为访问外部资源设置的一种线程,由于访问外部资源会受到外界各种因素影响,为防止让主线程长期处于阻塞状态,.Net为多个I/O操作都建立起了异步方法,例如:FileStream、TCP/IP、WebRequest、WebService等等,而且每个异步方法的使用方式都非常类似,都是以BeginXXX为开始,以EndXXX结束
(1)要调用I/O线程进行异步调用操作,必须使用以下构造函数,并且useAsync设置为true
FileStream stream = new FileStream ( string path, FileMode mode, FileAccess access, FileShare share, int bufferSize,bool useAsync ) ;
注:使用BeginRead和BeginWrite方法进行大量读写操作时效果较好,在小量读写时用异步线程时线程切换的时间比同步还慢。
(2)异步写入
public override IAsyncResult BeginWrite ( byte[] array, int offset, int numBytes, AsyncCallback userCallback, Object stateObject )
public override void EndWrite (IAsyncResult asyncResult )
方式与委托的BeginInvoke相似,最好使用回调函数,避免线程阻塞。AsyncCallback所绑定的回调函数必须是带单个 IAsyncResult 参数的无返回值方法。
(3)异步读取
public override IAsyncResult BeginRead ( byte[] array,int offset,int numBytes, AsyncCallback userCallback,Object stateObject)
public override int EndRead(IAsyncResult asyncResult)
(1) Socket异步
Scocket提供了BeginAccept异步接受请求、BeginSend异步发送数据、BeginReceive异步接收数据;回调函数的使用方式均类似。
(2) TcpListener与TcpClient
.NET把Socket的大部分操作都放在System.Net.TcpListener和System.Net.Sockets.TcpClient里面,这两个类大大地简化了Socket的操作
NetworkStream 提供了好几个方法控制套接字数据的发送与接收, 其中BeginRead、EndRead、BeginWrite、EndWrite 能够实现异步操作,而且异步线程是来自于CLR线程池的I/O线程。
public override IAsyncResult BeginRead (byte [] buffer, int offset, int size, AsyncCallback callback, Object state )
public override int EndRead(IAsyncResult result)
public override IAsyncResult BeginWrite (byte [] buffer, int offset, int size, AsyncCallback callback, Object state )
public override void EndWrite(IAsyncResult result)
System.Net.WebRequest 是 .NET 为实现访问 Internet 的 “请求/响应模型” 而开发的一个 abstract 基类, 它主要有三个子类:FtpWebRequest、HttpWebRequest、FileWebRequest。当使用WebRequest.Create(string uri)创建对象时,应用程序就可以根据请求协议判断实现类来进行操作。
FileWebRequest、FtpWebRequest、HttpWebRequest 各有其作用:
FileWebRequest 使用 “file://路径” 的URI方式实现对本地资源和内部文件的请求/响应
FtpWebRequest 使用FTP文件传输协议实现文件请求/响应
HttpWebRequest 用于处理HTTP的页面请求/响应。
处理请求/相应的常用方法:
public override Stream GetRequestStream ()
public override WebResponse GetResponse ()
-->用于异步向HttpWebRequest对象写入请求信息
public override IAsyncResult BeginGetRequestStream ( AsyncCallback callback, Object state )
public override Stream EndGetRequestStream ( IAsyncResult asyncResult )
-->用于异步发送页面请求并获取返回信息
public override IAsyncResult BeginGetResponse ( AsyncCallback callback, Object state )
public override WebResponse EndGetResponse ( IAsyncResult asyncResult )
注:① 请求与响应不能使用同步与异步混合开发模式,即当请求写入使用GetRequestStream同步模式,即使响应使用BeginGetResponse异步方法,操作也与GetRequestStream方法在于同一线程内。
② HttpWebRequire.Method默认为get,在写入请求前必须把HttpWebRequire.Method设置为post,否则在使用BeginGetRequireStream 获取请求数据流的时候,系统就会发出 “无法发送具有此谓词类型的内容正文" 的异常。
客户端在引用WCF服务时,选择 “生成异步操作”。然后使用 BeginMethod 启动异步方法, 在回调函数中调用EndMethod结束异步调用。
它使主线程不需要等待数据库的返回结果,在使用复杂性查询或批量插入时将有效提高主线程的效率。
public IAsyncResult BeginExecuteNonQuery (......)
public int EndExecuteNonQuery(IAsyncResult)
public IAsyncResult BeginExecuteReader(......)
public SqlDataReader EndExecuteReader(IAsyncResult)
public IAsyncResult BeginExecuteXmlReader (......)
public XmlReader EndExecuteXmlReader(IAsyncResult)
注:SqlCommand异步操作的特别之处在于线程并不依赖于CLR线程池,而是由Windows内部提供,这比使用异步委托更有效率。但如果需要使用回调函数的时候,回调函数的线程依然是来自于CLR线程池的工作者线程。
使用多线程开发时,存在一定的共用数据,为了避免多线程同时操作同一数据,可以为线程进行加锁。
加锁的原理:
每一个引用类型的对象都有一个同步索引块,指示当前使用该对象的线程数,每个线程执行到Lock语句块的时候就会判断当前锁定项(这里是this,当前窗体对象)的同步索引块是否等于0(即没有线程在访问该变量),如果等于0则进入执行块,首先将同步索引块的索引加1,表示当前多了一个线程使用this,等lock块执行完成再将同步索引块中的索引值减1,使得其它线程能够继续访问,这样就相当于实现了一个排队机制,使得在适当的时候该串行执行的代码串行执行
需要锁定对象:lock(this)
需要锁定代码段:Object obj = new Object(); lock(obj){…}
lock的语法糖,可以对对象进行锁定和解锁,比lock使用更加灵活
class Control { private object obj=new object(); public void Method() { Monitor.Enter(obj); try {......} catch(Excetion ex) {......} finally { Monitor.Exit(obj); } } }