C#之:并行编程 - 1

并行编程简介:

如果程序中有大量的计算任务,并且这些任务能分割成几个互相独立的任务块,那就应
该使用并行编程。并行编程可临时提高CPU 利用率,以提高吞吐量,若客户端系统中的
CPU 经常处于空闲状态,这个方法就非常有用,但通常并不适合服务器系统。大多数服
务器本身具有并行处理能力,例如ASP.NET 可并行地处理多个请求。某些情况下,在服
务器系统中编写并行代码仍然有用(如果你知道并发用户数量会一直是少数)。但通常情
况下,在服务器系统上进行并行编程,将降低本身的并行处理能力,并且不会有实际的
好处。

并行的两种形式:

  • 数据并行(data parallelism)。数据并行是指有大量的数据需要处理,并且每一块数据的处理过程基本上是彼此独立的。
  • 任务并行(task parallelim)。任务并行是指需要执行大量任务,并且每个任务的执行过程基本上是彼此独立的。任务并行可以是动态的,如果一个任务的执行结果会产生额外的任务,这些新增的任务也可以加入任务池。

实现数据并行有几种不同的做法:

  1. 一种做法是使用Parallel.ForEach 方法,它类似于foreach 循环,应尽可能使用这种做法。
  2. 另一种做法是使用PLINQ(Parallel LINQ), 它为LINQ 查询提供了AsParallel 扩展。

不管选用哪种方法,在并行处理时有一个非常重要的准则。每个任务块要尽可能的互相独立。
只要任务块是互相独立的,并行性就能做到最大化。一旦你在多个线程中共享状态,就必
须以同步方式访问这些状态,那样程序的并行性就变差了。

任务并行:
数据并行重点在处理数据,任务并行则关注执行任务。

Parallel 类的Parallel.Invoke 方法可以执行“分叉/ 联合”(fork/join)方式的任务并行。现在Task 这个类也被用于异步编程,但当初它是为了任务并行而引入的。任务并行中使用的一个Task 实例表示一些任务。可以使用Wait 方法等待任务完成,还可以使用Result和Exception 属性来检查任务执行的结果。直接使用Task 类型的代码比使用Parallel 类要复杂,但是,如果在运行前不知道并行任务的结构,就需要使用Task 类型。如果使用动态并行机制,在开始处理时,任务块的个数是不确定的,只有继续执行后才能确定。通常情况下,一个动态任务块要启动它所需的所有子任务,然后等待这些子任务执行完毕。为实现这个功能,可以使用Task 类型中的一个特殊标志TaskCreationOptions.
AttachedToParent。
跟数据并行一样,任务并行也强调任务块的独立性。委托(delegate)的独立性越强,程序的执效率就越高。在编写任务并行程序时,要格外留意下闭包(closure)捕获的变量。记住闭包捕获的是引用(不是值),因此可以在结束时以不明显地方式地分享这些变量。

任务不要特别短,也不要特别长 如果任务太短,把数据分割进任务和在线程池中调度任务的开销会很大。如果任务太长,线程池就不能进行有效的动态调整以达到工作量的平衡

Parallel 类:

Parallel 类是对线程的一个很好抽象。该类位于:System.Threading.Tasks 命名空间中,提供了数据和任务的并行性。
Parallel 类定义了并行的 for 和 foreach 的静态方法。对于C# 中 for 和 foreach 语句而言,循环从一个线程中运行。Parallel 使用多个任务,因此使用多个线程来完成这个作业。

官方链接:Parallel

Parallel.For()方法:

主要是针对处理数组元素的并行操作。

Dome1
第一个参数是开始索引(含)。第二个参数是结束索引(不含)。第三个参数是 Action委托。整型参数是循环的迭代次数,该参数被传递给委托引用的方法。

 static void Main(string[] args)
        {
            int[] nums = { 1, 2, 3, 4, 5 };
            Parallel.For(0, nums.Length, (i) => {
                Console.WriteLine("针对数组索引 {0} 对应元素{1} 的一些工作代码... ...",i,nums[i]);
            });

            Console.ReadKey();
        }

输出:
在这里插入图片描述
可以看到,工作代码并没有按照数组的索引次序进行遍历。这是因为我们遍历是并行的,不是顺序的。所以,如果我们输出必须是同步的或者必须是顺序输出的,则不应使用Parallel的方式。

