目录
1 For, Foreach和Parallel.For、Parallel.Foreach
所谓并行程序开发是我们不关系任务什么时候执行,只关心怎么利用计算机资源更快的执行。
并行编程是指软件开发的代码,它能在同一时间执行多个计算任务,提高执行效率和性能一种编程方式,属于多线程编程范畴。所以我们在设计过程中一般会将很多任务划分成若干个互相独立子任务,这些任务不考虑互相的依赖和顺序。
NET Framework4.0引入了Task Parallel Library(TPL)实现了基于任务设计而不用处理重复复杂的线程的并行开发框架。它支持数据并行,任务并行与流水线。核心主要是Task,但是一般简单的并行我们可以利用Parallel提供的静态类如下三个方法。
Parallel.Invoke 对给定任务实现并行开发
Parallel.For 对固定数目的任务提供循环迭代并行开发
parallel.Foreach 对固定数目的任务提供循环迭代并行开发
注意:所有的并行开发不是简单的以为只要将For或者Foreach换成Parallel.For与Parallel.Foreach这样简单。
1 For, Foreach和Parallel.For、Parallel.Foreach
1.1 基本分析
for与foreach
相对于原来的for语句foreach具有更好的执行效率
for与Parallel.For区别:
从CPU使用方面而言,Parallel.For 属于多线程范畴,可以开辟多个线程使用CPU内核,也就是说可以并行处理程序。For 循环是单线程的,一个线程执行完所有循环。也就说For是同步,Parallel.For 是异步执行。
任务的开销大小对并行任务的影响:
如果任务很小,那么由于并行管理的附加开销(任务分配,调度,同步等成本),可能导致并行执行并不是最优化方案。
Parallel.ForEach 和 ForEach 与 Parallel.For 和 For 一样,一个是异步执行,开辟多个线程。一个是同步执行,开辟一个线程。因此,效率方面同上,主要看执行的什么任务。
Parallel.For不能添加ref或out修饰的变量,错误提示为:不能在匿名表达式、lanmda表达式或查询表达式内使用ref或out变量
停止并行For循环的方法:
在 Parallel.For 或 Parallel.ForEach 循环中,不能使用与顺序循环中相同的 break 或 Exit 语句,这是因为这些语言构造对于循环是有效的,而并行“循环”实际上是方法,不是循环。 相反,可以使用 Stop 或 Break 方法。 Parallel.For 的一些重载接受 Action<int, ParallelLoopState>作为输入参数。 ParallelLoopState 对象由运行时在后台创建,你可以在 lambda 表达式中为它指定你喜欢的任何名称。
在 Action<int, ParallelLoopState>的委托方法中调用ParallelLoopState参数的Stop或Break方法。
Stop 方法;它将告知循环的所有迭代(包括那些在其他线程上的当前迭代之前开始的迭代)在方便的情况下尽快停止。
Break 方法;它会导致其他线程放弃对后续片段的工作(如果它们正忙于任何这样的工作),并在退出循环之前处理完所有前面的元素。
区别就在于,Stop仅仅通知其他迭代尽快结束,而Break不仅通知其他迭代尽快结束,同时还要保证退出之前要完成LowestBreakIteration之前的迭代。 例如,对于从 0 到 1000 并行迭代的 for 循环,如果从第 100 此迭代开始调用 Break,则低于 100 的所有迭代仍会运行,从 101 到 1000 的迭代则不必要。而调用Stop方法不保证低于 100 的所有迭代都会运行。
1.2 示例代码
public static void test()
{
Parallel.For(1, 5, x=>
{
Console.WriteLine(x);
});
Console.WriteLine("------------------------");
var numbers = Enumerable.Range(103, 8);
Parallel.ForEach(numbers, x =>
{
Console.WriteLine(x);
});
Console.WriteLine("------------------------");
Parallel.Invoke(
()=>{ Console.WriteLine("ABC");},
()=>{ Console.WriteLine("xxxOMG");},
()=>{ Console.WriteLine("MMG");},
()=>{ Console.WriteLine("oooMMG");},
() => { Console.WriteLine("999ABC"); },
()=>{ Console.WriteLine("888MMG");}
);
}
运行结果截图:
2 Parallel.Invoke
两个重载方法:
public static void Invoke(params Action[] actions);
public static void Invoke(ParallelOptions parallelOptions, params Action[] actions);
2.1 action调用模式及示例
2.1.1 调用方法解释
//方式一
Parallel.Invoke(() => Task1(), () => Task2(), () => Task3());
//方式二
Parallel.Invoke(Task1, Task2, Task3);
//方式三
Parallel.Invoke(
() =>
{
Task1();
},
Task2,
delegate () { Task3(); console.write('do someting!');});
2.1.2 示例代码
public class ParallelInvoke
{
/// <summary>
/// Invoke方式一 action
/// </summary>
public void Client1()
{
Stopwatch stopWatch = new Stopwatch();
Console.WriteLine("主线程:{0}线程ID : {1};开始", "Client1", Thread.CurrentThread.ManagedThreadId);
stopWatch.Start();
Parallel.Invoke(() => Task1("task1"), () => Task2("task2"), () => Task3("task3"));
stopWatch.Stop();
Console.WriteLine("主线程:{0}线程ID : {1};结束,共用时{2}ms", "Client1", Thread.CurrentThread.ManagedThreadId, stopWatch.ElapsedMilliseconds);
}
private void Task1(string data)
{
Thread.Sleep(5000);
Console.WriteLine("任务名:{0}线程ID : {1}", data, Thread.CurrentThread.ManagedThreadId);
}
private void Task2(string data)
{
Console.WriteLine("任务名:{0}线程ID : {1}", data, Thread.CurrentThread.ManagedThreadId);
}
private void Task3(string data)
{
Console.WriteLine("任务名:{0}线程ID : {1}", data, Thread.CurrentThread.ManagedThreadId);
}
}
调用:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Threading;
namespace myParallel
{
class Program
{
static void Main(string[] args)
{
ParallelInvoke pi = new ParallelInvoke();
pi.Client1();
}
}
}
2.1.3 分析
Parallel.Invoke 的使用过程中我们要注意以下特点:
- 没有特定的顺序,每个Task可能是不同的线程去执行,也可能是相同的;
- Invoke中的方法全部执行完才返回,这样对我们以后设计并行的时候,要考虑每个Task任务尽可能差不多,如果相差很大,比如一个时间非常长,其他都比较短,这样一个线程可能会影响整个任务的性能。这点非常重要;
- 但是即使有异常在执行过程中也同样会完成,他只是一个很简单的并行处理方法,特点就是简单,不需要我们考虑线程的问题。主要Framework已经为我们控制好线程池的问题。
- Invoke在每次调用都有开销的,不一定并行一定比串行好,要根据实际情况,内核环境多次测试调优才可以。如果在设计Invoke中有个需要很长时间,这样会影响整个Invoke的效率和性能,这个我们在设计每个task时候必须去考虑的。
- Invoke 参数是委托方法。
- 异常处理比较复杂。
ps:如果其中有一个异常怎么办? 带做这个问题修改了增加了一个Task4.
/// <summary>
/// Invoke方式一 action
/// </summary>
public void Client2()
{
Stopwatch stopWatch = new Stopwatch();
Console.WriteLine("主线程:{0}线程ID : {1};开始", "Client1", Thread.CurrentThread.ManagedThreadId);
stopWatch.Start();
try
{
Parallel.Invoke(
() => Task1("task1"),
() => Task2("task2"),
() => Task3("task3"),
delegate() { throw new Exception("我这里发送了异常"); });
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
Console.WriteLine(ex.Message);
}
stopWatch.Stop();
Console.WriteLine("主线程:{0}线程ID : {1};结束,共用时{2}ms", "Client1", Thread.CurrentThread.ManagedThreadId, stopWatch.ElapsedMilliseconds);
}
不建议在并行程序中写异常
2.2 ParallelOptions 参数模式
2.2.1 ParallelOptions类
ParallelOptions options = new ParallelOptions();
//指定使用的硬件线程数为4
options.MaxDegreeOfParallelism = 4;
有时候我们的线程可能会跑遍所有的内核,为了提高其他应用程序的稳定性,就要限制参与的内核,正好ParallelOptions提供了MaxDegreeOfParallelism属性。
2.2.2 示例代码
下述代码的执行原理为:
-
程序在执行过程中线程数码不超过3个
-
CancellationTokenSource/CancellationToken控制任务的取消。
// 定义CancellationTokenSource 控制取消
readonly CancellationTokenSource _cts = new CancellationTokenSource();
/// <summary>
/// Invoke方式一 action
/// </summary>
public void Client3()
{
Console.WriteLine("主线程:{0}线程ID : {1};开始{2}", "Client3", Thread.CurrentThread.ManagedThreadId, DateTime.Now);
var po = new ParallelOptions
{
CancellationToken = _cts.Token, // 控制线程取消
MaxDegreeOfParallelism = 3 // 设置最大的线程数3,仔细观察线程ID变化
};
Parallel.Invoke(po, () => Task1("task1"), () => Task5(po), Task6);
Console.WriteLine("主线程:{0}线程ID : {1};结束{2}", "Client3", Thread.CurrentThread.ManagedThreadId, DateTime.Now);
}
private void Task1(string data)
{
Thread.Sleep(5000);
Console.WriteLine("任务名:{0}线程ID : {1}", data, Thread.CurrentThread.ManagedThreadId);
}
// 打印数字
private void Task5(ParallelOptions po)
{
Console.WriteLine("进入Task5线程ID : {0}", Thread.CurrentThread.ManagedThreadId);
int i = 0;
while (i < 100)
{
// 判断是否已经取消
if (po.CancellationToken.IsCancellationRequested)
{
Console.WriteLine("已经被取消。");
return;
}
Thread.Sleep(100);
Console.Write(i + " ");
Interlocked.Increment(ref i);
}
}
/// <summary>
/// 10秒后取消
/// </summary>
private void Task6()
{
Console.WriteLine("进入取消任务,Task6线程ID : {0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000 * 10);
_cts.Cancel();
Console.WriteLine("发起取消请求...........");
}
2.3 中断并行
2.3.1 break与stop
如何中途退出并行循环?
是的,在串行代码中我们break一下就搞定了,但是并行就不是这么简单了,不过没关系,在并行循环的委托参数中
提供了一个ParallelLoopState,该实例提供了Break和Stop方法来帮我们实现。
- Break: 当然这个是通知并行计算尽快的退出循环,比如并行计算正在迭代100,那么break后程序还会迭代所有小于100的。
- Stop:这个就不一样了,比如正在迭代100突然遇到stop,那它啥也不管了,直接退出。
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
ConcurrentBag<int> bag = new ConcurrentBag<int>();
Parallel.For(0, 20000000, (i, state) =>
{
if (bag.Count == 1000)
{
//state.Break();
state.Stop();
return;
}
bag.Add(i);
});
Console.WriteLine("当前集合有{0}个元素。", bag.Count);
}
}
}
2.3.2 Cancel
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
public static void Main()
{
var cts = new CancellationTokenSource();
var ct = cts.Token;
Task.Factory.StartNew(() => fun(ct));
Console.ReadKey();
//Thread.Sleep(3000);
cts.Cancel();
Console.WriteLine("任务取消了!");
}
static void fun(CancellationToken token)
{
Parallel.For(0, 100000,
new ParallelOptions { CancellationToken = token },
(i) =>
{
Console.WriteLine("针对数组索引{0}的一些工作代码……ThreadId={1}", i, Thread.CurrentThread.ManagedThreadId);
});
}
}
}
配合本文你还可以参考: