设计模式——.net并行编程,清华大学出版的中译本。

  1. 相关资源地址主页面: http://parallelpatterns.codeplex.com/
  2. 代码下载: http://parallelpatterns.codeplex.com/releases/view/50473
  3. 书籍在线地址: https://msdn.microsoft.com/en-us/library/ff963553.aspx
  4. 使用并行编程的一些示例: https://code.msdn.microsoft.com/ParExtSamples
  5. Task的介绍页面: https://msdn.microsoft.com/en-us/library/system.threading.tasks.task(v=vs.110).aspx

这本书介绍了一些多线程编程的模式,也就是会使用多线程的场景,以及可以使用.net中的什么技术实现--当然主要是TPL(Task parallel Library)和PLINQ(parallel LINQ)。TPL是.NET Framework 4中加的功能,目的是封装以前的thread和同步,线程池等概念,大家只要使用task和System.Threading.Tasks.Parallel,提供并行任务,并行和同步的细节交给库处理。 PLINQ是LINQ to Objects的并行版本。下面是这些结构的一个概览。

1. 模式分类

  • 数据并行data parallelism。对不同的数据执行相同的计算——比如for循环中的计算,这是属于数据并行。 包括的模式有并行循环(parallel loops)和并行聚合(parallel aggregation)。并行循环强调并行循环之间没有数据依赖,不用控制并行顺序;并行聚合类似于map-reduce,任务有并发的部分,但数据也有需要控制同步的地方,.net提供的库很好的封装了同步,使用起来就像不需要同步一样简单。
  • 任务并行task parallelism。强调并行执行的任务不同,一般任务的输入数据也不同。 包括的模式有并行任务parallel tasks、future模式、动态任务并行(dynamic task parallelism)、流水线pipelines。

2. 并行循环

当需要对集合中的每个元素执行相同的独立操作时,可以使用并行循环模式,注意循环需要相互独立。
a. 一般情况。例如我们可能有这么一个for循环

int n = ...
for (int i = ; i < n; i++)
{
// do some task
}

对应的并行版本:

int n = ...
Parallel.For(, n, i =>
{
// ...
});

Foreach也有相应的并行版本

IEnumerable<MyObject> myEnumerable = ... 

        foreach (var obj in myEnumerable)
{
// ...
} IEnumerable<MyObject> myEnumerable = ... Parallel.ForEach(myEnumerable, obj =>
{
// ...
});

PLINQ多线程的例子

IEnumerable<MyObject> source = ... 

        // LINQ
var query1 = from i in source select Normalize(i); // PLINQ
var query2 = from i in source.AsParallel()
select Normalize(i);

b. 控制循环过程。可以在任务执行过程中控制其它并行任务,可以使用break,stop和cancel。一般stop和cancel使用比较常见。

i. 中断循环break。类似于for循环的break。注意break之后,依然会执行比break 的task的索引值小的任务,而且会保证所有索引值小的任务都会执行。如果索引值大的任务在break前开始执行,也会执行完毕。这种方式适用于任务顺序有依赖的情况,需要保证中断前的任务执行完毕。

int n = ...
for (int i = ; i < n; i++)
{
// ...
if (/* stopping condition is true */)
break;
}

采用多线程版本时可以这样退出循环:

int n = ...
Parallel.For(, n, (i, loopState) =>
{
// ...
if (/* stopping condition is true */)
{
loopState.Break();
return;
}
});

签名:

Parallel.For(int fromInclusive,
int toExclusive,
Action<int, ParallelLoopState> body);

检查task状态是不是中断退出的方法:

int n = ...
var result = new double[n]; var loopResult = Parallel.For(, n, (i, loopState) =>
{
if (/* break condition is true */)
{
loopState.Break();
return;
}
result[i] = DoWork(i);
}); if (!loopResult.IsCompleted &&
loopResult.LowestBreakIteration.HasValue)
{
Console.WriteLine("Loop encountered a break at {0}",
loopResult.LowestBreakIteration.Value);
}

ii. 中断停止stop。类似于break,不同的是stop以后不会再执行索引值小的任务,也就是正在执行的执行完毕,其它的就不再执行。任务之间完全没有依赖,只要有一个任务stop,那么不再调度剩下的任务。

var n = ...
var loopResult = Parallel.For(, n, (i, loopState) =>
{
if (/* stopping condition is true */)
{
loopState.Stop();
return;
}
result[i] = DoWork(i);
}); if (!loopResult.IsCompleted &&
!loopResult.LowestBreakIteration.HasValue)
{
Console.WriteLine(“Loop was stopped”);
}

