如果程序中有大量的计算任务,并且这些任务能分割成几个互相独立的任务块,那就应
该使用并行编程。并行编程可临时提高CPU 利用率,以提高吞吐量,若客户端系统中的
CPU 经常处于空闲状态,这个方法就非常有用,但通常并不适合服务器系统。大多数服
务器本身具有并行处理能力,例如ASP.NET 可并行地处理多个请求。某些情况下,在服
务器系统中编写并行代码仍然有用(如果你知道并发用户数量会一直是少数)。但通常情
况下,在服务器系统上进行并行编程,将降低本身的并行处理能力,并且不会有实际的
好处。
并行的两种形式:
实现数据并行有几种不同的做法:
不管选用哪种方法,在并行处理时有一个非常重要的准则。每个任务块要尽可能的互相独立。
只要任务块是互相独立的,并行性就能做到最大化。一旦你在多个线程中共享状态,就必
须以同步方式访问这些状态,那样程序的并行性就变差了。
任务并行:
数据并行重点在处理数据,任务并行则关注执行任务。
Parallel 类的Parallel.Invoke 方法可以执行“分叉/ 联合”(fork/join)方式的任务并行。现在Task 这个类也被用于异步编程,但当初它是为了任务并行而引入的。任务并行中使用的一个Task 实例表示一些任务。可以使用Wait 方法等待任务完成,还可以使用Result和Exception 属性来检查任务执行的结果。直接使用Task 类型的代码比使用Parallel 类要复杂,但是,如果在运行前不知道并行任务的结构,就需要使用Task 类型。如果使用动态并行机制,在开始处理时,任务块的个数是不确定的,只有继续执行后才能确定。通常情况下,一个动态任务块要启动它所需的所有子任务,然后等待这些子任务执行完毕。为实现这个功能,可以使用Task 类型中的一个特殊标志TaskCreationOptions.
AttachedToParent。
跟数据并行一样,任务并行也强调任务块的独立性。委托(delegate)的独立性越强,程序的执效率就越高。在编写任务并行程序时,要格外留意下闭包(closure)捕获的变量。记住闭包捕获的是引用(不是值),因此可以在结束时以不明显地方式地分享这些变量。
任务不要特别短,也不要特别长 如果任务太短,把数据分割进任务和在线程池中调度任务的开销会很大。如果任务太长,线程池就不能进行有效的动态调整以达到工作量的平衡
Parallel 类是对线程的一个很好抽象。该类位于:System.Threading.Tasks 命名空间中,提供了数据和任务的并行性。
Parallel 类定义了并行的 for 和 foreach 的静态方法。对于C# 中 for 和 foreach 语句而言,循环从一个线程中运行。Parallel 使用多个任务,因此使用多个线程来完成这个作业。
官方链接:Parallel
主要是针对处理数组元素的并行操作。
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
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 < 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();
}
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();
}
输出:
如果需要中断循环,就可以使用 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();
}
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;
}
});
}
下面的例子使用了一批矩阵,对每一个矩阵都进行旋转:
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 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 是单位个任务的返回值。
localInit 和 localFinally 比较难以理解。要理解这两个参数,首先要理解 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;
},
下一章继续… …