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,会警告你此方法以同步运行。