并行编程从业务实现的角度可分为数据并行与任务并行,也就是要解决的问题是以数据为核心还是以要处理的事情为核心。基于任务的并行编程模型TPL(任务并行库)是从业务角度实现的并行模型,它以System.Threading.Tasks命名空间下的Parallel类为实现核心类,优点是不需要我们考虑不同的硬件差异,只需要重点关注所实现的任务。

1.任务并行库TPL

  TPL主要包括数据并行和任务并行,无论是数据并行还是任务并行,都可以使用并行查询PLINQ提高数据查询的效率。数据并行是对数据集合中的元素同时执行相同的操作,实现方式主要是利用Parallel.For或Parallel.ForEach。任务并行是采用Parallel.Invoke方法来实现。并行查询是指并行实现LINQ查询,它与LINQ的主要区别就在于并行,它会利用多处理器去去同时执行查询,查询前会先对数据源进行分区,之后每个处理器开始执行其中一个小片段,这样就加快了查询的速度。

  TPL主要有以下优点,TPL编程模型使用CLR线程池执行多个任务,并能自动处理工作分区、线程调度和取消、状态管理以及其他低级别的细节操作;TPL更加智能性,它可以通过试探法来预判任务集并行运行是否有性能优势,当结果是没有性能优势时还会自动选择顺序运行;TPL还会动态地按比例调节并发程度,从而最有效地使用所有可用的处理器。对TPL进行一个总结就是,它让编程人员不必考虑多处理器如何并行工作以及合理调节等底层实现,只需要考虑业务逻辑就可以了。

  上面提到了使用数据并行时或PLINQ时,需要对数据源进行分区。主要实现类为System.Collections.Concurrent命名空间中提供的Partitioner类来实现,通过这个类来将数据源分成许多小片段。对于分区主要有4种方式:按范围分区,适用于已经知道长度的数据源,当多个线程并行处理数据片段时,每个线程接受唯一的开始和结束索引,并行处理时不会覆盖其他线程或被其他线程覆盖,这种分区的缺点是如果一个线程已执行完,它无法帮助那些未完成的线程;对于长度未知的集合则使用按区块分区,线程执行时会并行循环执行区块中一定数量的数据,执行完再检索其他区块。分区程序可确保分发所有元素,并且没有重复项,但是每次线程获取另一个区块时,这时会产生同步开销。一般情况下按区块分区比按范围分区速度快;TPL还支持动态数量的分区,即可以随时创建分区;编程人员也可以自定义分区程序,可从System.Collections.Concurrent.Partitioner<TSource>派生并重写虚方法。

2.Parallel.For和Parallel.ForEach

  在数据并行中,For和ForEach方法会自动对源集合进行分区,这样多个线程才可以并行执行。对这2个方法的理解可以是在for和foreach的基础上加了并行,要注意在多重循环中一般只对外部循环进行并行化,当内部循环执行的工作用时较少时使用内部并行会降低整体运行的性能。对于ForEach方法有我敲的2个小例子,一个是简单ForEach方法的使用,还有一个是按范围分区ForEach方法的使用。以下是ForEach的代码:

   int n = ;
private async void button1_Click(object sender, EventArgs e)
{
int[] a = Enumerable.Range(, n).ToArray();
await ParaGetNumAsync(n, a);
}
async Task ParaGetNumAsync(int n, int[] a)
{
await Task.Delay();
ConcurrentBag<double> cb = new ConcurrentBag<double>();
Stopwatch sw = Stopwatch.StartNew();
Parallel.ForEach(a, (v) => {
cb.Add(v * v);
Thread.Sleep(TimeSpan.FromMilliseconds());
});
sw.Stop();
double[] b = cb.ToArray();
Array.Sort(b);
AddInfo("用时:" + sw.ElapsedMilliseconds);
}
void AddInfo(string s)
{
listBox1.Items.Add(s);
} private void button2_Click(object sender, EventArgs e)
{
var source = Enumerable.Range(, ).ToArray();
var rangePartitioner = Partitioner.Create(, source.Length);
double[] results = new double[source.Length];
Stopwatch sw = Stopwatch.StartNew();
Parallel.ForEach(rangePartitioner, (range, loopState) => {
for (int i = range.Item1; i < range.Item2; i++)
results[i] = source[i] * source[i];
});
sw.Stop();
AddInfo("所用时间:"+sw.ElapsedMilliseconds);
sw.Restart();
Parallel.ForEach(source, (i) => {
results[i] = source[i] * source[i];
});
sw.Stop();
AddInfo("普通并行所用时间:" + sw.ElapsedMilliseconds);
}

  关于For一共有4个例子:

