二、Parallel类

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类也是可以的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值