iii. 外部循环取消。对于一些执行时间比较长的任务,可以使用取消操作。任务执行期间检查是否取消的标识。

            void DoLoop(CancellationTokenSource cts)
{
int n = ...
CancellationToken token = cts.Token; var options = new ParallelOptions
{ CancellationToken = token }; try
{
Parallel.For(, n, options, (i) =>
{
// ... // ... optionally check to see if cancellation happened
if (token.IsCancellationRequested)
{
// ... optionally exit this iteration early
return;
}
});
}
catch (OperationCanceledException ex)
{
// ... handle the loop cancellation
}
}

函数签名:

Parallel.For(int fromInclusive,
int toExclusive,
ParallelOptions parallelOptions,
Action<int> body);

问题:如果使用cancel,是否还会调度执行剩下的任务?

Cancel以后,如果任务还没有开始执行的会直接取消,已经开始的由任务自己决定是否需要取消。

c. 异常处理。如果有一个任务中抛出了异常,后面不会再调度新的任务,已经调度的会执行完成。最后所有任务可能的异常会打包放在一个异常AggregationException里面抛出来。

d. 分批执行小循环体。有的循环体执行时间较少,如果每次循环都调度一个任务,显然得不偿失。可以讲循环的执行过程分区,比如每100个循环调度一次。下面的例子根据cpu的核数自动分配每批任务的数量。

int n = ...
double[] result = new double[n];
Parallel.ForEach(Partitioner.Create(, n),
(range) =>
{
for (int i = range.Item1; i < range.Item2; i++)
{
// very small, equally sized blocks of work
result[i] = (double)(i * i);
}
});

函数签名:

Parallel.ForEach<TSource>(
Partitioner<TSource> source,
Action<TSource> body);

下面的设置每个任务执行50000个循环。

double[] result = new double[];
Parallel.ForEach(Partitioner.Create(, , ),
(range) =>
{
for (int i = range.Item1; i < range.Item2; i++)
{
// small, equally sized blocks of work
result[i] = (double)(i * i);
}
});

这里System.Collections.Concurrent.Partitioner将区间切割成IEnumerable<Tuple<int,int>>的形式。
e. 控制并行度。一般TPL会自动根据CPU内核数控制同时执行的任务数,你也可以通过ParallelOption的MaxDegreeOfParallelism来控制最大的并行任务数。

var n = ...
var options = new ParallelOptions()
{ MaxDegreeOfParallelism = };
Parallel.For(, n, options, i =>
{
// ...
});

函数签名:

Parallel.For(int fromInclusive,
int toExclusive,
ParallelOptions parallelOptions,
Action<int> body);

PLINQ使用示例:

IEnumerable<T> myCollection = // ...
myCollection.AsParallel()
.WithDegreeOfParallelism()
.ForAll(obj => /* ... */);

f. 在循环体中使用局部任务状态

int numberOfSteps = ;
double[] result = new double[numberOfSteps]; Parallel.ForEach( Partitioner.Create(, numberOfSteps), new ParallelOptions(), () => { return new Random(MakeRandomSeed()); }, (range, loopState, random) =>
{
for (int i = range.Item1; i < range.Item2; i++)
result[i] = random.NextDouble();
return random;
}, _ => {});

函数签名:

ForEach<TSource, TLocal>(
OrderablePartitioner<TSource> source,
ParallelOptions parallelOptions,
Func<TLocal> localInit,
Func<TSource, ParallelLoopState, TLocal, TLocal> body,
Action<TLocal> localFinally)

3. 并行任务

如果有多个一步任务可以同时执行,可以使用并行任务模式。例如

Parallel.Invoke(DoLeft, DoRight);

等价于下面的方法:

Task t1 = Task.Factory.StartNew(DoLeft);
Task t2 = Task.Factory.StartNew(DoRight); Task.WaitAll(t1, t2);

a. 处理异常https://msdn.microsoft.com/en-us/library/dd997415(v=vs.110).aspx)。使用Wait和WaitAll可以观察任务抛出来的异常,WaitAny不能。收到的异常会包装到AggregateException里面,可以使用Handle方法来处理里面的异常。

        try
{
Task t = Task.Factory.StartNew( ... );
// ...
t.Wait();
}
catch (AggregateException ae)
{
ae.Handle(e =>
{
if (e is MyException)
{
// ... handle exception ...
return true;
}
else
{
return false;
}
});
}