(1)当不需要取消或中断迭代,或者不需要保持线程本地状态,此时可采用简单For循环。
(2)指定并行选项以获得最佳性能。
(3)如果希望监视或控制并行循环的状态,可使用带循环状态的For循环。
(4)使用带线程局部变量的For循环。

 //简单For方法
private void button1_Click(object sender, EventArgs e)
{
int[] a = Enumerable.Range(, n).ToArray();
int[] b = Enumerable.Range(, n).ToArray();
int[] c=new int[n];
Action<int> action = (i) => { c[i] = a[i] + b[i]; };
Stopwatch sw = Stopwatch.StartNew();
Parallel.For(, n, action);
sw.Stop();
listBox1.Items.Add("并行用时:" + sw.ElapsedMilliseconds);
Stopwatch sw1 = Stopwatch.StartNew();
for (int i = ; i < n; i++)
{
c[i] = a[i]- b[i];
}
//Thread.Sleep(100);
sw1.Stop();
int ii = Convert.ToInt32(sw1.ElapsedMilliseconds);
ii = ii + ;
listBox1.Items.Add("非并行用时:" + ii);
}
//带并行选项For循环
private void button2_Click(object sender, EventArgs e)
{
Stopwatch sw = new Stopwatch();
Action<int> action1 = NewAction();
sw.Restart();
Parallel.For(, n, action1);
sw.Stop();
listBox1.Items.Add("并行用时:" + sw.ElapsedMilliseconds);
Action<int> action2 = NewAction();
ParallelOptions option = new ParallelOptions();
option.MaxDegreeOfParallelism = * Environment.ProcessorCount;
sw.Restart();
Parallel.For(, n, option, action2);
sw.Stop();
listBox1.Items.Add("带并行用时:" + sw.ElapsedMilliseconds); }
//并行循环状态的For方法
private void button3_Click(object sender, EventArgs e)
{
Stopwatch sw = Stopwatch.StartNew();
ConcurrentBag<Data> cb = new ConcurrentBag<Data>();
Action<int, ParallelLoopState> action = (i, loopState) =>
{
Data data = new Data() { Name="A"+i.ToString(),Number=i};
cb.Add(data);
if (i == )
loopState.Break();
};
var result = Parallel.For(, n, action);
sw.Stop();
listBox1.Items.Add(sw.ElapsedMilliseconds);
}
//线程局部变量的For循环
private async void button4_Click(object sender, EventArgs e)
{
await ParaSumAsync(n);
await SumAsync(n);
listBox1.Items.Add("执行完毕");
} async Task ParaSumAsync(int n)
{
await Task.Delay();
int total = ;
int[] test = new int[];
var cb = new ConcurrentBag<Data>();
Func<int> subInit=()=>; Func<int, ParallelLoopState, int, int> body = (i, loopState, subTotal) =>
{
Data data = new Data() { Name = i.ToString(), Number = i };
subTotal += data.Number;
data.Name = "T" + subTotal;
cb.Add(data);
//模拟每次循环至少用时100ms以上
Thread.Sleep(TimeSpan.FromMilliseconds());
return subTotal;
};
Action<int> action = (subTotal) =>
{
//由于total是全局变量,因此需要通过原子操作解决资源争用问题
total = Interlocked.Add(ref total, subTotal);
};
Stopwatch sw = Stopwatch.StartNew();
Parallel.For(, n, subInit, body, action);
sw.Stop();
string s="";
string strName="";
foreach (var v in cb)
{
s += v.Number.ToString() + ",";
strName += v.Name + " ";
}
AddInfo("每个对象的数:"+s.TrimEnd(','));
AddInfo(strName);
AddInfo("并行用时:"+sw.ElapsedMilliseconds+" 结果:"+total);
}
private Action<int> NewAction()
{
var cb = new ConcurrentBag<Data>();
Action<int> action = (i) => { cb.Add(new Data() { Name = "A" + i.ToString(), Number = i }); };
return action;
}
async Task SumAsync(int n)
{
int sum = ;
string s = "";
var list = new List<Data>();
Stopwatch sw = Stopwatch.StartNew();
for (int i = ; i < n; i++)
{
Data data = new Data() { Name = "A" + i.ToString(), Number = i };
list.Add(data);
sum += list[i].Number;
s += list[i].Number.ToString() + ",";
await Task.Delay(TimeSpan.FromMilliseconds());
}
sw.Stop();
AddInfo("每个对象中的数: " + s.TrimEnd(','));
AddInfo("非并行用时:" + sw.ElapsedMilliseconds+" 结果:"+sum);
}
void AddInfo(string s)
{
listBox1.Items.Add(s);
}

 3.Parallel.Invoke

  这个方法可以并行执行任务,而且并行的任务都是异步执行的,在练习的例子中写了一个简单的Invoke执行程序。如果不知道任务调度程序,则会使用默认的任务调度程序。默认的调度程序用两种队列对线程池中的线程进行排队,一种是按先进先出进行操作的全局队列,另一种是按后进先出进行操作的本地队列。这种默认调度程序还自动实现了用于负载平衡的工作窃取、用于实现最大吞吐量的线程注入和撤销。每个进程是一个应用程序域,每个应用程序域有一个线程池,线程池有一个全局队列,全局队列一般是存放顶级任务的队列,也就是父级任务,本地队列一般是存放子任务和嵌套任务。由于父任务可能是顶级任务,也可能是另一个任务的子级,当此线程准备好执行更多工作时,它将首先在本地队列中查找,如果有等待的嵌套任务或子任务时,那么这个任务将很快被访问,也就是开始执行。下面是一段简单的代码说明全局队列和本地队列。

  //ta是一个顶级任务,此线程将在线程池全局队列中排队
  Task ta = Task.Factory.StartNew(() => {
  //tb和tc属于嵌套任务,将会在本地队列排队
  Task tb = new Task();
  Task tc = new Task();
  });

  当线程池线程准备好执行更多的工作时,它将首先在其本地队列的开始部分查找,然后依次在全局队列和其他线程的本地队列中查找,如果在另一个线程的本地队列找到工作项将会先使用试探法来判断是否能有效地执行该工作,如果能有效运行,则按后进先出顺序使队列末尾的工作项离队,接着开始执行。这种模式可帮助线程池更有效地平衡工作负载。

  当需要自定义任务调度策略时,可以通过设置TaskCreationOption枚举值自定义调度策略。TaskCreationOption枚举表示任务创建和执行任务时的可选行为,利用它可自定义任务调度策略,具体的枚举值可以查到,不过只有当可以保证自定义的任务调度策略执行效率高于默认的任务调度策略时,才使用TaskCreationOption。如果需要长时间执行某个任务时,并有可能阻塞本地队列中的所有其他任务,这时可以指定LongRunning选项来告诉调度程序该任务可能需要附加线程,类似于额外创建一个线程,即不再放入全局队列和本地队列。以下是Invoke方法的简单使用:

    CancellationTokenSource cts;
