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();
        }

输出:
C#之:并行编程 - 1_第1张图片
可以看到,工作代码并没有按照数组的索引次序进行遍历。这是因为我们遍历是并行的,不是顺序的。所以,如果我们输出必须是同步的或者必须是顺序输出的,则不应使用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();
        }

输出:
C#之:并行编程 - 1_第2张图片
从输出可以看出,有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();
        }

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

提前停止Parallel.For :
可以提前中断 Parallel.For 方法,而不是完成所有迭代。 For()方法的一个重载版本接受第三个 Action 类型的参数。使用这些参数定义一个方法,就可以调用 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();
        }

输出:
C#之:并行编程 - 1_第4张图片
输出运行说明:迭代 在值大于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 < T L o c a l > ( ) Parallel.For<TLocal>() Parallel.For<TLocal>() 方法。除了 form 和 to 对应的值之外,For方法的泛型版本还接受3个委托参数。
第一个参数类型是: F u n c < T L o c a l > Func <TLocal> Func<TLocal> ,下面例子对TLocal使用字符串,所以该方法需要定义为 F u n c < S t r i n g > Func<String> Func<String> , 即返回string 的方法。这个方法仅对于执行迭代的每个线程调用一次。

第二个参数为循环体定义了委托。在示例中,该参数的类型是 F u n c < 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 > Func<int,ParallelLoopState,string,string> Func<int,ParallelLoopState,string,string> 。其中,第一个参数是循环迭代,第二个参数是允许停止循环。循环体方法通过第三个参数接受从 inint 方法返回的值,循环体方法还需要返回一个值,其类型用于泛型For方法的参数定义。

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

 static void Main(string[] args)
        {
            Parallel.For(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();
        }

输出:
C#之:并行编程 - 1_第5张图片

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(data, s =>
            {
                Console.WriteLine(s);
            });
            Console.ReadKey();
        }

输出:
C#之:并行编程 - 1_第6张图片
如果需要中断循环,就可以使用 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(data, (s,pls,i)=>
            {
                Console.WriteLine("{0},序号:{1}",s,i);
            });
            Console.ReadKey();
        }

输出:
C#之:并行编程 - 1_第7张图片
Dome2:实例运用:

     static void Main(string[] args)
        {
            List ls1 = new List() { 1, 1, 1, 1, 1, 1 };
            List ls2 = new List() { 2, 2, 2, 2, 2, 2 };
            Function(new List>() { 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> listNum, int n)
        {
            Parallel.ForEach(listNum, list =>
            {
                for (int i = 0; i < list.Count; i++)
                {
                    list[i] *= n;
                }
            });
        }

输出:
C#之:并行编程 - 1_第8张图片

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

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

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

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

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

       void RotateMatrices(IEnumerable 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(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(int fromInclusive, int toExclusive, Func localInit, Func body, Action 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;
            },

下一章继续… …

你可能感兴趣的:(C#,多线程,并行和异步,C#,多线程)