CSharp中的Thread,Task,Async,Await,IAsyncResult理解

CSharp中的Thread,Task,Async,Await,IAsyncResult理解


1. 线程(Thread)

多线程的意义在于一个应用程序中,有多个执行部分可以同时执行;对于比较耗时的操作(例如io,数据库操作),或者等待响应(如WCF通信)的操作,可以单独开启后台线程来执行,这样主线程就不会阻塞,可以继续往下执行;等到后台线程执行完毕,再通知主线程,然后做出对应操作!

在C#中开启一个新线程来执行一个耗时任务比较简单,代码如下:

static void Main(string[] args)
{
     Console.WriteLine("主线程开始");
	 
     //IsBackground=true,将其设置为后台线程
     Thread t = new Thread(Run) { IsBackground = true };
     t.Start();
	 
     Console.WriteLine("主线程在做其他的事!");
	 
     //主线程结束,后台线程会自动结束,不管有没有执行完成
     //Thread.Sleep(300);
     Thread.Sleep(1500);
     Console.WriteLine("主线程结束");
}

static void Run()
{
	 Thread.Sleep(700);
	 Console.WriteLine("这是后台线程调用");
}

执行结果如下图:
执行结果

我们可以根据执行结果看出,在启动后台线程之后,主线程继续往下执行了,并没有等到后台线程执行完之后,再向下执行.

1.1 线程池

试想一下,如果有大量的任务需要处理,例如网站后台对于HTTP请求的处理,那是不是要对每一个请求创建一个后台线程呢?显然不合适,这会占用大量内存,而且频繁地创建的过程也会严重影响速度,那怎么办呢?线程池就是为了解决这一问题,把创建的线程存起来,形成一个线程池(里面有多个线程),当要处理任务时,若线程池中有空闲线程(前一个任务执行完成后,线程不会被回收,会被设置为空闲状态),则直接调用线程池中的线程执行(例asp.net处理机制中的Application对象).

代码举例:

for(int i=0;i<10;I++)
{
	ThreadPool.QuequeUserWorkItem(m=>
	{
		Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString());
	});
}

运行结果:

运行结果
运行结果

可以看到我们虽然执行了10次,但是并没有创建10个线程,由此我们可以得出在处理简单的耗时任务时,我们可以使用线程池技术来处理,而不是手动的去开辟线程来处理耗时任务.

1.2 信号量(Semaphore)

Semaphore负责协调线程,可以限制对某一资源访问的线程数量,
下面是对SemaphoreSlim类的用法的简单描述:

static SemaphoreSlim semLim = new SemaphoreSlim(3); //3表示最多只能有三个线程同时访问
static void Main(string[] args)
{
	 for (int i = 0; i < 10; i++)
	 {
	 	new Thread(SemaphoreTest).Start();
	 }
	 Console.Read();
}
static void SemaphoreTest()
{
	 semLim.Wait();
	 Console.WriteLine("线程" + Thread.CurrentThread.ManagedThreadId.ToString() + "开始执行");
	 Thread.Sleep(2000);
	 Console.WriteLine("线程" + Thread.CurrentThread.ManagedThreadId.ToString() + "执行完毕");
	 semLim.Release();
}

执行结果:

初始状态
初始状态

运行一段时间之后
运行一段时间之后

可以看到,刚开始只有三个线程在执行,当一个线程执行完毕并释放之后,才会有新的线程来执行方法.

除了使用SemaphoreSlim类,还可以使用Semaphore类,感觉更加灵活,下面举例:


2.Task

跟线程池ThreadPool的功能类似,但是更加方便,常常搭配 async,以及await关键字一起使用,用Task开启新任务时,会从线程池中调用闲置线程来执行任务,演示代码如下:

Console.WriteLine("主线程启动");

//Task.Run启动一个线程池中的线程

//Task启动的是后台线程,要在主线程中等待后台线程执行完毕,可以调用Wait方法,Wait方法会阻塞当前线程,等待task启动的耗时任务结束.

//Task task = Task.Factory.StartNew(() => { Thread.Sleep(1500); Console.WriteLine("task启 动"); });

Task task = Task.Run(() => { 
 Thread.Sleep(1500);
 Console.WriteLine("task启动");
});
Thread.Sleep(300);
task.Wait();
Console.WriteLine("主线程结束");

执行结果如下:

执行结果
执行结果

开启新任务的方法:

Task.Run();
//或者
Task.Factory.StartNew();

开始的是后台线程,要在主线程中等待后台线程执行完毕,可以使用Wait方法(会以同步的方式来执行).不用Wait则会以异步方式来执行.

