【基础篇】
- 怎样创建一个线程
- 受托管的线程与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用于一种特殊结构实现的,不涉及模式切换,就是工作在用户方式下,同步速度较快,但是不能跨进程同步。