C# 多线程异步编程笔记

来源:互联网 发布:sd卡受损数据恢复 编辑:程序博客网 时间:2024/04/30 04:22

为什么要异步编程

1、避免性能瓶颈

从磁盘里读取数据,并对数据处理。同步方式,速度有限,必须读完再处理。 异步方式,一个线程读,一个线程处理,有可能读完了就处理完了。

2、增强应用程序的总体响应能力

异步对可能引起阻塞的活动至关重要。 比如 web对资源的 访问,UI界面响应。如果在一个同步应用程序中有任何的线程被阻塞了,那么所有线程都将被阻塞。 应用程序停止响应,使用异步方法时,将可能会引起阻塞的操作放在一个独立的线程中,主线程继续做其它事情。

下面记录几种异步编程的方式

一、Thread

最简单,最直接,最原始的多线程编程
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类型的参数

二、ThreadPool 线程池

创建和销毁线程是一个要耗费大量时间的过程,另外,太多的线程也会浪费内存资源,所以通过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之后,可用线程少了一个。

三、使用delegate(委托)

通过调用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 创建的都线程都是在线程池里面,由线程池管理。

四、Task

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<string> task3 = Task.Factory.StartNew<string>(() => run_thread("return") );task1.Wait(); // Jointask2.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

原创粉丝点击