原文来自Alexandra Rusina在CSharpFAQ的Parallel Programming in .NET Framework 4: Getting Started

从这篇文章开始,我准备启动一个系列来讲述.NET Framework 4中的并行编程并介绍任务并行库(TPL)。

我必须承认在多线程或并行计算方面我并非专家。然而,人们总是询问我关于新特性的简单介绍和初学者的简单例子。而在这个领域相对于初学者来讲我有个巨大的优势----我可以询问开发这些类库的人我哪里做错了和我下一步该做什么。

更新:好吧,如果你想询问谁关于你的并行编程下一步该做什么,我推荐你去这个论坛:Parallel Extensions to the .NET Framework Forum。

此刻我有个简单目标。我想并行运行一个运行较久的控制台程序然后增加一个有响应的WPF界面。顺便说下,我并没有太专心的去测量性能。我试图表现一些常见的警告,但在大多数情况只是看到应用程序对我来讲运行得很快。

现在,开始这段旅程。这是我希望并行的小程序。SumRootN方法返回所有从1到1千万的整数的n次方根的总和,n是参数。在Main方法中,我为root参数传入从2到19来调用这个方法。我使用Stopwatch类来检查程序运行花费了多少毫秒。

using System.Threading.Tasks;
using System.Threading;
using System.Diagnostics;
using System;

class Program
{

static void Main(string[] args)
{
var watch = Stopwatch.StartNew();
for (int i = 2; i < 20; i++)
{
var result = SumRootN(i);
Console.WriteLine("root {0} : {1} ", i, result);
}
Console.WriteLine(watch.ElapsedMilliseconds);
Console.ReadLine();
}

public static double SumRootN(int root)
{
double result = 0;
for (int i = 1; i < 10000000; i++)
{
result += Math.Exp(Math.Log(i) / root);
}
return result;
}
}

在我的4GB内存3GHz64位双核计算机上,运行这段程序花费大概18秒。(译注:在我的单核老机器上花费大概43秒!)

既然我使用了for循环,那么要添加并行用Parallel.For方法是最容易的方式。我要做的所有事情便是将

for (int i = 2; i < 20; i++)
{
var result = SumRootN(i);
Console.WriteLine("root {0} : {1} ", i, result);
}

替换为下面的并行代码:

Parallel.For(2, 20, (i) =>
{
var result = SumRootN(i);
Console.WriteLine("root {0} : {1} ", i, result);
});

注意,只是一点点的代码改变了。我提供开始和结束的索引(跟我在简单循环里的一样)和一个lambda表达式形式的委托。我不需要做任何其他改变,而现在我的小程序花费大概9秒。(译注:现在我的单核老机器花费大概36秒。也比之前快了点,我猜是因为多个线程导致分配到CPU的几率变大的原因)

当你使用Parallel.For方法时,.NET Framework自动管理为循环服务的线程,所以你不需要自己做这些。但是记住,在两个处理器上运行并行代码并不保证代码运行一定会有两倍的速度。没有什么是凭空产生的;虽然你不需要自己管理线程,.NET Framework仍然在背后使用他们。那么当然,这会导致开销。事实上,如果你的操作是很简单很快的,你运行大量短小的并行循环,那么也许并行化上的收益将远低于你的期望值。

你可能注意到另外一件事情,当你运行现在的代码时,你看不到正确顺序的结果:不同于看到递增的root,你看到完全不同的画面。但我们假装我们只需要结果,不需要任何特定顺序。在这篇文章里,我将不会解决这个问题。

现在是时候做更进一步的事情。我不想写一个控制台程序;我想要些界面。所以我转换到Windows Presentation Foundation(WPF)。我已经创建了一个小窗口,上面只有一个启动按钮,一个显示结果的文本块,和一个显示过去了多少时间的标签。

顺序执行的事件处理代码看上去非常简单:

private void start_Click(object sender, RoutedEventArgs e)
{
textBlock1.Text = "";
label1.Content = "Milliseconds: ";

var watch = Stopwatch.StartNew();
for (int i = 2; i < 20; i++)
{
var result = SumRootN(i);
textBlock1.Text += "root " + i.ToString() + " " +
result.ToString() + Environment.NewLine;

}
var time = watch.ElapsedMilliseconds;
label1.Content += time.ToString();
}

