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用于一种特殊结构实现的,不涉及模式切换,就是工作在用户方式下,同步速度较快,但是不能跨进程同步。