下面使用代码来比较Task和Thread:

static void Main(string[] args)
{
	 for (int i = 0; i < 5; i++)
	 {
	 	new Thread(Run1).Start();
	 }
	 for (int i = 0; i < 5; i++)
	 {
	 	Task.Run(() => { Run2(); });
	 }
}

static void Run1()
{
 	Console.WriteLine("Thread Id =" + Thread.CurrentThread.ManagedThreadId);
}
static void Run2()
{
 	Console.WriteLine("Task调用的Thread Id =" + Thread.CurrentThread.ManagedThreadId);
}

执行结果:

执行结果
执行结果

我们可以看出使用Thread会开启5个线程,用Task只开启了3个线程.

2.1 Task<TResult>

Task <TResult>就是有返回值的Task, TResult就是返回值类型.

示例代码:

Console.WriteLine("主线程开始");

//返回值类型为string
Task<string> task = Task<string>.Run(() => {
 Thread.Sleep(2000); 
 return Thread.CurrentThread.ManagedThreadId.ToString(); 
});

//会等到task执行完毕才会输出;
Console.WriteLine(task.Result);

Console.WriteLine("主线程结束");

运行结果:

运行结果
运行结果

根据运行结果,我们可以看出 task.Result方法,会阻塞当前线程,等待 Task 任务执行之后,返回函数运算结果之后,才继续向下执行.

注: Task任务可以通过CancellationTokenSource类来控制是否取消执行,下面演示CancellationTokenSource类的用法:


  • 演示:

3. async/await 关键字

async关键字用来修饰方法,表明这个方法是异步的,声明的方法的返回类型必须为:void,Task或者Task <TResult>. 并且按照规范,使用async关键字修改的方法名应该用Async结尾, 如 GetEmployeesAsync

await 关键字必须用来修饰Task或者 Task <TResult> ,而且只能出现在已经用 async 关键字修饰的异步方法中,通常情况下, async/await成对出现才有意义.

示例代码:


static void Main(string[] args)
{
 	Console.WriteLine("-------主线程启动-------");
	
 	Task<int> task = GetStrLengthAsync();
	
 	Console.WriteLine("主线程继续执行");
	
 	Console.WriteLine("Task返回的值" + task.Result);
	
 	Console.WriteLine("-------主线程结束-------");
}

static async Task<int> GetStrLengthAsync()
{
 	Console.WriteLine("GetStrLengthAsync方法开始执行");
	
 	//此处返回的<string>中的字符串类型,而不是Task<string>
 	string str = await GetString();
 	Console.WriteLine("GetStrLengthAsync方法执行结束");
 	return str.Length;
}

static Task<string> GetString()
{
	//Console.WriteLine("GetString方法开始执行")
 	return Task<string>.Run(() =>
 	{
 		Thread.Sleep(2000);
 		return "GetString的返回值";
 	});
}

运行结果:

运行结果
运行结果

可以看出,main函数调用 GetStrLengthAsync 方法后,在await之前,都是同步执行到,遇到await关键字之后,主线程才会从GetStrLengthAsync退出来继续往下执行.

那么是否在遇到await关键字的时候程序自动开启了一个后台线程去执行GetString方法呢?

现在把GetString方法中的那行注释解除,运行结果如下:

运行结果
运行结果

大家可以看到,在遇到await关键字后,没有继续执行GetStrLengthAdync方法后面的操作,也没有马上返回到main方法,而是执行了GetString的第一行,以此可以判断await这里并额米有开启新的线程去执行GetString方法,而是以同步的方式让GetString方法执行,等到执行GetString方法中的Task<string>.Run()的时候才由Task开启了后台线程!

那么await的作用是什么呢?

可以从字面上理解,上面提到task.wait可以让主线程等待后台线程执行完毕,await和wait类似,同样是等待,等待Task<string>.Run()开始的后台线程执行完毕,不同的是await不会阻塞主线程,只会让GetStrLengthAsync方法暂停执行。

那么await是怎么做到的呢?有没有开启新线程去等待?

运行分析
运行分析

只有两个线程(主线程和Task开启的线程)!至于怎么做到的后续在进行深入研究.

4. IAsyncResult

包含可异步操作的方法的类需要实现IAsyncResult接口,Task类就实现了此接口.

反编译结果
反编译结果

在不借助Task的情况下怎么实现异步呢?
一种方法是:我们可以使用委托的方式,下面来描述一下另一种方式

class Program
{
 	static void Main(string[] args)
 	{
		 Console.WriteLine("主程序开始--------------------");
		 int threadId;
		 AsyncDemo ad = new AsyncDemo();
		 AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod);
		 