编译并运行这个程序以确定一切都正常工作。正如你可能已经注意到的,界面被冻结而文本块在所有计算完成前并没有更新。这是一个说明为什么WPF推荐永远不要在界面线程执行长时间运行操作的好例子。

让我们把for循环改为并行计算:
 
Parallel.For(2, 20, (i) =>
{
var result = SumRootN(i);
textBlock1.Text += "root " + i.ToString() + " " +
result.ToString() + Environment.NewLine;

});

点击按钮...然而...得到一个InvalidOperationException异常“调用线程不能访问这个对象因为另一个不同的线程拥有它”。

发生了什么事?好吧,就像我之前提到的,任务并行库仍然使用线程。当你调用Parallel.For方法,.NET Framework自动启动新线程。我在控制台程序没有遇到问题是因为Console类是线程安全的。但是在WPF,界面组件只能被专门的界面线程安全的访问。既然Parallel.For使用与界面线程无关的工作线程,那么在循环体直接操作文本块是不安全的。如果说,你使用Windows Forms,你会得到另一个差不多的问题(另一个异常或程序崩溃)。

幸好,WPF提供一个API来解决这个问题。大多数控件提供一个特别的Dispatcher对象,用来允许其他线程通过发送异步消息作用于界面线程。(译注:Windows Forms上控件也有类似的实现,如BeginInvoke方法) 于是我们的并行循环事实上应该是这样:

Parallel.For(2, 20, (i) =>
{
var result = SumRootN(i);
this.Dispatcher.BeginInvoke(new Action(() =>
textBlock1.Text += "root " + i.ToString() + " " +
result.ToString() + Environment.NewLine)
, null);
});

在上面的代码中,我们使用Dispatcher来给界面线程发送委托。委托会在界面线程空闲时执行。如果界面忙于做什么其他的事情,委托会被放入一个队列。但是记住,与界面线程的这种交互可能会使你的程序变慢些。

现在我在我计算机上运行我们的并行WPF程序差不多快了两倍。然而这个被冻结的用户界面呢?让这么时尚的程序界面没有响应?然而如果Parallel.For启动新线程,为什么界面线程仍然被阻塞?

原因是Parallel.For试图精确的模拟传统的for循环的行为,所以它阻塞代码的进一步执行直到它完成所有工作。

让我们在这里暂停一下。如果你已经有一个程序,它能正常工作且满足你所有的要求,而你想简单的利用并行处理提升速度,将循环替换成Parallel.For或Parallel.ForEach可能足够了。但在很多情况下你需要更高级的工具。

为了让界面有响应,我将使用任务,它是任务并行库带来的新概念。一个任务便是一个通常运行在工作线程的异步操作。.NET Framework优化负载均衡,并提供一个管理任务且使他们之间异步调用的不错的API。为了启动异步操作,我将使用Task.Factory.StartNew方法。

于是我将删除Parallel.For并将之替换为下面的代码,再一次尝试尽量少的改变。

for (int i = 2; i < 20; i++)
{
var t = Task.Factory.StartNew(() =>
{
var result = SumRootN(i);
this.Dispatcher.BeginInvoke(new Action(() =>
textBlock1.Text += "root " + i.ToString() + " " +
result.ToString() + Environment.NewLine)
,null);

});
}

编译,运行....很好,界面有响应了。在程序还在计算结果的时候我可以移动窗口和调整窗口大小。但现在又有两个问题:

1.我的程序告诉我,它执行花了0秒。
2.程序仅仅计算20次方根,并显示给我一样的列表结果。

让我们做最后一步。C#专家可以大声喊出来:闭包(closure)!是的,i是在循环里使用,那么当工作线程启动时,i的值已经改变了。当i等于20的时候循环退出,这便是传递给新创建的任务的值。

当你用lambda表达式的形式(译注:事实上匿名委托跟lambda表达式几乎等价,所以这里也应包括匿名委托)分配一些委托时(异步编程通常不可避免),闭包的这个问题是很常见的,所以要留意。解决方案相当简单。只需将循环的变量拷贝到循环内声明的一个变量。然后使用这个本地变量代替循环变量。

