c#什么是异步

1.Task

Task.Run(() =>
{
	for (int i = 0; i < 100; i++)
	{
		Console.WriteLine(i);
	}
});

for (int i = 0; i < 100; i++)
{
	Console.WriteLine(i);
}

Task.Run会创建一个后台线程来执行任务。
这让很多人以为异步就是多线程。

2.多线程任务

假如,我想知道一个文件夹有多大。
办法是做一个递归方法。
对每个文件夹,获取子文件夹的大小。
并加上他里面文件的大小。

long 获取文件夹大小(DirectoryInfo dir)
{
	long sum = 0;

	var dirs = dir.GetDirectories();
	for (int i = 0; i < dirs.Length; i++)
	{
		sum += 获取文件夹大小(dirs[i]);
	}
	var fils = dir.GetFiles();
	for (int i = 0; i < fils.Length; i++)
	{
		sum += 获取文件大小(fils[i]);
	}
	return sum;
}
long 获取文件大小(FileInfo file)
{
	return file.Length;
}

现在你嫌弃他太慢了。因为他是单线程任务。
你打算把他改成多线程任务。

long 获取文件夹大小(DirectoryInfo dir)
{
	var dirs = dir.GetDirectories();
	var tdir = new Task<long>[dirs.Length];
	for (int i = 0; i < dirs.Length; i++)
	{
		tdir[i] = Task.Run(() => 获取文件夹大小(dirs[i]));
	}

	var fils = dir.GetFiles();
	var tfil = new Task<long>[fils.Length];
	for (int i = 0; i < fils.Length; i++)
	{
		tfil[i] = Task.Run(() => 获取文件大小(fils[i]));
	}

	long sum = 0;
	for (int i = 0; i < tdir.Length; i++)
	{
		sum += tdir[i].Result;
	}
	for (int i = 0; i < tfil.Length; i++)
	{
		sum += tfil[i].Result;
	}
	return sum;
}

利用Task来创建线程,这样可以造成多线程的任务。从逻辑上是没有问题的。

3. 空等待任务

但是我们深入一点看看,这样做会发生什么。
我们从 C:\Windows 文件夹调用这个方法。

然后他创建了 C:\Windows\System 和C:\Windows\System32 的任务。
你现在有一万个文件夹,所以有了一万个线程。

而你的CPU只有8个线程。那这一万个线程如何运作呢?
答案是排队轮循。

现在轮到了C:\Windows\System。
但是他无法执行下去。因为System下有子文件夹,而子文件夹的任务没有完成,没有得到结果。

那子文件夹什么时候算好呢?不知道,反正不是现在。因为现在正轮到System。
那么这次给他轮询的时间,就完成浪费掉了。

这种时候的情形就是,你有一万个线程。轮到你鼠标移动的线程概率很低。你鼠标动都动不了。
但你打开任务管理器却发现,CPU占用率10%。
那你就会很奇怪:明明卡的要死,CPU却没有负载。

4.挂起线程

异步就是为了解决这种问题的。
既然你占着CPU,又什么都不做,那你占着他干嘛?
你干脆不要参与轮循了,等你准备好再来。

控制线程的挂起,这才是异步的核心理念。

5.续接任务

但是Task是不能控制一个任务什么时候挂起什么时候执行的。
Task的作用是创建一个挂起的任务。然后他一旦开始执行,就一直执行了。
他的控制方式是将任务分段。每次只有一段任务会进入线程轮循。
他控制每段任务什么时候开始,开始的任务才会进入线程轮循。

将上述的操作改为异步方法

Task<long> 获取文件夹大小(DirectoryInfo dir)
{
	var dirs = dir.GetDirectories();
	var tDir = new Task<long>[dirs.Length];
	for (int i = 0; i < dirs.Length; i++)
	{
		tDir[i] = 获取文件夹大小(dirs[i]);
	}
	Task<long[]> all = Task.WhenAll(tDir);

	var tSum = all.ContinueWith(all =>
	{
		long sum = 0;
		var fils = dir.GetFiles();
		for (int i = 0; i < fils.Length; i++)
		{
			sum += 获取文件大小(fils[i]);
		}
		for (int i = 0; i < all.Result.Length; i++)
		{
			sum += all.Result[i];
		}
		return sum;
	});
	return tSum;
}
long 获取文件大小(FileInfo file)
{
	return file.Length;
}

在方法的开始,首先创建了一堆的线程任务。
而这些任务不是获取大小,而是继续创建任务,创建完成后这一段就结束了。

然后Task.WhenAll,创建了一个新任务。他会在这些任务全部完成时开始。
这个任务什么也不会做,仅仅是获取一个执行的时机。

接着ContinueWith,当任务完成时,开始一个任务。
这是接在Task.WhenAll后面的,也就是说在所有子文件夹计算完时,开始执行任务。
这里面的内容才是正真的计算大小。这个任务开始时,已经获取到了所有子文件夹的大小了。
所以这个任务不会空等待。

6.异步方法

异步的关键点在于ContinueWith将多个任务分段。
而异步方法提供的语法糖只有让使用await代替ContinueWith。

async Task<long> 获取文件夹大小(DirectoryInfo dir)
{
	var dirs = dir.GetDirectories();
	var tDir = new Task<long>[dirs.Length];
	for (int i = 0; i < dirs.Length; i++)
	{
		tDir[i] = 获取文件夹大小(dirs[i]);
	}
	long[] all = await Task.WhenAll(tDir);

	long sum = 0;
	var fils = dir.GetFiles();
	for (int i = 0; i < fils.Length; i++)
	{
		sum += 获取文件大小(fils[i]);
	}
	for (int i = 0; i < all.Length; i++)
	{
		sum += all[i];
	}
	return sum; 
}
long 获取文件大小(FileInfo file)
{
	return file.Length;
}

这和上面一版的区别是,Task.WhenAll原本返回的是Task<long[]>类型。
而经过await后,去掉了Task的外壳,直接获得了long[]类型。
而且最末尾返回的类型是long的sum。异步方法会自动为他添加Task外壳,成为Task<long>。

此外,await代替了ContinueWith。后面的内容不需要使用写在ContinueWith的委托里,
而是在外面直接就写。异步方法会自动从每个await的节点切割,分段成很多个Task。

但是,第一个await之前的内容,不会做成Task,不会使用多线程执行,就是普通的方法。
所以异步方法中如果没有await,会警告你此方法以同步运行。
在这里插入图片描述

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值