private void button1_Click(object sender, EventArgs e)
{
Action a1 = () => MyMethod("a");
Action a2 = () => MyMethod("b");
Action a3 = () => MyMethod("c");
ParallelOptions options = new ParallelOptions();
//将界面与当前同步上下文关联起来
options.TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
cts = new CancellationTokenSource();
options.CancellationToken = cts.Token;
Parallel.Invoke(options, a1,a2, a3);
}
async void MyMethod(string s)
{
while (cts.IsCancellationRequested == false)
{
listBox1.Items.Add(s);
await Task.Delay();
}
}

  并行执行多个任务时,有时候可能需要某个任务等待另一个任务完成后才能开始执行,这时需要用到任务等待与组合。其中Task.WaitAny方法等待参数中提供的任一个Task对象完成执行过程,只要指定的多个任务中有一个任务完成,就不再等待。最终当被等待的任务执行完毕后,可通过Task.FromResult方法判断执行的结果。以下是WaitAll和WaitAny方法的使用:

        //Task.WaitAll
private void button5_Click(object sender, EventArgs e)
{
Func<object, int> func = (object obj) =>
{
int k = (int)obj;
return k + ;
};
Task<int>[] tasks = new Task<int>[];
for (int i = ; i < tasks.Length; i++)
{
tasks[i] = new Task<int>(func, i);
tasks[i].Start();
}
Task.WaitAll(tasks);
listBox1.Items.Add("所有任务都已经执行完了");
}
//Task.WaitAny
private void button4_Click(object sender, EventArgs e)
{
Action act = () =>
{
int i=;
i++;
};
Task task = new Task(act);
task.Start();
Task.WaitAny(task);
listBox1.Items.Add("任务已经执行完了");
}