for (int i = 2; i < 20; i++)
{
int j = i;
var t = Task.Factory.StartNew(() =>
{
var result = SumRootN(j);
this.Dispatcher.BeginInvoke(new Action(() =>
textBlock1.Text += "root " + j.ToString() + " " +
result.ToString() + Environment.NewLine)
, null);
});
}

让我们来看第二个问题(译注:事实上是第一个):执行时间无法确定。我执行异步的任务,所以没有阻塞任何代码执行。程序启动任务后执行到下一行,这时读取时间并显示它。由于它并没有长时间占用,所以我的计时器得到0。

有时,对于不需要等待线程完成他们的工作便继续是可以的。但还有些时候,你需要在工作完成的时候得到一个信号,因为它影响着你的工作流程。在第二种情景,计时器是个好例子。

为了得到我的时间测量,我需要包装用来读取计时器值的代码到另一个来自任务并行库的方法:TaskFactory.ContinueWhenAll。这正是我需要的:它等待数组中的所有线程完成后执行委托。这个方法只在数组上工作。所以我需要将所有任务存储在某个地方以便可以等待他们完成。

这是我最终的代码,如下所示:

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public static double SumRootN(int root)
{
double result = 0;
for (int i = 1; i < 10000000; i++)
{
result += Math.Exp(Math.Log(i) / root);
}
return result;
}

private void start_Click(object sender, RoutedEventArgs e)
{
textBlock1.Text = "";
label1.Content = "Milliseconds: ";

var watch = Stopwatch.StartNew();
List<Task> tasks = new List<Task>();
for (int i = 2; i < 20; i++)
{
int j = i;
var t = Task.Factory.StartNew(() =>
{
var result = SumRootN(j);
this.Dispatcher.BeginInvoke(new Action(() =>
textBlock1.Text += "root " + j.ToString() + " " +
result.ToString() +
Environment.NewLine)
, null);
});
tasks.Add(t);
}

Task.Factory.ContinueWhenAll(tasks.ToArray(),
result =>
{
var time = watch.ElapsedMilliseconds;
this.Dispatcher.BeginInvoke(new Action(() =>
label1.Content += time.ToString()));
});

}
}

最终,所有事情都如预期的那样工作:我有一个结果列表,界面没有冻结,并且花费的时间正确显示。此代码看上去跟最初的代码完全不同,但令人惊讶的是它并没有那么长,而我可以重用它的大部分(感谢lambda表达式语法)。

同样,我试图覆盖大部分初学者在并行编程和多线程下在没有进入更深的细节时容易遇到的常见问题。但我期望这只是系列文章中的第一篇。但如果你不想等待我的下一篇文章,可以看看MSDN的.NET Framework中的并行编程和the parallel framework team blog。

当然,就如我在这个blog发布的其他文章一样,我希望它对你是非常有用的。但这篇文章跟之前我写的纯粹C#的文章有点不太一样。所以,这里有个问题:这种初学者的内容对你有多大的用处?你是否想看到更多这样的?如果你现在正在考虑使用并行和异步编程,在你想到的是什么情景?我真的非常需要你们的反馈,我期待你们的评论。(译注:若想给作者反馈请去原文)

P.S.

感谢所有此时帮助我的人:Dmitry Lomov和Michael Blome帮助我了解TPL,Danny Shih和Mads Torgersen检阅这篇文章并提供些意见,Mick Alberts和Robin Reynolds-Haertle进行编辑。

转载于:https://www.cnblogs.com/tianfan/archive/2010/06/14/parallel-programming-in-net-framework-4-getting-started.html

