下面记录几种异步编程的方式
Thread thread = new Thread ( new ThreadStart(callback));
Thread thread = new Thread ( new ParameterizedThreadStart(callback))); //带一个参数
thread.start();
thread.start(object parameter);
后台线程当主线程自己执行完后,不会等待子线程结束再退出。除非手动指定join。
thread.IsBackground = true; // 设置为后台线程
thread.Join();
callback 的返回值必须为 void ,可以带一个object类型的参数
创建和销毁线程是一个要耗费大量时间的过程,另外,太多的线程也会浪费内存资源,所以通过Thread类来创建过多的线程反而有损于性能,为了改善这样的问题 ,.net中就引入了线程池。
线程池形象的表示就是存放应用程序中使用的线程的一个集合(就是放线程的地方,这样线程都放在一个地方就好管理了)。CLR(公共语言运行库)初始化时,线程池中是没有线程的,在内部, 线程池维护了一个操作请求队列,当应用程序想执行一个异步操作时,就调用一个方法,就将一个任务放到线程池的队列中,线程池中代码从队列中提取任务,将这个任务委派给一个线程池线程去执行,当线程池线程完成任务时,线程不会被销毁,而是返回到线程池中,等待响应另一个请求。由于线程不被销毁, 这样就可以避免因为创建线程所产生的性能损失。
线程池中分且分为两种线程:
工作者线程(workerThreads)和I/O线程 (completionPortThreads)
I/O线程 顾名思义 是用于 与外部系统交换信息(input/output)。
除了与外部交换信息的线程,其余都可以看做工作者线程。
除了Thread 类之外,不管使用委托还是task,或者是一些异步的方法(异步I/O,异步Socket,异步WebRequest)创建的都线程都是在线程池里面,由线程池管理。
线程池里的线程都全部是后台线程
直观来说,ThreadPool是一个静态类,所有线程共享。
使用ThreadPool创建一个工作者线程,用户并不需要显式使用类似new Thread的语句。正如上面所说,CLR初始化时,线程池中是没有线程的,此时添加线程, 线程池才可能才需要new一个新的线程,但也是线程池内部自己管理,用户并不知道。而当线程池线程完成任务时,线程不会被销毁,而是返回到线程池中,等待响应另一个请求,所以当线程池有空闲线程时,就更不用new了。
本文所说的“创建一个工作者线程”,都是指上面含义,不是说每次都真的new了新的线程。
代码示例:
class Program
{
static void Main(string[] args)
{
ThreadPool.SetMaxThreads(1000, 1000);
ThreadPoolMessage("Main Thread");
ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback2), "hello");
ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback), "world");
for (int n = 0; n < 5; n++)
Console.WriteLine(" Main thread do work!");
Console.WriteLine("");
Console.ReadKey();
}
static void AsyncCallback(object result)
{
ThreadPoolMessage("AsyncCallback");
Console.WriteLine(result as string);
}
static void AsyncCallback2(object result)
{
ThreadPoolMessage("AsyncCallback2");
Console.WriteLine(result as string);
}
static void ThreadPoolMessage(string data)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("{0}\n CurrentThreadId is {1}\n " +
"WorkerThreads is:{2} CompletionPortThreads is :{3}",
data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
QueueUserWorkItem创建一个线程,不用start()之类,callback函数中的内容将直接在另一个线程中被执行。
callback 的返回值必须为 void ,可以带一个object类型的参数
GetAvailableThreads(out int ,out int)方法可以获取当前线程池中还有多少可用的工作者和I/O线程。
运行上面的程序可以发现,调用一次QueueUserWorkItem之后,可用线程少了一个。
通过调用ThreadPool的QueueUserWorkItem方法来来启动工作者线程非常方便,但是WaitCallback只能带一个参数,并且返回值一定是void,如果我们实际操作中需要有返回值,或者需要带有多个参数,这时通过这样的方式实现就要多费周折。为了解决这样的问题,我们可以通过委托来建立工作者线程。
异步委托主要就是依靠两个函数:(如果你还不知道什么是Invoke先去了解一下C#的委托)
IAsyncResult BeginInvoke (..., callback, object obj);
var EndInvoke (IAsyncResult result);
讲到这里就不得不说一下 Beginxxx 和 Endxxx 类型的异步了……
Beginxxx 和 Endxxx 模式 都有一个共同的特点。那就是在 Beginxxx里面启动异步模式,即开始进入了另一个线程。而在Beginxxx函数里一定有一个参数是callback函数。然后再在这个callback函数里调用Endxxx,结束异步模式。.
还是说委托,先看一个代码
class Program
{
delegate int MyDelegate(string name, int x);
static void Main(string[] args)
{
ThreadPoolMessage("Main Thread");
MyDelegate myDelegate = new MyDelegate(HelloInNewThread);
IAsyncResult result = myDelegate.BeginInvoke("Leslie", 6, new AsyncCallback(AsyncCallback), "hello");
for (int n = 0; n < 5; n++)
Console.WriteLine(" Main thread do work!");
Console.ReadKey();
}
static int HelloInNewThread(string name, int x)
{
Console.WriteLine(x);
ThreadPoolMessage("Async Thread");
Thread.Sleep(2000); //虚拟异步工作
return x;// "HelloInNewThread " + name;
}
static void AsyncCallback(IAsyncResult result)
{
ThreadPoolMessage("AsyncCallback");
Console.WriteLine(result.AsyncState as string);
AsyncResult _result = (AsyncResult)result;
MyDelegate myDelegate = (MyDelegate)_result.AsyncDelegate;
int data = myDelegate.EndInvoke(result);
Console.WriteLine(data);
}
static void ThreadPoolMessage(string data)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("{0}\n CurrentThreadId is {1}\n " +
"WorkerThreads is:{2} CompletionPortThreads is :{3}",
data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
在这里,通过委托将HelloInNewThread这个函数放到了新的线程里异步执行。
具体:
BeginInvoke的前几个参数对应myDelegate绑定的函数的参数,
倒数第二个参数是callback,和之前的不同,这里的callback 带一个IAsyncResult 的参数,返回值还是void。
IAsyncResult 是一个结构体。
public interface IAsyncResult
{
object AsyncState { get; }
WaitHandle AsyncWaitHandle { get; }
bool CompletedSynchronously { get; }
bool IsCompleted { get; }
}
最后一个参数可以通过 IAsyncResult的AsyncState取得。
BeginInvoke回一个IAsyncResult对象
EndInvoke的参数是BeginInvoke返回的那个IAsyncResult对象
intdata = myDelegate.EndInvoke(result);
继续,AsyncResult 也是一个结构体,并且实现了接口IAsyncResult
public class AsyncResult : IAsyncResult, IMessageSink
{
public virtual object AsyncDelegate { get; }
public virtual object AsyncState { get; }
public virtual WaitHandle AsyncWaitHandle { get; }
public virtual bool CompletedSynchronously { get; }
public bool EndInvokeCalled { get; set; }
}
Callback的参数为 IAsyncResult ,要先把它强制转换成AsyncResult ,AsyncResult 中的AsyncDelegate 记录了 BeginInvoke的那个委托,再调用这个委托的EndInvoke方法结束委托。
(这里测试 EndInvoke的参数为 IAsyncResult 或 AsyncResult 类型都可以)
最后, BeginInvoke是在原线程中被执行, hello 和EndInvoke都是在新的线程中被执行。
除了 BeginInvoke 与 EndInvoke之外,
还有诸如以下类似的Begin/End 异步操作。
BeginWrite 与 EndWrite // 异步写 (I/O线程)
BeginRead 与 EndRead // 异步读 (I/O线程)
BeginGetRequestStream 与 EndGetRequestStream // 异步webRequest
BeginGetResponse 与 EndGetResponse 等
这种类型的异步 callback 的参数都必须指定为IAsyncResult类型
大同小异,都是在Begin之后进入异步。要执行的内容完毕之后,调用callback,
并且在callback中调用End方法,End方法的参数是Begin方法的返回值。
Callback函数也都是在另一线程中执行,不会影响到主线程。
使用Beginxxx/Endxxx 创建的都线程都是在线程池里面,由线程池管理。
Thread 和 ThreadPool中的QueueUserWorkItem都不带返回值。
委托使我们可以使用带返回值,带多个参数的工作者线程。但是并不方便。
作为更高级的Task,可以带参数,也可以有返回值,并且使用简单。
System.Threading.Tasks中的类被统称为任务并行库(Task Parallel Library,TPL),TPL使用CLR线程池把工作分配到CPU,并能自动处理工作分区、线程调度、取消支持、状态管理以及其他低级别的细节操作,极大地简化了多线程的开发。
要使用Task创建工作者线程,最好对C# lambda表达有所了解。
用Task创建一个线程很简单。主要是两种方法:
Task.Run(run_thread);
Task.Factory.StartNew(run_thread);
两者似乎没有什么差别。都是在另一个线程里运行run_thread中的内容,run_thread可以是任何函数,最多带16个参数,可以有返回值。
看一个代码:
class Program
{
static void Main(string[] args)
{
Task task1 = Task.Run(() => run_thread("hello") );
Task task2 = Task.Factory.StartNew(() => run_thread("world") );
Task task3 = Task.Factory.StartNew(() => run_thread("return") );
task1.Wait(); // Join
task2.Wait();
task3.Wait();
Console.WriteLine("Main: " + task3.Result);
Console.ReadKey();
}
public static string run_thread(string str)
{
ThreadPoolMessage("AsyncCallback");
Console.WriteLine(str);
return str.ToUpper();
}
static void ThreadPoolMessage(string data)
{
int a, b;
ThreadPool.GetAvailableThreads(out a, out b);
string message = string.Format("{0}\n CurrentThreadId is {1}\n " +
"IsBackground: {2}\n "+
"WorkerThreads is:{3} CompletionPortThreads is :{4}",
data, Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsBackground.ToString(),a.ToString(), b.ToString());
Console.WriteLine(message);
}
}
直观来说,task1 和 task2 没什么差别。
他们都传递了一个string类型的参数,但是并没有捕获run_thread的返回值。
task3 后面跟了一个泛型参数,这个参数的类型要必须和run_thread的返回值匹配,参数的个数最多为16个。
通过task3.Result就可以取得run_thread的返回值了。
Task结合lambda表达式使用起来非常方便,也是msdn推荐使用的方法。
从Thread 到 Task,并行编程越来越方便。上文所提到的也仅仅只是异步编程的几种方式,还有很多细节完全没有理会,实际编程中肯定还会碰到各种各样的问题。
还有一些异步编程模式没有提到:
异步I/O(I/0线程)
异步SqlCommand
数据并行
await和 async
后面三者个人感觉实际上用上的时候并不多,特别是2,3两点,一般人可能很难遇到要使用它的场景。
await和 async 是为了用同步的代码逻辑编写异步代码,是比较新的东西,好用但是理解起来有点麻烦。
而异步I/O大多也是开一个线程读数据一个线程处理数据。用同步机制保证安全。
对于我来说……目前来线程池都还没用过,日常工作中Thread完全就能满足我的需求了。
对C# 接触不久,对于它的核心 delegate 理解还不是很深,这篇文章我隔段时间就会读一遍,然后按照新的理解再修改。
参考:
http://www.cnblogs.com/leslies2/archive/2012/02/08/2320914.html#t6
https://msdn.microsoft.com/zh-cn/library/system.threading.tasks.task(v=vs.110).aspx