由于异常有可能嵌套其它的异常,形成一个多级的树结构,可以使用Flatten压平树结构,然后调用handle,可以保证聚合的所有异常都可以被处理。

try
{
Task t1 = Task.Factory.StartNew(() =>
{
Task t2 = Task.Factory.StartNew(() =>
{
// ...
throw new MyException();
});
// ...
t2.Wait();
});
// ...
t1.Wait();
}
catch (AggregateException ae)
{
ae.Flatten().Handle(e =>
{
if (e is MyException)
{
// ... handle exception ...
return true;
}
else
{
return false;
}
});
}

b. 等待第一个任务完成。可以使用WaitAny等待第一个任务完成。注意WaitAny不会观察到异常,后面加了WaitAll来处理异常。

var taskIndex = -; 

          Task[] tasks = new Task[]
{
Task.Factory.StartNew(DoLeft),
Task.Factory.StartNew(DoRight),
Task.Factory.StartNew(DoCenter)
};
Task[] allTasks = tasks; // Print completion notices one by one as tasks finish.
while (tasks.Length > )
{
taskIndex = Task.WaitAny(tasks);
Console.WriteLine("Finished task {0}.", taskIndex + );
tasks = tasks.Where((t) => t != tasks[taskIndex]).ToArray();
} // Observe any exceptions that might have occurred.
try
{
Task.WaitAll(allTasks);
}
catch (AggregateException ae)
{
...
}

下面的例子等待第一个完成的任务,然后取消其它任务,处理取消操作异常,其它的异常会重新抛出。

public static void SpeculativeInvoke(
params Action<CancellationToken>[] actions)
{
var cts = new CancellationTokenSource();
var token = cts.Token;
var tasks =
(from a in actions
select Task.Factory.StartNew(() => a(token), token))
.ToArray(); // Wait for fastest task to complete.
Task.WaitAny(tasks); // Cancel all of the slower tasks.
cts.Cancel(); // Wait for cancellation to finish and observe exceptions.
try
{
Task.WaitAll(tasks);
}
catch (AggregateException ae)
{
// Filter out the exception caused by cancellation itself.
ae.Flatten().Handle(e => e is OperationCanceledException);
}
finally
{
if (cts != null) cts.Dispose();
}
}

c. 新手易犯的错误

i. 闭包捕获的变量问题。考虑下面这段代码。

for (int i = ; i < ; i++)
{ // WARNING: BUGGY CODE, i has unexpected value
Task.Factory.StartNew(() => Console.WriteLine(i));
}

你可能希望输出数字1,2,3,4,只是打乱了顺序。实际上你很可能看到4,4,4,4. 因为几个Task其实访问了同一个变量i。可以使用下面的方法来避免这个问题。

for (int i = ; i < ; i++)
{
var tmp = i;
Task.Factory.StartNew(() => Console.WriteLine(tmp));
}

ii. 错误的时间清理任务所需资源的问题。考虑下面这段代码。

Task<string> t;
using (var file = new StringReader("text"))
{
t = Task<string>.Factory.StartNew(() => file.ReadLine());
}
// WARNING: BUGGY CODE, file has been disposed
Console.WriteLine(t.Result);

很可能任务执行的时候file已经dispose了。

d. 任务的生命周期

e. 任务调度机制。一个Task Scheduler的例子:"How to: Create a Task Scheduler That Limits the Degree of Concurrency."

4. 并行合并计算

类似于map-reduce,先并行计算出中间结果,然后合并得到最终结果。下面的例子先并发计算部分和,然后合并总和(注意同步)。

double[] sequence = ...
object lockObject = new object();
double sum = 0.0d; Parallel.ForEach(
// The values to be aggregated
sequence, // The local initial partial result
() => 0.0d, // The loop body
(x, loopState, partialResult) =>
{
return Normalize(x) + partialResult;
}, // The final step of each local context
(localPartialSum) =>
{
// Enforce serial access to single, shared result
lock (lockObject)
{
sum += localPartialSum;
}
});
return sum;

5. future模式

在一个任务结束后执行其他的任务。下面的例子使用ContinueWith和ContinueWhenAll实现任务的延续执行。

TextBox myTextBox = ...;

