一周一话题之二(多线程学习总结)

本话题根据《细说多线程》学习整理,有部分内容根据自己理解添加补充。

一、多线程

1.多线程基础

(1) 线程的引入:

进程是资源分配的最小单位,程序隔离的边界。

CPU的时间片轮转,在不同的时间段切换执行不同的进程,但是切换进程是比较耗时的;就引来了轻量级进程,也就是所谓的线程,一个进程中包括多个线程(代码流,其实也就是进程中同时跑的多个方法体)

image

进程与线程的关系

image

(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方法的线程开始进入阻塞而等待信号通知的状态。

2.线程池

(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)

2. 委托启动

通过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}
  ThreadId is:{1}",
                   data,Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine(message);
        }
    }

3. IAsyncResult提高主线程性能

(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[]中的数组索引。

4. 回调函数

.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+"
"+message);
        }

三、I/O线程

I/O线程时.Net专为访问外部资源设置的一种线程,由于访问外部资源会受到外界各种因素影响,为防止让主线程长期处于阻塞状态,.Net为多个I/O操作都建立起了异步方法,例如:FileStream、TCP/IP、WebRequest、WebService等等,而且每个异步方法的使用方式都非常类似,都是以BeginXXX为开始,以EndXXX结束

1. FileStream

(1)要调用I/O线程进行异步调用操作,必须使用以下构造函数,并且useAsync设置为true

FileStream stream = new FileStream ( string path, FileMode mode, FileAccess access, FileShare share, int bufferSize,bool useAsync ) ;

注:使用BeginReadBeginWrite方法进行大量读写操作时效果较好,在小量读写时用异步线程时线程切换的时间比同步还慢。

(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)

2.TCP/IP异步通信

(1) Socket异步

Scocket提供了BeginAccept异步接受请求、BeginSend异步发送数据、BeginReceive异步接收数据;回调函数的使用方式均类似。

(2) TcpListenerTcpClient

.NET把Socket的大部分操作都放在System.Net.TcpListener和System.Net.Sockets.TcpClient里面,这两个类大大地简化了Socket的操作

NetworkStream 提供了好几个方法控制套接字数据的发送与接收, 其中BeginReadEndReadBeginWriteEndWrite 能够实现异步操作,而且异步线程是来自于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)

3.异步WebRequest

System.Net.WebRequest 是 .NET 为实现访问 Internet 的 “请求/响应模型” 而开发的一个 abstract 基类, 它主要有三个子类:FtpWebRequestHttpWebRequestFileWebRequest。当使用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 获取请求数据流的时候,系统就会发出 “无法发送具有此谓词类型的内容正文" 的异常。

4. 异步调用Web服务

客户端在引用WCF服务时,选择 “生成异步操作”。然后使用 BeginMethod 启动异步方法, 在回调函数中调用EndMethod结束异步调用。

5.SqlCommond异步操作

它使主线程不需要等待数据库的返回结果,在使用复杂性查询或批量插入时将有效提高主线程的效率。

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,使得其它线程能够继续访问,这样就相当于实现了一个排队机制,使得在适当的时候该串行执行的代码串行执行

1. lock

需要锁定对象:lock(this)

需要锁定代码段:Object obj = new Object(); lock(obj){…}

2. Montior

lock的语法糖,可以对对象进行锁定和解锁,比lock使用更加灵活

class Control
{
      private object obj=new object();
 
      public void Method()
      {
            Monitor.Enter(obj);
            try
            {......}
            catch(Excetion ex)
            {......}
            finally
            {
                Monitor.Exit(obj);
            }
      }
}

 《练习源码下载》

原文地址:https://www.cnblogs.com/SpringDays/p/3517943.html