Parallel类是对线程的一个很好的抽象。该类位于System.Threading.Tasks名称空间中,提供了数据和任务并行性。
Parallel类定义了并行的for和foreach的静态方法。对于C#的for和foreach语句而言,循环从一个线程中运行。Parallel类使用多个任务,因此使用多个线程来完成这个作业。
Parallel.For()和Parallel.ForEach()方法在每次迭代中调用相同的代码,而Parallel.Invoke()方法允许同时调用不同的方法。Parallel.Invoke()用于任务并行性,而Parallel.ForEach()用于数据并行性。
1. 使用Parallel.For()方法循环
Parallel.For()方法类似于C#的for循环语句,也是多次执行一个任务。使用Parallel.For()方法,可以并行运行迭代。迭代的顺序没有定义。
ParallelSamples的示例代码使用了如下名称空间:
名称空间
System
System.Linq
System.Threading
System.Threading.Tasks
注意:
这个示例使用命令行参数。为了了解不同的特性,应在启动示例应用程序时传递不同的参数,如下所示,或检查Main()方法。在Visual Studio中,可以在项目属性的Debug选项中传递命令行参数。使用dotnet命令行传递命令行参数-p,则可以启动命令dotnet run-- -p。
有关线程和任务的信息,下面的Log方法把线程和任务标识符写到控制台:
static void Log(string prefix)=>
System.Console.WriteLine($"{prefix},task:{Task.CurrentId},thread:{Thread.CurrentThread.ManagedThreadId}");
下面看看在Parallel.For()方法中,前两个参数定义了循环的开头和结束。示例从0迭代到9。第3个参数是一个Action<int>委托。整数参数是循环的迭代次数,该参数被传递给委托引用的方法。Parallel.For()方法的返回类型是ParallelLoopResult结构,它提供了循环是否结束的信息。
static void ParallelFor()
{
ParallelLoopResult result = Parallel.For(0,10,i=>
{
Log($"Start: {i}");
Task.Delay(10).Wait();
Log($"End: {i}");
});
Console.WriteLine($"Is completed: {result.IsCompleted}");
}
在Parallel.For()的方法体中,把索引、任务标识符和线程标识符写入控制台中。从输出可以看出,顺序是不能保证的。如果再次运行这个程序,可以看到不同的结果。程序这次的运行顺序是1-5-0-2-3,有18个任务和10个线程。任务不一定映射到一个线程上。线程也可以被不同的任务重用。
Start: 1,task:2,thread:4
Start: 5,task:6,thread:9
Start: 0,task:1,thread:1
Start: 2,task:3,thread:5
Start: 3,task:4,thread:6
Start: 6,task:7,thread:7
Start: 4,task:5,thread:8
End: 3,task:4,thread:6
End: 5,task:6,thread:9
Start: 9,task:18,thread:9
End: 2,task:3,thread:5
End: 0,task:1,thread:1
End: 1,task:2,thread:4
Start: 7,task:16,thread:6
End: 6,task:7,thread:7
End: 4,task:5,thread:8
Start: 8,task:8,thread:10
End: 7,task:16,thread:6
End: 9,task:18,thread:9
End: 8,task:8,thread:10
Is completed: True
并行体内的延迟等待10毫秒,会有更好的机会来创建线程。如果删除这行代码,就会使用更少的线程和任务。
在结果中可以看到,循环的每个end-log使用与start-log相同的线程和任务。使用Task.Delay()和Wait()方法会阻塞当前线程,知道延迟结束。
修改前面的示例,现在使用await关键字和Task.Delay()方法:
static void ParallelForWithAsync()
{
ParallelLoopResult result = Parallel.For(0,10,async i=>
{
Log($"Start: {i}");
await Task.Delay(10);
Log($"End: {i}");
});
Console.WriteLine($"Is completed: {result.IsCompleted}");
}
其结果如以下代码片段所示。在输出中可以看到,调用Task.Delay()方法后,线程发生了变化。例如,循环迭代8在延迟前的线程ID为7,在延迟后的线程ID为5。在输出中还可以看出,任务不再存在,只有线程了,而且这里重用了前面的线程。另一个重要的方面是,Parallel类的For()方法并没有等待延迟,而是直接完成。Parallel类只等待它创建的任务,而不等待其他后台活动。在延迟后,也有可能完全看不到方法的输出,出现这种情况的原因是主线程(是一个前台线程)结束,所有的后台线程被终止。下一章将讨论前台线程和后台线程。
以下示例代码增加了“是否为后台线程”的判断:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ParallelTest
{
class Program
{
static void Main(string[] args)
{
//for (int i = 0; i < 10; i++)
//{
// Log($"Start {i}");
// Task.Delay(10).Wait();
// Log($"End {i}");
//}
//ParallelFor();
System.Console.WriteLine($"Start thread: {Thread.CurrentThread.ManagedThreadId}, "+
$"Is background thrread:{Thread.CurrentThread.IsBackground}");
ParallelForWithAsync();
}
static void Log(string prefix) =>
System.Console.WriteLine($"{prefix},task:{Task.CurrentId}, "+
$"thread:{Thread.CurrentThread.ManagedThreadId}, Is background thrread:{Thread.CurrentThread.IsBackground}");
// static void ParallelFor()
// {
// ParallelLoopResult result = Parallel.For(0,10,i=>
// {
// Log($"Start: {i}");
// Task.Delay(10).Wait();
// Log($"End: {i}");
// });
// Console.WriteLine($"Is completed: {result.IsCompleted}");
// }
static void ParallelForWithAsync()
{
ParallelLoopResult result = Parallel.For(0,10,async i=>
{
Log($"Start: {i}");
await Task.Delay(10);
Log($"End: {i}");
});
Console.WriteLine($"Is completed: {result.IsCompleted}");
}
}
}
输出如下:
Start thread: 1, Is background thrread:False
Start: 0,task:1,thread:1, Is background thrread:False
Start: 2,task:7,thread:5, Is background thrread:True
Start: 3,task:3,thread:6, Is background thrread:True
Start: 1,task:4,thread:4, Is background thrread:True
Start: 6,task:5,thread:9, Is background thrread:True
Start: 4,task:6,thread:7, Is background thrread:True
Start: 5,task:2,thread:8, Is background thrread:True
Start: 7,task:1,thread:1, Is background thrread:False
Start: 8,task:3,thread:6, Is background thrread:True
Start: 9,task:7,thread:5, Is background thrread:True
Is completed: True
加一行代码:Console.ReadKey(),以延迟主线程退出,得到输出结果如下:
Start thread: 1, Is background thrread:False
Start: 4,task:1, thread:7, Is background thrread:True
Start: 5,task:2, thread:8, Is background thrread:True
Start: 2,task:3, thread:5, Is background thrread:True
Start: 6,task:4, thread:9, Is background thrread:True
Start: 3,task:5, thread:6, Is background thrread:True
Start: 1,task:6, thread:4, Is background thrread:True
Start: 0,task:7, thread:1, Is background thrread:False
Start: 7,task:2, thread:8, Is background thrread:True
Start: 8,task:1, thread:7, Is background thrread:True
Start: 9,task:3, thread:5, Is background thrread:True
Is completed: True
End: 1,task:, thread:4, Is background thrread:True
End: 0,task:, thread:5, Is background thrread:True
End: 5,task:, thread:5, Is background thrread:True
End: 3,task:, thread:5, Is background thrread:True
End: 4,task:, thread:5, Is background thrread:True
End: 7,task:, thread:8, Is background thrread:True
End: 2,task:, thread:4, Is background thrread:True
End: 9,task:, thread:9, Is background thrread:True
End: 8,task:, thread:6, Is background thrread:True
End: 6,task:, thread:7, Is background thrread:True
注意:
从这里可以看到,虽然使用.NET和C#的异步功能十分方便,但是知道后台发生了什么仍然很重要,而且必须留意一些问题。
2. 提前中断Parallel.For
也可以提前中断Parallel.For()方法,而不是完成所有迭代。For()方法的一个重载版本接受Action<int,ParallelLoopState>类型的第三个参数。使用这些参数定义一个方法,就可以调用ParallelLoopStete的Break()或Stop()方法,以影响循环的结果。
注意,迭代的顺序没有定义。
static void StopParallelForEarly()
{
ParallelLoopResult result = Parallel.For(10,40,(int i,ParallelLoopState pls)=>
{
Log($"Start: {i}");
if(i > 12)
{
pls.Break();
Log($"Break now...{i}");
}
Task.Delay(10).Wait();
Log($"End: {i}");
});
Console.WriteLine($"Is Completed: {result.IsCompleted}");
Console.WriteLine($"Lowest break iteration: {result.LowestBreakIteration}");
}
应用程序的这次运行说明,迭代值大于12时中断,但其他任务可以同时运行,有其他值的任务也可以运行。在中断前开始的所有任务都可以继续运行,直到结束。利用LowestBreakIteration属性,可以忽略其他不需要的任务的结果。
Start thread: 1, Is background thrread:False
Start: 20,task:3, thread:5, Is background thrread:True
Start: 15,task:2, thread:4, Is background thrread:True
Start: 10,task:1, thread:1, Is background thrread:False
Break now...20,task:3, thread:5, Is background thrread:True
Break now...15,task:2, thread:4, Is background thrread:True
End: 20,task:3, thread:5, Is background thrread:True
End: 10,task:1, thread:1, Is background thrread:False
Start: 11,task:1, thread:1, Is background thrread:False
End: 15,task:2, thread:4, Is background thrread:True
End: 11,task:1, thread:1, Is background thrread:False
Start: 12,task:1, thread:1, Is background thrread:False
End: 12,task:1, thread:1, Is background thrread:False
Start: 13,task:1, thread:1, Is background thrread:False
Break now...13,task:1, thread:1, Is background thrread:False
End: 13,task:1, thread:1, Is background thrread:False
Is Completed: False
Lowest break iteration: 13
当使用Stop()方法时,输出结果如下
Start thread: 1, Is background thrread:False
Start: 10,task:1, thread:1, Is background thrread:False
Start: 15,task:2, thread:4, Is background thrread:True
Break now...15,task:2, thread:4, Is background thrread:True
End: 10,task:1, thread:1, Is background thrread:False
End: 15,task:2, thread:4, Is background thrread:True
Is Completed: False
Lowest break iteration:
拓展:Break()与Stop()的区别
Stop():它将告知循环的所有迭代(包括那些在其他线程上的当前迭代之前开始的迭代)在方便的情况下尽快停止。
Break():它会导致其他线程放弃对后续片段的工作(如果它们正忙于任何这样的工作),并在退出循环之前处理完所有前面的元素。
区别就在于,Stop仅仅通知其他迭代尽快结束,而Break不仅通知其他迭代尽快结束,同时还要保证退出之前要完成LowestBreakIteration之前的迭代。 例如,对于从 0 到 1000 并行迭代的 for 循环,如果从第 100 此迭代开始调用 Break,则低于 100 的所有迭代仍会运行,从 101 到 1000 的迭代则不必要。而调用Stop方法不保证低于 100 的所有迭代都会运行。
3. Parallel.For()方法的初始化
Parallel.For()方法使用几个线程来执行循环。如果需要对每个线程进行初始化,就可以使用Parallel.For<TLocal>()方法。除了from和to对应的值之外,For()方法的泛型版本还接受3个委托参数。第一个参数类型是Func<TLocal>。因为这里的例子对于TLocal使用字符串,所以该方法需要定义为Func<srtring>,即返回string的方法。这个方法仅对用于执行迭代的每个线程调用一次。
第二个委托参数为循环定义了委托。在实例中,该参数的类型是Func<int,ParallelLoopState,string,string>。其中第一个参数是循环迭代,第二个参数ParallelLoopState允许停止循环,如前所述。循环体方法通过第3个参数接受从init方法返回的值,循环体方法还需要返回一个值,其类型是用For泛型参数定义的。
For()方法的最后一个参数指定一个委托Action<TLocal>;在该示例中,接收一个字符串。这个方法仅对于每个线程调用一次,这是一个线程退出方法。
static void ParallelForWithInit()
{
Parallel.For<string>(0,10,()=>
{
//invoked once for each thread
Log("init: ");
return $"T{Thread.CurrentThread.ManagedThreadId}";
},
( i,pls,str1)=>
{
//invoked for each member
Log($"Num i: {i} str1: {str1}");
Task.Delay(10).Wait();
return $"i: {i}";
},
(str1)=>
{
//final action on each thread
Log($"Finally: {str1}");
});
}
运行一次这个程序的结果如下:
Num i: 0 str1: T1,task:1, thread:1, Is background thrread:False
init: ,task:3, thread:5, Is background thrread:True
Num i: 2 str1: T5,task:3, thread:5, Is background thrread:True
init: ,task:4, thread:6, Is background thrread:True
Num i: 3 str1: T6,task:4, thread:6, Is background thrread:True
init: ,task:5, thread:7, Is background thrread:True
Num i: 4 str1: T7,task:5, thread:7, Is background thrread:True
init: ,task:10, thread:9, Is background thrread:True
Num i: 5 str1: T9,task:10, thread:9, Is background thrread:True
init: ,task:12, thread:8, Is background thrread:True
Num i: 6 str1: T8,task:12, thread:8, Is background thrread:True
Finally: i: 2,task:3, thread:5, Is background thrread:True
init: ,task:16, thread:5, Is background thrread:True
Num i: 7 str1: T5,task:16, thread:5, Is background thrread:True
Finally: i: 3,task:4, thread:6, Is background thrread:True
Num i: 9 str1: i: 0,task:1, thread:1, Is background thrread:False
Finally: i: 1,task:2, thread:4, Is background thrread:True
Finally: i: 4,task:5, thread:7, Is background thrread:True
Finally: i: 6,task:12, thread:8, Is background thrread:True
Finally: i: 5,task:10, thread:9, Is background thrread:True
init: ,task:13, thread:10, Is background thrread:True
Num i: 8 str1: T10,task:13, thread:10, Is background thrread:True
Finally: i: 7,task:16, thread:5, Is background thrread:True
Finally: i: 8,task:13, thread:10, Is background thrread:True
Finally: i: 9,task:1, thread:1, Is background thrread:False
输出显示,为每个线程只掉用一次init()方法;循环体从初始化中接收第一个字符串,并用相同的线程将这个字符串传递到下一个迭代体。最后,为每个线程调用一次最后一个动作,从每个体中接收最后的结果。
通过这个功能,这个方法完美地累加了大量数据集合的结果。
拓展:
改下代码,会得到奇怪的结果,如下所示:
static void ParallelForWithInit()
{
Parallel.For<string>(0,30,()=>
{
//invoked once for each thread
//Log("init: ");
//return $"T{Thread.CurrentThread.ManagedThreadId}";
return "Parallel";
},
( i,pls,str1)=>
{
//invoked for each member
//Log($"Num i: {i} str1: {str1}");
Task.Delay(10).Wait();
//return $"i: {i}";
return $"{i}: {str1} Thread: {Thread.CurrentThread.ManagedThreadId}";
},
(str1)=>
{
//final action on each thread
//Log($"Finally: {str1}");
Console.WriteLine(str1);
});
}
Start thread: 1, Is background thrread:False
5: Parallel Thread: 4
10: Parallel Thread: 6
15: Parallel Thread: 5
20: Parallel Thread: 7
1: Parallel Thread: 8
25: Parallel Thread: 9
8: Parallel Thread: 10
4: Parallel Thread: 8
7: 6: Parallel Thread: 4 Thread: 4
27: 26: Parallel Thread: 9 Thread: 9
12: 11: Parallel Thread: 6 Thread: 6
13: Parallel Thread: 11
9: Parallel Thread: 10
17: 16: Parallel Thread: 5 Thread: 5
14: Parallel Thread: 8
22: 21: Parallel Thread: 7 Thread: 7
29: 28: Parallel Thread: 10 Thread: 10
24: 23: 3: 2: 0: Parallel Thread: 1 Thread: 1 Thread: 1 Thread: 1 Thread: 1
19: 18: Parallel Thread: 4 Thread: 4
4. 使用Parallel.ForEach()方法循环
Parallel.ForEach()方法遍历实现了IEnumerable的集合,其方式类似于foreach语句,但以异步方式遍历。这里也没有确定遍历顺序。
static void ParallelForEach()
{
string[] data = { "zero","one","two","three","four","five","six","seven","eight","nine","ten","eleven","twelve"};
ParallelLoopResult result = Parallel.ForEach<string>(data,s=> {
Console.WriteLine($"{s}: thread {Thread.CurrentThread.ManagedThreadId} task {Task.CurrentId}");
});
输入结果:
Start thread: 1, Is background thrread:False
zero: thread 1 task 1
two: thread 4 task 2
one: thread 1 task 1
three: thread 4 task 2
six: thread 4 task 2
seven: thread 4 task 2
four: thread 1 task 1
eight: thread 4 task 2
five: thread 1 task 1
eleven: thread 1 task 1
nine: thread 4 task 2
ten: thread 5 task 3
twelve: thread 1 task 1
如果需要中断循环,就可以使用ForEach()方法的重载版本和ParallelLoopState参数。其方式与前面的For()方法相同。ForEach()方法的一个重载版本也可以用于访问索引器,从而获得迭代次数,如下所示:
static void ParallelForEach()
{
string[] data = { "zero","one","two","three","four","five","six","seven","eight","nine","ten","eleven","twelve"};
//ParallelLoopResult result = Parallel.ForEach<string>(data,s=> {
// Console.WriteLine($"{s}: thread {Thread.CurrentThread.ManagedThreadId} task {Task.CurrentId}");
//});
Parallel.ForEach(data,(s,pls,i)=>
{
Console.WriteLine($"content: {s},Index: {i}");
});
}
输出结果:
Start thread: 1, Is background thrread:False
content: zero,Index: 0
content: one,Index: 1
content: two,Index: 2
content: three,Index: 3
content: four,Index: 4
content: six,Index: 6
content: seven,Index: 7
content: five,Index: 5
content: eight,Index: 8
content: nine,Index: 9
content: ten,Index: 10
content: eleven,Index: 11
content: twelve,Index: 12
5. 通过Parallel.Invoke()方法调用多个方法
如果多个任务将并行运行,就可以使用Parallel.Invoke()方法,它提供了任务并行性模式。Prallel.Invoke()方法允许传递一个Action委托的数组,在其中可以指定将运行的方法。示例代码传递了要并行调用的Foo()和Bar()方法:
static void ParallelInvoke()
{
Parallel.Invoke(Foo,Bar);
//or
//Parallel.Invoke(new Action[] { Foo,Bar});
}
static void Foo() => Console.WriteLine("Foo");
static void Bar() => Console.WriteLine("Bar");
输出结果:
Foo
Bar
Parallel类使用起来十分方便,而且既可以用于任务,又可以用于数据并行性。如果需要更细致的控制,并且不想等到Parallel类结束后再开始动作,就可以使用Task类。当然,结合使用Task类和Parallel类也是可以的。