“抢先式多任务”&“协同式多任务”
在“多任务”一文中,我们提到了“协同式多任务”与“抢先式多任务”的概念和二者的区别,谈到现在主流的多任务实现是“抢先式多任务”,并且留下关于未来“协同式多任务”是否会被淘汰的疑问。今天在此再举几个例子作对比,为前面提出的疑问做个推测。
第一个例子先来看看“抢先式多任务”程序的运行表现。在这个例子中,我们用C#创建一个console程序,这个程序要做的事很简单,就是在命令行窗口记录下从程序启动到做完一个正整数累加(从1累加到50亿)的时间,程序代码如下:
1 class WasteTime 2 { 3 public void run() 4 { 5 Int64 j; 6 7 for (Int64 i = 0; i < 5000000000; i++) 8 { 9 j = i; 10 } 11 } 12 }
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.Write("Start : "); 6 Console.WriteLine(DateTime.Now.ToLongTimeString()); 7 8 /* 9 // Method 1 10 // Simple Loop 11 12 Int64 j; 13 14 for (Int64 i = 0; i < 5000000000; i++) 15 { 16 j = i; 17 } 18 */ 19 20 21 /* 22 // Method 2 23 // Using Object 24 25 26 WasteTime w1 = new WasteTime(); 27 w1.run(); 28 29 WasteTime w2 = new WasteTime(); 30 w2.run(); 31 */ 32 33 34 /*// method 3 35 // multithreads 36 */ 37 WasteTime w1 = new WasteTime(); 38 WasteTime w2 = new WasteTime(); 39 40 Thread t1 = new Thread(new ThreadStart(w1.run)); 41 Thread t2 = new Thread(new ThreadStart(w2.run)); 42 43 t1.Start(); 44 t2.Start(); 45 46 t1.Join(); 47 t2.Join(); 48 49 50 Console.Write("Finish : "); 51 Console.WriteLine(DateTime.Now.ToLongTimeString()); 52 53 54 Console.ReadKey(); 55 } 56 }
分别用三种不同的方法,得到如下的结果:
|
|
Method1 |
Method2 |
Method3 |
|
时间 |
13s |
27s |
16s |
|
Cpu 使 用 率 情 况 |
![]() |
![]() |
![]() |
注:以上数据在i5 3230m CPU电脑测试,测试过程中仅开启Visual Studio和资源监视器,图像右侧小部分曲线为开启截图软件导致
根据记录,可以看到如下现象:
1、 Method2和method3完成的工作量都是method1的两倍;
2、 method2大概用了method1两倍的时间做完累加运算;
3、 method3做完累加运算所需时间比method1多,但远比method2少;
4、 method1和method2在运算过程中都分别占用了一个cpu核心;
5、 method3在运算过程中占用了两个cpu核心;
分析以上现象,得到结论:在单线程的情况下,程序完成相同的运算所需时间基本相同,且在程序执行过程中始终占用同一个cpu内核,工作量和所需时间成线性比;在多线程的情况下,程序将任务分配在不同的cpu内核,工作量和所需时间不成线性比。
例子中,method2和method3其实可以看成是要分别完成两个任务(这里是两个相同的任务),在method2中,完成这两个任务是有先后顺序的,且在执行第二个任务的前提是第一个任务已经完成;在method3中,两个任务则是同时开始执行,且它们是独立完成的。method3的做法其实就是典型的“抢先式多任务”的实现,线程t1与t2由系统分配cpu资源和时间片,两个线程开始后,二者互相之间没有影响(仅限在这个例子中,因为线程是共享内存资源的),即不会因为其中一个线程执行的任务繁复而影响另一个线程的执行。但“抢先式多任务”的实现是需要付出更多的系统开销(overhead)的,所以,虽然两个任务分配到两个不同cpu内核,且并发执行,但它还是需要花费比method1更多的时间。这在前文也已经交代过,“抢先式多任务”的盛行,是以成熟的硬件支持为前提的。
我们针对method3,再进一步做一组测试,在这个测试中,我们分别创建1,2,3,4,5,6,7,8条线程,并记录下它们的执行时间和cpu使用情况,代码如下:
1 class Program 2 { 3 4 static void Main(string[] args) 5 { 6 7 Console.Write("Start : "); 8 Console.WriteLine(DateTime.Now.ToLongTimeString()); 9 10 // method 3 11 // multithreads 12 13 14 WasteTime w1 = new WasteTime(); 15 WasteTime w2 = new WasteTime(); 16 //WasteTime w3 = new WasteTime(); 17 //WasteTime w4 = new WasteTime(); 18 //WasteTime w5 = new WasteTime(); 19 //WasteTime w6 = new WasteTime(); 20 //WasteTime w7 = new WasteTime(); 21 //WasteTime w8 = new WasteTime(); 22 23 24 Thread t1 = new Thread(new ThreadStart(w1.run)); 25 Thread t2 = new Thread(new ThreadStart(w2.run)); 26 //Thread t3 = new Thread(new ThreadStart(w3.run)); 27 //Thread t4 = new Thread(new ThreadStart(w4.run)); 28 //Thread t5 = new Thread(new ThreadStart(w5.run)); 29 //Thread t6 = new Thread(new ThreadStart(w6.run)); 30 //Thread t7 = new Thread(new ThreadStart(w7.run)); 31 //Thread t8 = new Thread(new ThreadStart(w8.run)); 32 33 34 t1.Start(); 35 t2.Start(); 36 //t3.Start(); 37 //t4.Start(); 38 //t5.Start(); 39 //t6.Start(); 40 //t7.Start(); 41 //t8.Start(); 42 43 44 45 t1.Join(); 46 t2.Join(); 47 //t3.Join(); 48 //t4.Join(); 49 //t5.Join(); 50 //t6.Join(); 51 //t7.Join(); 52 //t8.Join(); 53 54 Console.Write("Finish : "); 55 Console.WriteLine(DateTime.Now.ToLongTimeString()); 56 Console.ReadKey(); 57 58 } 59 }
测试结果记录如下:
|
线程数 |
1 |
2 |
3 |
|
时间 |
13s |
16s |
19s |
|
Cpu使用率情况 |
![]() |
![]() |
![]() |
|
|
|
|
|
|
线程数 |
4 |
5 |
6 |
|
时间 |
22s |
28s |
33s |
|
Cpu使用率情况 |
![]() |
![]() |
![]() |
|
|
|
|
|
|
线程数 |
7 |
8 |
|
|
时间 |
40s |
46s |
|
|
Cpu使用率情况 |
![]() |
![]() |
|
注:以上数据在i5 3230m cpu上测试得到,测试过程中仅开启visual studio和资源监视器,图像右侧小部分曲线为开启截图软件导致
由以上数据可以看出:
1、 当线程数不超过cpu内核数时,每增加一条线程,完成任务付出的额外时间较之线程数超过cpu内核数时增加线程数完成任务所需时间更少;
2、 当线程数超过cpu内核数时,程序执行过程中,会有若干间隔时间cpu使用率骤降,且线程数越多,间隔越频繁;
由此可以进一步得出结论:在“抢先式多任务”实现中,尽管线程数超过了cpu内核数,但各线程还是被系统分配到了不同的内核执行;随着线程数量的增加,时间片的分配也变得更频繁,系统内销也随之加大,增加一条线程额外增加的时间也变得更多。
从中我们可以看出,“抢先式多任务”的原则,是要把诸多的任务进行分割,做平行处理,因此也带来了更大的系统开销。因为无论是时间片的分配、任务的挂起和恢复都是需要系统做更多的工作,并且这些工作都是建立在一定的硬件支持上的。
以上是“抢先式多任务”例子的分析,下面举个“协同式多任务”的例子,先看如下代码:
1 interface Runnable 2 { 4 5 void run(); 6 7 }
1 class Cat : Runnable 2 3 { 4 5 public void run() 6 7 { 8 9 Console.WriteLine("I am a cat"); 10 11 } 12 13 }
1 class Cow : Runnable 2 3 { 4 5 public void run() 6 7 { 8 9 Console.WriteLine("I am a cow"); 10 11 } 12 13 }
1 class Dog : Runnable 2 3 { 4 5 6 7 public void run() 8 9 { 10 11 Console.WriteLine("I am a dog"); 12 13 } 14 15 }
1 class WasteTime:Runnable 2 3 { 4 5 public void run() 6 7 { 8 9 int j; 10 11 for (int i = 0; i < 1000000000;i++ ) 12 13 { 14 15 j = i; 16 17 } 18 19 Console.WriteLine("==========This is an interval============="); 20 21 } 22 23 }
1 class Program 2 3 { 4 5 static void Main(string[] args) 6 7 { 8 9 List<Runnable> eventqueue = new List<Runnable>(); 10 11 12 13 eventqueue.Add(new Cat()); 14 15 eventqueue.Add(new Dog()); 16 17 eventqueue.Add(new Cow()); 18 19 eventqueue.Add(new WasteTime()); 20 21 22 23 for (int i = 0; i < 10; i++) 24 25 { 26 27 foreach (Runnable r in eventqueue) 28 29 { 30 31 32 33 r.run(); 34 35 } 36 37 } 38 39 40 41 Console.ReadKey(); 42 43 44 45 } 46 47 }
例子中的代码要做的事,其实就是反复的每隔一段时间运行Cat,Cow,Dog三个类实例的run方法(WasteTime在这里仅仅为了取得时间间隔的效果),三个实例的run方法可以看成三个任务,因为相对于WasteTime间隔的时间,三个实例执行run方法十分迅速,可以看成是同一时间内进行,这就是一个典型的“协同式多任务”的实现——因为当执行Cat的run方法时,只要程序还没跳出该方法的函数体,cpu的使用权就一直掌握在该方法所属任务手中,这里的run方法就相当于主程序的回调函数;同样的道理适用在Cow和Dog中。任务的执行并非由系统做出统筹分配,而是“该到谁就到谁”。假如我们在其中某个run方法体内加入了复杂的代码,使得程序执行长时间不能跳出方法体,这时其他任务将被推迟执行。由于这种有秩序的执行多任务的方式,“协同式多任务”相对于“抢先式多任务”并不需要太大的系统内销,而且其在编码时能使程序员更清晰当前思路,因为这是一种程序“自上而下”的执行方式;但它却要求每个回调函数不能承载太繁重的任务。
由此可见,两种多任务的实现方式各有所长,在可预见的未来,都会在不同的应用场合中发挥作用。












浙公网安备 33010602011771号