var futureB = Task.Factory.StartNew<int>(() => F1(a));
var futureD = Task.Factory.StartNew<int>(() => F3(F2(a))); var futureF = Task.Factory.ContinueWhenAll<int, int>(
new[] { futureB, futureD },
(tasks) => F4(futureB.Result, futureD.Result)); futureF.ContinueWith((t) =>
myTextBox.Dispatcher.Invoke(
(Action)(() => { myTextBox.Text = t.Result.ToString(); }))
);

6. 动态任务并行

动态添加任务。父子任务关系。下面是并行处理快速排序的例子。

static void ParallelQuickSort(int[] array, int from,
int to, int depthRemaining)
{
if (to - from <= Threshold)
{
InsertionSort(array, from, to);
}
else
{
int pivot = from + (to - from) / ;
pivot = Partition(array, from, to, pivot);
if (depthRemaining > )
{
Parallel.Invoke(
() => ParallelQuickSort(array, from, pivot,
depthRemaining - ),
() => ParallelQuickSort(array, pivot + , to,
depthRemaining - ));
}
else
{
ParallelQuickSort(array, from, pivot, );
ParallelQuickSort(array, pivot + , to, );
}
}
}

任务链与父子任务。创建任务时使用TaskCreationOptions.AttachedToParent可以建立父子任务关系,在子任务完成前,父任务结束运行时,会进入WaitingForChildrenToComplete的状态。

static void ParallelWalk2<T>(Tree<T> tree, Action<T> action)
{
if (tree == null) return;
var t1 = Task.Factory.StartNew(
() => action(tree.Data),
TaskCreationOptions.AttachedToParent);
var t2 = Task.Factory.StartNew(
() => ParallelWalk2(tree.Left, action),
TaskCreationOptions.AttachedToParent);
var t3 = Task.Factory.StartNew(
() => ParallelWalk2(tree.Right, action),
TaskCreationOptions.AttachedToParent);
Task.WaitAll(t1, t2, t3);
}

7. 流水线

利用并发队列实现并发任务顺次处理。下面是处理语句的例子,先读取字符,校正,输出。注意这里使用了容器BlockingCollection,实现了生产者消费者的同步。

int seed = ...
int BufferSize = ...
var buffer1 = new BlockingCollection<string>(BufferSize);
var buffer2 = new BlockingCollection<string>(BufferSize);
var buffer3 = new BlockingCollection<string>(BufferSize); var f = new TaskFactory(TaskCreationOptions.LongRunning,
TaskContinuationOptions.None); var stage1 = f.StartNew(() => ReadStrings(buffer1, ...));
var stage2 = f.StartNew(() => CorrectCase(buffer1, buffer2));
var stage3 = f.StartNew(() => CreateSentences(buffer2, buffer3));
var stage4 = f.StartNew(() => WriteSentences(buffer3)); Task.WaitAll(stage1, stage2, stage3, stage4);

读取字符

static void ReadStrings(BlockingCollection<string> output,
int seed)
{
try
{
foreach (var phrase in PhraseSource(seed))
{
Stage1AdditionalWork();
output.Add(phrase);
}
}
finally
{
output.CompleteAdding();
}
}

几种并行模式的比较

Await/async模式

  1. 相关资源合集: https://msdn.microsoft.com/library/hh191443.aspx
  2. 常见问题: http://blogs.msdn.com/b/pfxteam/archive/2012/04/12/10293335.aspx
  3. 解释由来的文章:Asynchronous Programming: Easier Asynchronous Programming with the New Visual Studio Async CTP
  4. 解释实现机制:Asynchronous Programming: Pause and Play with Await
  5. 解释** Asynchronous Programming: Understanding the Costs of Async and Await
  6. 解释: Await, SynchronizationContext, and Console Apps
  7. MSDN Magazine: http://blogs.msdn.com/b/msdnmagazine/