声明:本文原创发表于博客园,作者为方小白 ,如有错误欢迎指出。本文未经作者许可不许转载,否则视为侵权。

C#基础之并行编程的更多相关文章

  1. Parallel并行编程初步

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

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

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

  3. Parallel并行编程

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

  4. C#并行编程

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

  5. Task C# 多线程和异步模型 TPL模型 【C#】43. TPL基础——Task初步 22 C# 第十八章 TPL 并行编程 TPL 和传统 .NET 异步编程一 Task.Delay() 和 Thread.Sleep() 区别

    Task C# 多线程和异步模型 TPL模型   Task,异步,多线程简单总结 1,如何把一个异步封装为Task异步 Task.Factory.FromAsync 对老的一些异步模型封装为Task ...

  6. 并行编程OpenMP基础及简单示例

    OpenMP基本概念 OpenMP是一种用于共享内存并行系统的多线程程序设计方案,支持的编程语言包括C.C++和Fortran.OpenMP提供了对并行算法的高层抽象描述,特别适合在多核CPU机器上的 ...

  7. C#基础提升系列——C#任务和并行编程

    C#任务和并行编程 我们在处理有些需要等待的操作时,例如,文件读取.数据库或网络访问等,这些都需要一定的时间,我们可以使用多线程,不需要让用户一直等待这些任务的完成,就可以同时执行其他的一些操作.即使 ...

  8. C#并发编程-3 并行编程基础

    如果程序中有大量的计算任务,并且这些任务能分割成几个互相独立的任务块,那就应该使用并行编程. 并行编程用于分解计算密集型的任务片段,并将它们分配给多个线程.这些并行处理方法只适用于计算密集型的任务. ...

  9. .Net中的并行编程-2.ConcurrentStack的实现与分析

    在上篇文章<.net中的并行编程-1.基础知识>中列出了在.net进行多核或并行编程中需要的基础知识,今天就来分析在基础知识树中一个比较简单常用的并发数据结构--.net类库中无锁栈的实现 ...

随机推荐

  1. Java的Stream流

    yi.控制台输入输出流, 1.读取控制台输入 Java的控制台输入由System.in完成.为了获得一个绑定到控制台的字符流,可以把System.in包装在一个BufferedReader对象中来创建 ...

  2. github air项目中遇到的几个问题及解决(nodejs居多)

    https://github.com/cambecc/air 1.按照github中给出的步骤,执行到npm install,项目中的package.json包含了要安装的包的版本,但是安装的时候,p ...

  3. DateConvertUtil 日期工具类

    package com.hxqc.basic.dependency.util; import java.text.DateFormat; import java.text.ParseException ...

  4. 【[TJOI2010]电影迷】

    一度自闭 这道题一看就是最小割无疑 我们假设源点\(S\)表示看这个电影,汇点\(T\)表示不看这部电影 如果一个电影的价值为正,我们就从源点点连一条容量为\(val\)的边,表示割掉这个边也就是选择 ...

  5. Django中模型(二)

    Django中模型(二) 三.定义模型 1.模型.属性.表.字段间的关系: 一个模型类在数据库中对应一张表:在模型类中定义的属性,对应该模型对照表中的字段. 2.定义属性 A.概述 ·django根据 ...

  6. seek()和tell()在文件里转移

    Seek()方法允许在输入和输出流移动到任意的位置,seek()有好几种形式.包含:seekp() 方法和seekg()方法,p是put的意思,g是get的意思:其中输入流里用seekg()函数,输出 ...

  7. rocket-console控制台安装

    1.下载 github地址:https://github.com/apache/rocketmq-externals 2.选择稳定版本: 3.下载到本地:       环境需求 maven  jdk ...

  8. EF Core 中多次从数据库查询实体数据,DbContext跟踪实体的情况

    使用EF Core时,如果多次从数据库中查询一个表的同一行数据,DbContext中跟踪(track)的实体到底有几个呢?我们下面就分情况讨论下. 数据库 首先我们的数据库中有一个Person表,其建 ...

  9. C编程规范, 演示样例代码。

    /*************************************************************** *Copyright (c) 2014,TianYuan *All r ...

  10. 【Dubbo源码阅读系列】之 Dubbo SPI 机制

    最近抽空开始了 Dubbo 源码的阅读之旅,希望可以通过写文章的方式记录和分享自己对 Dubbo 的理解.如果在本文出现一些纰漏或者错误之处,也希望大家不吝指出. Dubbo SPI 介绍 Java ...