		 IAsyncResult result = caller.BeginInvoke(3000,out threadId, null, null);   //关键步骤
		 
		 Thread.Sleep(0);
		 Console.WriteLine("主线程线程 {0} 正在运行.",Thread.CurrentThread.ManagedThreadId)
		 //会阻塞线程,直到后台线程执行完毕之后,才会往下执行
		 result.AsyncWaitHandle.WaitOne();                                                          //关键步骤
		 Console.WriteLine("主程序在做一些事情!!!");
		 
		 //获取异步执行的结果
		 string returnValue = caller.EndInvoke(out threadId, result);                      //关键步骤
		 
		 //释放资源
		 result.AsyncWaitHandle.Close();
		 Console.WriteLine("主程序结束--------------------");
		 Console.Read();
 	}
}
public class AsyncDemo
{
	 //供后台线程执行的方法
 	public string TestMethod(int callDuration, out int threadId)
 	{
		 Console.WriteLine("测试方法开始执行.");
		 Thread.Sleep(callDuration);
		 threadId = Thread.CurrentThread.ManagedThreadId;
		 return String.Format("测试方法执行的时间 {0}.", callDuration.ToString());
	 }
}

public delegate string AsyncMethodCaller(int callDuration, out int threadId);

运行结果:

运行结果
运行结果

和Task的用法差异不是很大!result.AsyncWaitHandle.WaitOne()就类似Task的Wait。

5. Parallel

5.1 循环例子

Parallel静态类,提供了可以在循环中开启线程的方法,示例代码如下:

Stopwatch watch1 = new Stopwatch();

watch1.Start();
for (int i = 1; i <= 10; i++)
{
 	Console.Write(i + ",");
 	Thread.Sleep(1000);
}
watch1.Stop();
Console.WriteLine(watch1.Elapsed);

//下面的代码是使用Parallel.For循环在循环的过程中开启线程

Stopwatch watch2 = new Stopwatch();
watch2.Start();

//会调用线程池中的线程
Parallel.For(1, 11, i =>
{
 Console.WriteLine(i + ",线程ID:" + Thread.CurrentThread.ManagedThreadId);
 Thread.Sleep(1000);
});
watch2.Stop();

Console.WriteLine(watch2.Elapsed);

运行结果:

运行结果
运行结果

5.2 循环List <T>

List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 6, 7, 8, 9 };
Parallel.ForEach<int>(list, n =>
{
 	Console.WriteLine(n);
 	Thread.Sleep(1000);
});

5.3 执行Action[]数组中的方法

Action[] actions = new Action[]
{ 
 	new Action(()=>
	{
 		Console.WriteLine("方法1");
 	}),
	
 	new Action(()=>{
 		Console.WriteLine("方法2");
 	})
};

Parallel.Invoke(actions);

6. 异步回调

文中所有Task<TResult>的返回值都是直接用task.result获取,这样如果后台任务没有执行完毕的话,主线程会等待其执行完毕,这样的话就和同步一样了(看上去一样,但其实await的时候并不会造成线程的阻塞,web程序感觉不到,但是wpf,winform这样的桌面程序若不使用异步,会造成UI线程的阻塞)。简单演示一下Task回调函数的使用:

Console.WriteLine("主线程开始");

Task<string> task = Task<string>.Run(() => {
	Thread.Sleep(2000); 
 	return Thread.CurrentThread.ManagedThreadId.ToString(); 
});

//会等到任务执行完之后执行
task.GetAwaiter().OnCompleted(() =>
{
 	Console.WriteLine(task.Result);
});

Console.WriteLine("主线程结束");
Console.Read();

执行结果:

执行结果
执行结果

OnCompleted中的代码会在任务执行完成之后执行,另外ContinueWith也是一个重要的方法:

Console.WriteLine("主线程开始");

Task<string> task = Task<string>.Run(() => {
 	Thread.Sleep(2000); 
 return Thread.CurrentThread.ManagedThreadId.ToString(); 
});

task.GetAwaiter().OnCompleted(() =>
{
 	Console.WriteLine(task.Result);
});

task.ContinueWith(m=>{

	Console.WriteLine("第一个任务结束啦!我是第二个任务");
});
Console.WriteLine("主线程结束");
Console.Read();

执行结果:

执行结果
执行结果

ContinueWith(); 方法可以让该后台线程继续执行新的任务.

7. 委托方式实现异步

可以参考以下博文:
委托方式实现方法的同步执行,异步执行,异步回调

posted @ 2020-04-01 15:25  HelloZyjS  阅读(1942)  评论(0编辑  收藏  举报