.NET并行编程1 - 并行模式的更多相关文章

  1. .NET 并行编程——任务并行

    本文内容 并行编程 任务并行 隐式创建和运行任务 显式创建和运行任务 任务 ID 任务创建选项 创建任务延续 创建分离的子任务 创建子任务 等待任务完成 组合任务 任务中的异常处理 取消任务 Task ...

  2. .NET 并行编程——数据并行

    本文内容 并行编程 数据并行 环境 计算 PI 矩阵相乘 把目录中的全部图片复制到另一个目录 列出指定目录中的所有文件,包括其子目录 最近,对多线程编程,并行编程,异步编程,这三个概念有点晕了,之前我 ...

  3. 一、并行编程 - 数据并行 System.Threading.Tasks.Parallel 类

    一.并行概念 1.并行编程 在.NET 4中的并行编程是依赖Task Parallel Library(后面简称为TPL) 实现的.在TPL中,最基本的执行单元是task(中文可以理解为"任 ...

  4. 【读书笔记】.Net并行编程(三)---并行集合

    为了让共享的数组,集合能够被多线程更新,我们现在(.net4.0之后)可以使用并发集合来实现这个功能.而System.Collections和System.Collections.Generic命名空 ...

  5. Delphi XE7并行编程: 并行For循环

    从Delphi XE7开始,引入了全新的并行编程库用于简化并行编程,它位于System.Threading单元中. 下面是一个判断素数的简单例子:function IsPrime (N: Intege ...

  6. .Net多线程 并行编程(三)---并行集合

    为了让共享的数组,集合能够被多线程更新,我们现在(.net4.0之后)可以使用并发集合来实现这个功能. 而System.Collections和System.Collections.Generic命名 ...

  7. C#并行编程中的Parallel.Invoke

    一.基础知识 并行编程:并行编程是指软件开发的代码,它能在同一时间执行多个计算任务,提高执行效率和性能一种编程方式,属于多线程编程范畴.所以我们在设计过程中一般会将很多任务划分成若干个互相独立子任务, ...

  8. Parallel并行编程

    Parallel并行编程 Parallel并行编程可以让我们使用极致的使用CPU.并行编程与多线程编程不同,多线程编程无论怎样开启线程,也是在同一个CPU上切换时间片.而并行编程则是多CPU核心同时工 ...

  9. C#并行编程

    C#并行编程中的Parallel.Invoke 一.基础知识 并行编程:并行编程是指软件开发的代码,它能在同一时间执行多个计算任务,提高执行效率和性能一种编程方式,属于多线程编程范畴.所以我们在设计过 ...

随机推荐

  1. sql server 中xml 数据类型的insert、update、delete

    近日对SQL操作XML作了如下整理: 1.插入 XML DECLARE @myDoc XMLSET @myDoc = '<Root> <ProductDescription Prod ...

  2. linux进程命令

    1. ps命令 命令 命令名称: 命令名称: ps 功能: 功能: 查询正在执行的进程 语法: 可选参数] 语法: ps [可选参数] 描述: 命令提供 命令提供Linux系统中正在发生的事情的   ...

  3. JSP基础——属性保存范围和request对象

    JSP属性保存范围 JSP中提供了四种属性保存范围,分别为page,request,session及application. 1.page范围,指设置的属性只在当前页面有效.通过pageContext ...

  4. 新冲刺Sprint3(第六天)

    一.Sprint介绍 商家功能模块继续完善着,加快了工作的步伐. 二.Sprint周期 看板: 燃尽图:

  5. 《Android深度探索HAL与驱动开发》第一章阅读心得

    首先了解到Android系统架构是由四层构成:其中第一层是Linux内核,他的作用是负责Linux的驱动程序以及内存.进程.电源等管理操作:第二层是C/C++代码库,也就是Linux下.so的文件:第 ...

  6. 编写安装中断7ch的中断例程:将一个以0结尾的字符串,转化为大写

    中断的安装程序: assume cs:code code segment start: mov ax, cs mov ds, ax mov si, offset chstr mov es, ax mo ...

  7. 合并果子 2004年NOIP全国联赛普及组

    时间限制: 1 s 空间限制: 128000 KB 题目等级 : 钻石 Diamond 题目描述 Description 在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆 ...

  8. Linux下的网卡驱动程序的编写过程(转)

    工作需要写了我们公司一块网卡的linux驱动程序.经历一个从无到有的过程,深感技术交流的重要.Linux作为挑战微软垄断的强有力武器,日益受到大家的喜爱.真希望她能在中国迅速成长.把程序文档贴出来,希 ...

  9. maven clean deploy -Pproduction

    今天我修改了公司的组件,要发布.然后腾飞告诉我用这个命令:clean deploy -Pproduction发布. 然后报了个401错误.(当时还是不知道401是什么错)正好经理在旁边问了一下,没想到 ...

  10. linux 关闭防火墙

    ) 重启后生效 开启: chkconfig iptables on 关闭: chkconfig iptables off ) 即时生效,重启后失效 开启: service iptables start ...