[翻译].NET framework 4.0并行编程:入门相关推荐

  1. Flash:Flash动画设计软件界面的简介、Flash AS 3.0代码编程入门教程之详细攻略

    Flash:Flash动画设计软件界面的简介.Flash AS 3.0代码编程入门教程之详细攻略 目录 Flash动画设计软件界面的简介 快捷键 菜单栏 下边工具栏 右边工具栏 工具箱 Flash A ...

  2. mysql储存过程编程,MySQL 5.0存储过程编程入门

    首先看MySQL 5.0参考手册中关于创建存储过程的语法说明: CREATE [DEFINER = { user | CURRENT_USER }] PROCEDURE sp_name ([proc_ ...

  3. .Net并行编程系列文章导航

    .Net4.0并行编程系列文章如下: 多核时代 .NET Framework 4 中的并行编程9---线程安全集合类 多核时代 .NET Framework 4 中的并行编程8---任务的同步 多核时 ...

  4. .NET Framework 4.0 和 Dublin 中的 WCF 和 WF 服务 - z

    在 2008 年 10 月份召开的专业开发人员大会 (PDC) 上,Microsoft 发布了有关 Microsoft .NET Framework 4.0 中将要提供的大量改进的详细信息,尤其是在 ...

  5. Concurrency in C# Cookbook中文翻译 :1.3并发性概述:响应式编程入门(Rx)

    Introduction to Reactive Programming (Rx) 响应式编程入门(Rx) Reactive programming has a higher learning cur ...

  6. linux c 并行编程从入门到精通,VISUAL STUDIO 2010并行编程从入门到精通(微软技术丛书)...

    摘要: <微软技术丛书:Visual Studio2010并行编程从入门到精通>循序渐进,步骤式动手练习迅速帮助读者掌握并行编程的基础知识. <微软技术丛书:Visual Studi ...

  7. C#的变迁史08 - C# 5.0 之并行编程总结篇

    C# 5.0 搭载于.NET 4.5和VS2012之上. 同步操作既简单又方便,我们平时都用它.但是对于某些情况,使用同步代码会严重影响程序的可响应性,通常来说就是影响程序性能.这些情况下,我们通常是 ...

  8. 脑残式网络编程入门(八):你真的了解127.0.0.1和0.0.0.0的区别?

    本文由"小姐姐养的狗"原创发布于"小姐姐味道"公众号,原题<127.0.0.1和0.0.0.0地址的区别>,收录时有优化和改动.感谢原作者的分享. ...

  9. C++教程从0到1入门编程中知识点记录!

    C++教程从0到1入门编程中知识点记录! 一.C语言 1.冒泡排序 示例代码: #include <iostream> using namespace std; int main() { ...

  10. 【黑马程序员 C++教程从0到1入门编程】【笔记3】C++核心编程(内存分区模型、引用、函数提高)

    黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难 文章目录 1 内存分区模型 1.1 程序运行前 1.2 程序运行后(手动开辟内存:c语言malloc,c++new) 1.3 new操作 ...

最新文章

  1. Android 保存图片到系统及相关问题的解决方案
  2. 深入理解计算机系统(3)
  3. python Intel Realsense udp协议 局域网传输实时视频流并通过窗口显示 (opencv压缩解码)
  4. 合并工具_分享一个SM to HISM合并工具
  5. 使用gulp-connect实现web服务器
  6. h5通过php微信支付宝支付,用H5调用支付微信公众号支付的解析
  7. Win10开机跳过欢迎界面直接进系统桌面的设置方法
  8. Atitit..文件上传组件选型and最佳实践总结(2)----断点续传
  9. C语言入门20个简单程序|最新更新2021.7.13
  10. 关于limn阶乘/n^n的若干解法(2)
  11. 双显示屏切单显时打不开关掉的显示屏上打开的软件的问题
  12. python中import requests是什么意思_python中requests库使用方法详解
  13. 999瓶水一瓶毒药,10只小鼠,使用python暴力编程
  14. 如何成为有效学习的高手(笔记)
  15. gdiplus 水印_GDI+ 实现透明水印和文字
  16. ,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microso ft.com/fwlink/?LinkID=135170 中的 about_Execution_Policies
  17. 对抗式学习pythonC day1 菜鸟档案
  18. ERP中的“蝴蝶效应”:重视过程的控制
  19. css grid布局实现水平垂直居中 文字水平垂直居中
  20. Image加载二进制数据图片

热门文章

  1. linux rtl8188eu ap模式 密码错误 disassoc reason code(8)
  2. 浅析变长数组(VLA)和动态数组
  3. js中的innerText、innerHTML、属性值、value与jQuery中的text()、html()、属性值、val()总结...
  4. Oracle 11g RAC oc4j/gsd Offline
  5. http2-协议协商过程
  6. 使用delphi 开发多层应用(十四)使用Basic4android 显示kbmMW server数据
  7. ​5月9日数据匹配图论、匈牙利、KM算法,多目标跟踪
  8. 平时碰到系统CPU飙高和频繁GC,你会怎么排查?
  9. 从零开始学安全(二十三)●用PHP编写留言板
  10. 模因(meme)收集