说到异步,其实这并不是软件的概念,而是计算机的概念,而与之对应的就是同步了,相信大多数编程人员对这两个词都不陌生。
首先我们先明确几个概念,线程,进程,多线程,以及并行。
线程:可以理解为计算机执行的最小单位。
进程:可以理解为是维持某一个应用程序的线程集合,线程包括一个甚至是多个线程。
多线程:指的是多个线程并发执行进行工作。
并行:指的是多个线程包括主线程一起执行工作,其实并行是多线程的一种。
下图可以看到一般情况下线程的数量大都是大于进程的数量的。
之所以现在要学习异步和多线程,主要是因为在项目中有时候,只是操作单独一根子线程有时候并不是很方便,面对不同的需求,有时候不得不使用多个线程去做不同的事情,来提高系统的性能。
同步
同步只有一根主线程,主线程从上到下去按顺序执行代码。下图显示的就是同步的方法,编程人员其实一开始接触的便是同步,只不过大多数初学者还没有这个概念而已,所以在这里对于同步就不过多的介绍了。
List<Student> students = GetStudentList();
Console.WriteLine("创建数组完毕");
List<Student> newStudents = new List<Student>();
同步的方法
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < 10; i++)
{
var stus1 = students.Where(c => c.ID > 500 && c.ID < 999999).ToList();
newStudents.AddRange(stus1);
}
stopwatch.Stop();
Console.WriteLine("查询得到的学生的个数" + newStudents.Count);
Console.WriteLine("主线程的ID是" + Thread.CurrentThread.ManagedThreadId + "耗时毫秒" + stopwatch.ElapsedMilliseconds);
异步
.NET Framework 允许您异步调用任何方法。定义与您需要调用的方法具有相同签名的委托,公共语言运行库(CLR)将自动为该委托定义具有适当签名。
所谓异步其实也是多线程的一种。在C#中无论是使用异步还是使用多线程,都不得不使用委托,这是因为异步其实是会开启一根子线程去执行任务,而在C#这门面向对象的语言中,委托或者函数(方法)则更像是一件事情,可以理解为是一个任务,这就像需要让一个人做一件事情一样。
在c#中可以通过BeginInvoke ()来启动一个异步,调用 BeginInvoke 后可随时调用 EndInvoke() 方法;如果异步调用未完成,EndInvoke 将一直阻塞到,如果委托是由返回值的,则EndInvoke() 将带回委托的返回值。下面的代码是展示的是启动带有返回值的委托,并取得其结果:
try
{
stopwatch.Restart();
Func<List<Student>, List<Student>, List<Student>> func = Test;
IAsyncResult asyncResult = func.BeginInvoke(students, newStudents, ar =>
{
Console.WriteLine("回调函数");
}, "dasd");
asyncResult.AsyncWaitHandle.WaitOne();
stopwatch.Stop();
Console.WriteLine("查询得到的学生的个数" + newStudents.Count);
Console.WriteLine("主线程的id是" + Thread.CurrentThread.ManagedThreadId + "耗时毫秒" + stopwatch.ElapsedMilliseconds);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
其是在这个方法中,有一点是需要注意的就是newStudents.AddRange(func.EndInvoke(asyncresult));
就是可能有些人会认为这里存在一个线程安全的问题,其实并不是这样的,我们循环创建的每一个异步,其实都对应着一个EndInvoke()的方法,这样就不存在什么线程安全的问题了。
public static List<Student> Test(List<Student> students,List<Student> newStudents)
{
for (int i = 0; i < 10; i++)
{
Func<List<Student>, List<Student>> func = WhereStudents;
IAsyncResult asyncresult = func.BeginInvoke(students, argumenst =>
{
Console.WriteLine("子线程线程的id是" + Thread.CurrentThread.ManagedThreadId);
}, $"编号{i}");
newStudents.AddRange(func.EndInvoke(asyncresult));
}
return newStudents;
}
看执行的结果,根据结果我们可以看到异步和同步查询到学生的数量都是一样的,而且在耗时方面异步的耗时比同步的耗时更少,毕竟多个人比一个人做一件事件效率更高。另外我们也可以发现在异步中除了编号为1的主线程外,还有子线程5和6。为什么我们启动了10个异步却只有2个子线程?其实在循环的时候,主线程遇见了异步操作只会向OS(操作系统)发出一个信号,然后主线程就走开了,OS接到信号后,会去找线程池去取线程,由于我们的代码只是做了一个简单的查询,子线程做的事情比较少,导致主线程在循环到某一步时,子线程已经执行完任务,被放回到了线程池,而下次需要子线程执行任务的时候,OS又将刚刚回到线程池的线程又派出去执行任务去了。
异步顺序的控制
无论是在异步还是在多线程中,都不要去使用线程ID或者是通过Sleep()方法来控制,因为这都是很不靠谱的,也许这一次能行,但是下一次就未必了。
回调函数
对于异步来说,每次执行玩一个异步都会执行一个回调函数
IAsyncResult asyncresult = func.BeginInvoke(students, argumenst =>
{
Console.WriteLine("子线程线程的id是" + Thread.CurrentThread.ManagedThreadId);
}, $"编号{i}");
这里的lamdba表达式其实就是一个回调函数,可以通过回调去对异步进行一些控制。
EndInvoke ()函数
与BeginInvoke对应的就是EndInvoke ,它既可以标识一个异步的完成,也可以得到异步的返回值,如果异步一直未结束,那EndInvoke 就会被一直堵塞。
IsCompleted属性
每一个异步执行完毕后都会得到一个AsyncResult的结果,我们可以通过判断asyncresult.IsCompleted()来检测异步是否已经结束了,但是不推荐这种,因为如果异步为未执行完毕,主线程则会一直等待,如果是window程序则会造成界面的卡死
if (asyncResult.IsCompleted)
{
Console.WriteLine("子线程还没执行完毕");
}
Console.WriteLine("子线程已经执行完毕了");
AsyncWaitHandle.WaitOne函数
这个方法与第三个方法类似,当子线程未执行完毕时,主线程都会被堵塞,但是不同的是AsyncWaitHandle.WaitOne() 函数可以传递一个毫秒数进去,也就是说我们可以让主线程等待一段时间,如果主线程等待完毕有,子线程未执行完毕,那么主线程也会继续执行任务,这样我们就可以通过这个函数进行一些超时的控制。
Func<List<Student>, List<Student>, List<Student>> func = Test;
IAsyncResult asyncResult = func.BeginInvoke(students, newStudents2, ar =>
{
Console.WriteLine("回调函数");
}, "dasd");
asyncResult.AsyncWaitHandle.WaitOne();