Dome2:
Parallel.For()方法的返回类型是 ParallelLoopResult 结构体,它提供了循环是否结束的信息。

 static void Main(string[] args)
        {
            ParallelLoopResult result = Parallel.For(0, 10, i =>
            {
                Console.WriteLine("序号:{0},任务:{1},线程:{2}",i,Task.CurrentId,Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(10);
            });

            Console.WriteLine("是否完成所有任务:{0}",result.IsCompleted);

            Console.ReadKey();
        }

输出:
在这里插入图片描述
从输出可以看出,有5个任务和5个线程。任务不一定映射到一个线程上,线程也可以被不同的任务重用。

Dome3,异步等待:
Task.Delay(); 是一个异步延迟等待方法,用于释放线程提供其他任务使用。下面的代码使用 await 关键字,所以一旦完成延迟,就立即开始调用这些代码。延迟后执行的代码和延迟前执行的代码可以运行在不同的线程中。

      static void Main(string[] args)
        {
            //async:异步方法前加关键字
            ParallelLoopResult result = Parallel.For(0, 10, async i =>
            {
                Console.WriteLine("序号:{0},任务:{1},线程:{2}", i, Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
                await  Task.Delay(10);
                Console.WriteLine("序号:{0},任务:{1},线程:{2}", i, Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
            });

            Console.WriteLine("是否完成所有任务:{0}", result.IsCompleted);

            Console.ReadKey();
        }

输出:
在这里插入图片描述
从输出结果可以看出,调用 Task.Delay() 方法后,线程发生了变化。例如:在循环迭代序号2 在延迟前线程ID是 4,在延迟后线程ID是9。还可以看到任务不存在,只留下线程了。而且这里重用了前面的线程。
另一个重要方面是,Parallel类的 for 方法并没有等待延迟,而是直接完成。Parallel 类只等待他创建的任务,而不等待其他后台活动。在延迟后,也有可能完全看不到方法的输出。出现这种情况的原因是主线程(前台线程)结束。所有的后台线程被终止。

提前停止Parallel.For :
可以提前中断 Parallel.For 方法,而不是完成所有迭代。 For()方法的一个重载版本接受第三个 Action<int, ParallelLoopState> 类型的参数。使用这些参数定义一个方法,就可以调用 ParallelLoopState 的 Break() 或 Stop()方法,以影响循环结果。

static void Main(string[] args)
        {
            //async:异步方法前加关键字
            ParallelLoopResult result = Parallel.For(10, 40, async (int i,ParallelLoopState pls) =>
            {
                Console.WriteLine("序号:{0},任务:{1},线程:{2}", i, Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
                await Task.Delay(10);
                if(i>15)
                {
                    pls.Break();
                }
            });

            Console.WriteLine("是否完成所有任务:{0}", result.IsCompleted);
            Console.WriteLine("最后结束迭代索引:{0}",result.LowestBreakIteration);
            Console.ReadKey();
        }

输出:
在这里插入图片描述
输出运行说明:迭代 在值大于15时中断,但其他任务可以同时运行,有其他值的任务也可以运行。利用 LowestBreakIteration 属性,可以忽略其他任务结果。

Break():告知 System.Threading.Tasks.Parallel 循环应在系统方便的时候尽早停止执行当前迭代之外的迭代。
Stop():告知 System.Threading.Tasks.Parallel 循环应在系统方便的时候尽早停止执行。

使用Break()方法每次停止迭代的索引都不相同。因为这是有系统决定的。

Parallel.For()的重载Dome:

Parallel.For() 方法可以使用几个线程来执行循环,如果需要对每个线程进行初始化,就可以使用 P a r a l l e l . F o r &lt; T L o c a l &gt; ( ) Parallel.For&lt;TLocal&gt;() Parallel.For<TLocal>() 方法。除了 form 和 to 对应的值之外,For方法的泛型版本还接受3个委托参数。
第一个参数类型是: F u n c &lt; T L o c a l &gt; Func &lt;TLocal&gt; Func<TLocal> ,下面例子对TLocal使用字符串,所以该方法需要定义为 F u n c &lt; S t r i n g &gt; Func&lt;String&gt; Func<String> , 即返回string 的方法。这个方法仅对于执行迭代的每个线程调用一次。

第二个参数为循环体定义了委托。在示例中,该参数的类型是 F u n c &lt; i n t , P a r a l l e l L o o p S t a t e , s t r i n g , s t r i n g &gt; Func&lt;int,ParallelLoopState,string,string&gt; Func<int,ParallelLoopState,string,string> 。其中,第一个参数是循环迭代,第二个参数是允许停止循环。循环体方法通过第三个参数接受从 inint 方法返回的值,循环体方法还需要返回一个值,其类型用于泛型For方法的参数定义。

For()方法的最后一个参数指定一个委托 A c t i o n &lt; T L o c a l &gt; Action&lt;TLocal&gt; Action<TLocal>,在下例中,接受一个字符串。这个方法对于每个线程调用一次,这是一个线程退出的方法。

 static void Main(string[] args)
        {
            Parallel.For<string>(0, 20, () => {
                //为每个线程调用一次,必须有返回值
                Console.WriteLine("初始化 线程:{0};任务:{1}",Thread.CurrentThread.ManagedThreadId,Task.CurrentId);
                return string.Format("Thread{0}", Thread.CurrentThread.ManagedThreadId);
            }, (i, pls, str1) => {
                //每个数字调用一次,必须有返回值
                Console.WriteLine("body i {0}; str1 {1}; thread {2}; task {3}",i,str1,Thread.CurrentThread.ManagedThreadId,Task.CurrentId);
                Thread.Sleep(10);
                return string.Format("i {0}", i);
            }, (str1) => {
                //对每个线程的最后操作
                Console.WriteLine("最后:{0}",str1);
            });


            Console.ReadKey();
        }

输出:
在这里插入图片描述

Parallel.ForEach()方法:

Parallel.ForEach()方法 遍历实现了IEnumerable 的集合,其方式 类似于 foreach 语句,但以异步方式遍历。这里也没有确定遍历顺序。

Dome1:

 static void Main(string[] args)
        {
            string[] data = { "A", "B", "C", "D", "E", "F", "G", "J", "K", "L", "M", "N" };
            ParallelLoopResult result = Parallel.ForEach<string>(data, s =>
            {
                Console.WriteLine(s);
            });
            Console.ReadKey();
        }

输出:
在这里插入图片描述
如果需要中断循环,就可以使用 ForEach()方法的重载版本和 ParallelLoopState参数。其方式与前面的For()方法相同。Foreach()方法的一个重载版本也可以用于索引器,从而获得迭代次数。

 static void Main(string[] args)
        {
            string[] data = { "A", "B", "C", "D", "E", "F", "G", "J", "K", "L", "M", "N" };
            ParallelLoopResult result = Parallel.ForEach<string>(data, (s,pls,i)=>
            {
                Console.WriteLine("{0},序号:{1}",s,i);
            });
            Console.ReadKey();
        }

输出:
在这里插入图片描述
Dome2:实例运用:

     static void Main(string[] args)
        {
            List<int> ls1 = new List<int>() { 1, 1, 1, 1, 1, 1 };
            List<int> ls2 = new List<int>() { 2, 2, 2, 2, 2, 2 };
            Function(new List<List<int>>() { ls1, ls2 }, 2);

            foreach (var item in ls1)
            {
                Console.Write(item+",");
            }
            Console.WriteLine();
            foreach (var item in ls2)
            {
                Console.Write(item + ",");
            }

            Console.ReadKey();
        }

        static void Function(IEnumerable<List<int>> listNum, int n)
        {
            Parallel.ForEach(listNum, list =>
            {
                for (int i = 0; i < list.Count; i++)
                {
                    list[i] *= n;
                }
            });
        }

输出:
在这里插入图片描述

下面的例子使用了一批矩阵,对每一个矩阵都进行旋转:

void RotateMatrices(IEnumerable<Matrix> matrices, float degrees)
{
    Parallel.ForEach(matrices, matrix => matrix.Rotate(degrees));
}

在某些情况下需要尽早结束这个循环,例如发现了无效值时。下面的例子反转每一个矩
阵,但是如果发现有无效的矩阵,则中断循环:

 void InvertMatrices(IEnumerable<Matrix> matrices)
        {
            Parallel.ForEach(matrices, (matrix, state) =>
            {
                if (!matrix.IsInvertible)
                    state.Stop();
                else
                    matrix.Invert();
            });
        }

更常见的情况是可以取消并行循环,这与结束循环不同。结束(stop)循环是在循环内部
进行的,而取消(cancel)循环是在循环外部进行的。例如,点击“取消”按钮可以取消
一个CancellationTokenSource,以取消并行循环,方法如下:

       void RotateMatrices(IEnumerable<Matrix> matrices, float degrees,CancellationToken token)
        {
            Parallel.ForEach(matrices,new ParallelOptions { CancellationToken = token }, matrix => matrix.Rotate(degrees));
        }

Parallel的注意点:

在 Parallel for 或 Foreach 方法的一些复杂方法应用中。我们可以在每个任务启动时执行初始化操作,在每个任务结束后,又执行一些后续工作,同时,还允许我们监视线程的状态。

如下:

 		static void Main(string[] args)
        {
            int[] nums = new int[] { 1, 2, 3, 4 };
            int total = 0;
            Parallel.For<int>(0, nums.Length, () =>
            {
                return 1;
            }, (i, state, subTotal) =>
            {
                subTotal += nums[i];
                return subTotal;
            },x=> {
                Interlocked.Add(ref total, x);
            });
            Console.WriteLine("total={0}",total);     
            Console.ReadKey();
        }

这段代码有可能输出11,也许时12,理论上输出13或14。为什么会这样输出,首先来了解一下For方法的各个参数:

 public static ParallelLoopResult For<TLocal>(int fromInclusive, int toExclusive, Func<TLocal> localInit, Func<int, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally);

前面两个参数时:fromInclusive(起始索引) 和 toExclusive(结束索引)。
参数:body;即任务的本身,其中 subTotal 是单位个任务的返回值。
localInitlocalFinally 比较难以理解。要理解这两个参数,首先要理解 Parallel.For 方法的运作模式。For 是采用并发的方式来启动循环体中的每个任务,这意味着,任务是交给线程池管理的。在上面的代码中循环次数共计 4 次,实际允许时调度的后台线程也许就只有一个或两个。这是并发的优势,也是线程池的优势,Parallel 通过内部的算法,最大的节约了线程的消耗。localInit的作用是 ,如果Parallel 为我们新起了一个线程时,它就会执行一些初始化的任务。在上面的例子中:

			() =>
            {
                return 1;
            },

他会将任务体中的 subTotal 这个值初始化为 1。

localFinally 的作用是,在每个线程结束时,它执行一些收尾工作:

 Interlocked.Add(ref total,  x);

这行代码所代表的收尾工作就是:

total = total+ subTotal ;

其中的 x ,其实代表的就是任务体中的返回值,具体在这个例子中就是 subTotal 在返回时的值。使用 Interlocked 对 total 进行原子操作,以避免并发带来的问题。
现在,应该很好理解上面的例子输出不确定了。 Parallel 一共启动了4个任务,但不能确定 Parallel 启动了多少个线程,那是运行时根据自己的算法决定的。如果所有的并发任务只用了一个线程,则输出为 11 ; 如果用来两个线程,那么根据程序逻辑来看,输出就是12。
在这段代码中,如果让 localInit 的返回值为 0 也许就永远不会注意到这个问题。

		() =>
            {
                return 0;
            },

下一章继续… …

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
内容简介 您想淋漓尽致地发挥多核计算机系统的处理能力吗?《C#并行编程高级教程:精通NET 4 Parallel Extensions》将帮助您实现这一夙愿。这本精品书籍浓墨重彩地描述如何使用C# 4、Visual Studio 2010和.NET Framework 4高效地创建基于任务的并行应用程序,详细讲述最新的单指令、多数据流指令和向量化等并行编程技术,介绍现代并行库,讨论如何珠联璧合地使用高级Intel工具与C#,并指导您巧妙使用新引入的轻型协调结构来开发自己的解决方案并解决最棘手的并发编程问题。 主要内容 ◆介绍如何基于新Task Parallel Library和.NET 4设计稳定的可扩展并行应用程序。 ◆讲解命令式数据并行、命令式任务并行、并发集合以及协调数据结构。 ◆描述PLINQ高级声明式数据并行。 ◆讨论如何使用新的Visual Studio 2010并行调试功能来调试匿名方法、任务和线程。 ◆演示如何对数据源进行分区,以便在不同任务和线程之间合理分配工作负荷。 作者简介 Caston C.Hillar是一位独立软件咨询师,自1997年起便一直从事并行编程、多处理器和多核领域的研究,Gaston拥有使用C#和.NET Framework来设计和开发各种复杂并行解决方案的丰富经验,曾于2009年荣膺Intel Black Belt Software Developer奖。 目录 第1章 基于任务的程序设计 1.1 使用共享内存的多核系统 1.1.1 共享内存多核系统与分布式内存系统之间的区别 1.1.2 并行程序设计和多核程序设计 1.2 理解硬件线程和软件线程 1.3 理解Amdahl法则 1.4 考虑Gustafson法则 1.5 使用轻量级并发模型 1.6 创建成功的基于任务的设计 1.6.1 以并发的思想指导设计 1.6.2 理解交错并发、并发和并行之间的区别 1.6.3 并行化任务 1.6.4 尽量减少临界区 1.6.5 理解多核并行程序的设计原则 1.7 为NUMA架构和更高的可扩展性做好准备 1.8 判断是否适合并行化 1.9 小结 第2章 命令式数据并行 2.1 加载并行任务 2.1.1 System.Threading.Tasks.Parallel类 2.1.2 Parallel.Invoke 2.2 将串行代码转换为并行代码 2.2.1 检测可并行化的热点 2.2.2 测量并行执行的加速效果 2.2.3 理解并发执行 2.3 循环并行化 2.3.1 Parallel.For 2.3.2 Parallel.ForEach 2.3.3 从并行循环中退出 2.4 指定并行度 2.4.1 ParallelOptions 2.4.2 计算硬件线程 2.4.3 逻辑内核并不是物理内核 2.5 通过甘特图检测临界区 2.6 小结 第3章 命令式任务并行 3.1 创建和管理任务 3.1.1 System.Threading.Tasks.Task 3.1.2 理解Task状态和生命周期 3.1.3 通过使用任务来对代码进行并行化 3.1.4 等待任务完成 3.1.5 忘记复杂的线程 3.1.6 通过取消标记取消任务 3.1.7 从任务返回值 3.1.8 TaskCreationOptions 3.1.9 通过延续串联多个任务 3.1.10 编写适应并发和并行的代码 3.2 小结 第4章 并发集合 4.1 理解并发集合提供的功能 4.1.1 System.Collections.Concurrent 4.1.2 ConcurrentQueue 4.1.3 理解并行的生产者-消费者模式 4.1.4 ConcurrentStack 4.1.5 将使用数组和不安全集合的代码转换为使用并发集合的代码 4.1.6 ConcurrentBag 4.1.7 IProducerConsumerCollection 4.1.8 BlockingCollection 4.1.9 ConcurrentDictionary 4.2 小结 第5章 协调数据结构 5.1 通过汽车和车道理解并发难题 5.1.1 非预期的副作用 5.1.2 竞争条件 5.1.3 死锁 5.1.4 使用原子操作的无锁算法 5.1.5 使用本地存储的无锁算法 5.2 理解新的同步机制 5.3 使用同步原语 5.3.1 通过屏障同步并发任务 5.3.2 屏障和ContinueWhenAll 5.3.3 在所有的参与者任务中捕捉异常 5.3.4 使用超时 5.3.5 使用动态数目的参与者 5.4 使用互斥锁 5.4.1 使用Monitor 5.4.2 使用锁超时 5.4.3 将代码重构为避免使用锁 5.5 将自旋锁用作互斥锁原语 5.5.1 使用超时 5.5.2 使用基于自旋的等待 5.5.3 自旋和处理器出让 5.5.4 使用volatile修饰符 5.6 使用轻量级的手动重置事件 5.6.1 使用ManualResetEventSlim进行自旋和等待 5.6.2 使用超时和取消 5.6.3 使用ManualResetEvent 5.7 限制资源的并发访问 5.7.1 使用SemaphoreSlim 5.7.2 使用超时和取消 5.7.3 使用 Semaphore 5.8 通过CountdownEvent简化动态fork和join场景 5.9 使用原子操作 5.10 小结 第6章 PLINQ:声明式数据并行 6.1 从LINQ转换到PLINQ 6.1.1 ParallelEnumerable及其AsParallel方法 6.1.2 AsOrdered和orderby子句 6.2 指定执行模式 6.3 理解PLINQ中的数据分区 6.4 通过PLINQ执行归约操作 6.5 创建自定义的PLINQ聚合函数 6.6 并发PLINQ任务 6.7 取消PLINQ 6.8 指定所需的并行度 6.8.1 WithDegreeOfParallelism 6.8.2 测量可扩展性 6.9 使用ForAll 6.9.1 foreach和ForAll的区别 6.9.2 测量可扩展性 6.10 通过WithMergeOptions配置返回结果的方式 6.11 处理PLINQ抛出的异常 6.12 使用PLINQ执行MapReduce算法 6.13 使用PLINQ设计串行多步操作 6.14 小结 第7章 Visual Studio 2010的任务调试能力 7.1 充分利用多显示器的支持 7.2 理解并行任务调试器窗口 7.3 查看Parallel Stacks图 …… 第8章 线程池 第9章 异步编程模型 第10章 并行测试和调优 第11章 向量化、SIMD指令以及其他并行库 附录A .NET 4中与并行相关的类图 附录B 并发UML模型 附录C Parallel Extensions Extras

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值