C# 多线程异步编程笔记

为什么要异步编程

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(); // 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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值