zoukankan      html  css  js  c++  java
  • C# 多线程 详解

    【基础篇】

    • 怎样创建一个线程
    • 受托管的线程与Windows线程
    • 前台线程与后台线程
    • 名为BeginXXX和EndXXX的方法是做什么用的
    • 异步和多线程有什么关联

    【WinForm多线程编程篇】

    • 多线程WinForm程序总是抛出InvalidOperationException,怎么解决
    • Invoke和BeginInvoke干什么用的,内部是怎么实现的
    • 每个线程都有消息队列吗
    • 为什么WinForm不允许跨线程修改UI线程控件的值
    • 有没有什么办法可以简化WinForm多线程的开发

    【线程池】

    • 线程池的作用是什么
    • 所有进程使用一个共享的线程池,还是每个进程使用独立的线程池
    • 线程池中线程的分类
    • .NET线程池有什么不足

    【同步】

    • CLR怎样实现lock(obj)锁定
    • 互斥对象(Mutex)、事件(Event)对象与lock语句的比较

    基础篇

    怎样创建一个线程

    方法一:使用Thread类

       public static void Main(string[] args)
            {
                //方法一:使用Thread类
                ThreadStart threadStart = new ThreadStart(Calculate);//通过ThreadStart委托告诉子线程执行什么方法                        Thread thread = new Thread(threadStart);
                thread.Start();//启动新线程
            }

            public static void Calculate()
            {
                Console.Write("执行成功");
                Console.ReadKey();
            }

    方法二:使用Delegate.BeginInvoke

       delegate double CalculateMethod(double r);//声明一个委托,表明需要在子线程上执行的方法的函数签名
            static CalculateMethod calcMethod = new CalculateMethod(Calculate);

       static void Main(string[] args)
            {
                //方法二:使用Delegate.BeginInvoke
                //此处开始异步执行,并且可以给出一个回调函数(如果不需要执行什么后续操作也可以不使用回调)
                calcMethod.BeginInvoke(5, new AsyncCallback(TaskFinished), null);
                Console.ReadLine();
            }

       public static double Calculate(double r)
            {
                return 2 * r * Math.PI;
            }
            //线程完成之后回调的函数
            public static void TaskFinished(IAsyncResult result)
            {
                double re = 0;
                re = calcMethod.EndInvoke(result);
                Console.WriteLine(re);
            }

    方法三:使用ThreadPool.QueueworkItem

    受托管的线程与Windows线程

      .NET应用的线程实际上仍然是Windows线程。但是,当某个线程被CLR所知时,我们将它称为受托管的线程。具体来说,由受托管的代码创建出来的线程就是受托管的线程。不过,一旦该线程执行了受托管的代码它就变成了受托管的线程。

      一个受托管的线程和非受托管的线程的区别在于,CLR将创建一个System.Threading.Thread类的实例来代表并操作前者。在内部实现中,CLR将一个包含了所有受托管线程的列表保存在一个叫做ThreadStore地方。

      CLR确保每一个受托管的线程在任意时刻都在一个AppDomain中执行,但是这并不代表一个线程将永远处在一个AppDomain中,它可以随着时间的推移转到其他的AppDomain中。

    前台线程与后台线程

      启动了多个线程的程序在关闭的时候却出现了问题,如果程序退出的时候不关闭线程,那么线程就会一直的存在,但是大多启动的线程都是局部变量,不能一一的关闭,如果调用Thread.CurrentThread.Abort()方法关闭主线程的话,就会出现ThreadAbortException异常。可以通过这个方法:Thread.IsBackground设置线程为后台线程。

      msdn对前台线程和后台线程的解释:托管线程或者是后台线程,或者是前台线程。后台线程不会是托管执行环境处于活动状态,除此之外,后台线程与前台线程是一样的。一旦所有前台进程在托管进程(其中.exe文件时托管程序集)中被停止,系统将停止所有后台线程并关闭。通过设置Thread.IsBackground属性,可以将一个线程指定为后台线程或者前台线程。从非托管代码进入托管执行环境的所有线程都被标记为后台线程。通过创建并启动新的Thread对象而生成的所有线程都是前台线程。

    名为BeginXXX和EndXXX的方法是做什么用的

      这是.net的一个异步方法名称规范。

      .net在设计的时候为异步编程设计了一个异步编程模型(APM),比如所有的Stream就是BeginRead,EndRead,Socket,WebRequet,SqlCommand都运用到了这个模式,一般来讲,调用BeginXXX的时候,一般会启动一个异步过程去执行一个操作,EndInvoke可以接受这个异步操作的返回,当然如果异步操作在EndIncoke调用的时候还没有执行完成,EndInvoke会一直等待异步操作完成或者超时。

      .NET的异步编程模型(APM)一般包含BeginXXX,EndXXX,IAsyncResult这三个元素,BeginXXX方法都有返回一个IAsyncResult,而EndXXX都需要接受一个IAsyncResult作为参数。

    异步和多线程

      异步有许多种方法,我们可以用进程来做异步,或者使用线程,或者硬件的一些特性,比如在实现异步IO的时候,可以以下两种方案:

      方案一:可以通过初始化一个子线程,然后在子线程里进行IO,而让主线程顺利往下执行,当子线程执行完毕就回调

      方案二:使用硬件的支持(现在许多硬件都有自己的处理器),来实现完全的异步,这时我们只需将IO请求告知硬件驱动程序,然后迅速返回,然后等着硬件IO就绪通知我们就可以了

    WinForm多线程编程篇 

    多线程WinForm程序总是抛出InvalidOperationException,怎么解决

      在WinForm中使用线程时,常常遇到一个问题,当在子线程(非UI线程)中修改一个空间的值:比如修改进度条进度,时会抛出异常。

      解决方法就是利用控件提供的Invoke和BeginInvoke把调用封送回UI线程,也就是让控件属性修改在UI线程上执行。

      例如:

       delegate void changeText(double result);
            public Form1()
            {
                InitializeComponent();
                ThreadStart threadStart = new ThreadStart(Calculate);
                Thread thread = new Thread(threadStart);
                thread.Start();
            }

            public void Calculate()
            {
                double r = 2;
                double result = 2 * Math.PI * r;
                CalcFinished(result);
            }
            public void CalcFinished(double result)
            {
                if (this.InvokeRequired)
                {
                    this.BeginInvoke(new changeText(CalcFinished), result);
                }
                else
                {
                    this.textBox1.Text = result.ToString();
                }
            }

      这里用到了Control的一个属性InvokeRequired(这个属性石可以在其它线程里访问),这个属性表明调用是否来自非UI线程,如果是,使用BeginInvoke来调用这个函数,否则就直接调用,省去线程封送的过程。

    Invoke和BeginInvoke干什么用的,内部是怎么实现的

      这两个方法主要是让给出的方法在控件创建的线程上执行。

      Invoke使用了Win32API的SendMessage     BeginInvoke使用了Win32API的PostMessage

      这两个方法想UI线程的消息队列中放入一个消息,当UI线程处理这个消息时,就会在自己的上下文中执行传入的方法,换句话说,凡是使用BeginInvoke和Invoke调用的线程都是在UI主线程中执行,所以如果这些方法里涉及一些静态变量,不用考虑加锁的问题。

    每个线程都有消息队列吗?

      不是,知识创建了窗体对象的线程才会有消息队列(下面是《Windows核心编程》关于这一段的描述)

      当一个线程第一希被建立时,系统假定线程不会被用于任何与用户相关的任务。这样可以减少线程对系统资源的要求。但是,一旦这个线程调用一个与图形用户界面有关的函数(例如检查它的消息队列或建立一个窗口),系统就会为该线程分配一些另外的资源,以便它能够执行与用户界面有关的任务。特别是,系统分配一个THREADINFO结构,并将这个数据结构与线程联系起来。

      这个THREADINFO结构包含一组成员变量,利用这组成员,线程可以认为它是在自己独占的环境中运行。THREADINFO是一个内部的、未公开的数据结构,用来指定线程的登记消息队列(posted-message queue)、发送消息队列(send-message queue)、应答消息队列(reply-message queue)、虚拟输入队列(virtualized-input queue)、唤醒标志(wake flag)以及用来描述线程局部输入状态的若干变量。

    为什么WinForm不允许跨线程修改UI线程控件的值

      vs2005及以上版本,当在Visual Studio调试器中运行代码时,如果您从一个线程访问某个UI元素,而该线程不是创建该UI元素时所在的线程,则会引发InvalidOperationException调试器引发该异常以警告您存在危险的编程操作。UI元素不是线程安全的,所以只应在创建它们的线程上进行访问。

    有没有什么办法可以简化WinForm多线程的开发

      使用backgroundworker,使用这个组件可以避免回调时的Invoke和BeginInvoke,并且提供了许多丰富的方法和事件

    线程池

    线程池的作用是什么

      减小线程创建和销毁的开销

       创建线程涉及到用户模式和内核模式的切换,内存分配,dll通知等一系列过程,线程销毁的步骤也是开销很大的,所以如果应用程序使用完一个线程,我们能把线程暂时存放起来,以备下次使用,就可以减小这些开销。

     所有进程使用一个共享的线程池,还是每个进程使用独立的线程池

      每个进程都有一个线程池,一个进程中只能有一个实例,它在各个应用程序域(AppDomain)是共享的,线程池仅仅保留相当少的线程,保留的线程可以用SetMinThread这个方法设置,当程序需要一个线程时,线程池中没有空闲的线程时,线程池就会负责创建这个线程,调用完后,不会立即销毁,而是把它放在池子里,以备下次使用,但是,如果超出一定时间没使用,线程池就会回收线程,所以线程池里存在的线程数实际是个动态的过程。

    线程池中线程的分类

      线程池里的线程按照公用被分成了两大类:工作线程和IO线程(IO完成线程),前者用于执行普通操作,后者专用于异步IO。它们分别在什么情况下被使用,二者工作原理有什么不同?通过下面这个例子,我们用一个流读出一个很大的文件(文件大,操作时间长,便于观察),然后用另一个输出流把所读出的文件的一部分写到磁盘上。

      用两种方法创建输出流,分别是:

      创建一个异步的流(注意构造函数最后那个true)

      创建一个同步流

       string readPath = "d:\工作常用软件\VS2012Documentation.iso";
            string writePath = "d:\vs2012.ios";
            byte[] buffer = new byte[90000000];
            //创建一个异步流
            FileStream outputfs = new FileStream(writePath, FileMode.Create, FileAccess.Write, FileShare.None, 256, true);
            Console.WriteLine("异步流");
            //创建一个同步流
            //FileStream outputfs = File.OpenWrite(writePath);
            //Console.WriteLine("同步流");
            //然后在写文件期间查看线程池的状况
            ShowThreadDetail("初始状态");
            FileStream fs = File.OpenRead(readPath);
            fs.BeginRead(buffer, 0, 90000000, delegate(IAsyncResult o)
            {
                outputfs.BeginWrite(buffer, 0, buffer.Length, delegate(IAsyncResult o1)
                {
                    Thread.Sleep(1000);
                    ShowThreadDetail("BeginWrite的回调线程");
                }, null);
                Thread.Sleep(500);
            },
            null);
            Console.ReadLine();

       public static void ShowThreadDetail(string caller)
            {
                int IO;
                int Worker;
                ThreadPool.GetAvailableThreads(out Worker, out IO);
                Console.WriteLine("Worker:{0};IO:{1}", Worker, IO);
            }

      输出结果

      异步流

      Worker:1023;    IO:1000

      Worker:1023;    IO:999

      同步流

      Worker:1023;    IO:1000

      Worker:1022;    IO:1000

      这两个构造函数创建的流都可以使用BeginWrite来异步写数据,但二者行为不同,当使用同步的流进行异步写时,通过回调的输出我们可以看到,它使用的是工作线程,而非IO线程,而异步流使用IO线程。

    .NET线程池有什么不足

       没有提供方法控制加入线程池的线程:一旦加入线程池,我们没办法挂起,终止这些线程,唯一可以做的就是等他自己执行

    • 不能为线程设置优先级
    • 所支持的Callback不能有返回值。WaitCallback只能带一个object类型的参数
    • 不适合用于长期执行某任务的场合

    同步

    CLR怎样实现lock(obj)锁定

       从原理上讲,lock和Syncronized Attribute都是用Moniter.Enter实现的,例如:

      object obj = new object();

      lock(obj){

      //do things...

      }

      在编译时,会被编译为类似

      try{

        Moniter.Enter(obj){

        //do things...

        }

      }

      catch{...}

      finally{

        Moniter.Exit(obj);

      }

      每个对象实例头部都有一个指针,这个指针指向的结构包含了对象的锁定信息,当第一次使用Moniter.Enter(obj)是,这个obj对象的锁定结构就会被初始化,第二次调用时,会检验这个object的锁定结构,如果锁没有被释放,则调用会阻塞。

    互斥对象(Mutex)、事件(Event)对象与lock语句的比较

      这里所谓的事件是一种用于同步的内核机制,互斥对象和事件对象属于内核对象,利用内核对象进行线程同步,线程必须要在用户模式和内核模式间切换,所有一般效率很低,但利用互斥对象和事件对象这样的内核对象,可以在多个线程中的各个线程间进行同步。

      lock或者Moniter是.net用于一种特殊结构实现的,不涉及模式切换,就是工作在用户方式下,同步速度较快,但是不能跨进程同步。

  • 相关阅读:
    类与类之间的几种关系
    spring之BeanFactory
    java打开本地应用程序
    java笔记六:线程间的协调
    继承与组合的优缺点
    适配器模式之对象适配器
    java笔记三:List接口
    java笔记二:final关键字用法总结
    设计模式之命令模式
    利用栈实现迷宫的求解
  • 原文地址:https://www.cnblogs.com/wxfsoft/p/3453952.html
Copyright © 2011-2022 走看看