**
.NET并发编程-数据结构不可变性
**
作为程序员经常遇到产品上线后出现各种莫名其妙的问题,在我本地是好好的啊,也成为程序员面对未知问题的第一反应。这种不容易复现的问题,无非就是硬件不一致和软件不一致,更多的问题出在软件环境上,用户量、 并发这种测试容易遗漏的点。
为了保证编写的代码在不同的环境中出现一致的行为结果,通常就要利用不可变的数据结构。数据一旦创建后就不能修改其本身,修改后会产生新的数据。
- .NET不可变集合
- immutable 不变的;不可改变的
在.NET4.5引入不可变集合,在命名空间System.Collecttions.Immutable中。(注意这个类库不是.net核心类库,需要从nuget上安装)。不可变的集合结构每次修改数据后都会生成新的集合。像String类型一样,对它Substring,Replace都会生成新的字符串。
//可以将普通可变集合直接转为不可变集合
var dic = new Dictionary<int, int>().ToImmutableDictionary();
//直接创建不可变集合 不变的;不可改变的
var list = ImmutableList.Create<int>();
list = list.Add(1);
list = list.Add(2);
list = list.Add(3);
由于集合不可变,也就保证了多线程的安全。直接将集合丢给每个线程,原始集合不会变化。
-
.NET并发集合
-
Concurrent 同时发生的;并存的;同意的,一致的;协调的;合作的;共同(或同时)起作用的;<律>有相等权力的,同时(实施)的;<数>共点的,会合的
n. 共存物;同时发生的事情;竞争者还有一种线程安全集合在System.Collections.Concurrent中,在多线程环境中建议使用此类集合。Concurrent集合是可变集合,但提供了细粒度和无锁模式来提高多线程应用程序的性能和可扩展性。像ConcurrentDictionary字典,除了像传统字典Dictionary使用,还提供了很多兼容并发的方法,如AddOrUpdate或GetOrAdd等。如果不使用并发集合,在多线程环境中我们需要设置锁来保证数据的一致性。
-
函数式数据结构
可持久化数据结构也称之为函数式数据结构。
可持久化意味着数据结构是不可变的,修改只会返回修改后的新数据结构。(这里数据持久化和IO持久化区分)。大多数命令式数据结构都是短暂的,修改就破坏其结构。如Dictionary,List,Queue等。不可变性可能会带来一定的损耗,每次修改都会生成新的数据数据结构。
但在托管编程语言中,如C# 和Java中,已经做了足够多的优化,且在多核时代,基本可以忽略性能的影响。
以链表数据结构为例说明托管语言在共享数据结构上做的优化。
不可变的数据集合,每次修改后,不是完整拷贝原集合,比如集合中追加一项,只会修改引用指向的位置,共享剩余其他结构。
- 设计一个不可变类
C#有readonly和const两个关键字,还记得他们的区别和用处吗。
const静态常量,编译时被解析,通过类访问。
readonly动态常量,可延迟到构造函数中初始化,通过类实例访问。
public class Person
{
public const string Contry = "中国";
public string Name { get; }
public readonly Address Address;
public Person(string name, Address address)
{
this.Name = name;
this.Address = address;
}
}
public class Address
{
public string Street;
public Address(string street)
{
this.Street = street;
}
}
代码示例中,控制了Person的Address地址是不能被修改的,但它的底层字段Street仍然可以被修改。这就会导致person.Address.Street="M78星云"这样的行为,所以这就是浅不可变。微软考虑到不可变编程的重要性,随后又在C#6.0又引入了自动属性的概念,可以轻松的创建一个不可变类。像示例中的public string Name { get; }这样。
to be contiued!
NET并发编程-数据并行 Fork/Join是体现一个分而治之思想的任务框架
fork/join中处于等待中的任务实际上不阻止线程。
在多线程的情况下,使用forkjoin会提高工作效率,比如说多线程中有几个线程同时在执行任务,但是有的线程会先执行完毕,但是如果闲置这个已经跑完的线程就会造成资源上的浪费,如果让这个已经跑完的线程去帮助执行其他线程没执行完的任务,就可以减少整体的运行时间.
同时要知道,Fork和Join是两种动作,Fork是拆分,Join是合并.
要想实现通过ForkJoin提高效率那应该怎么做?
1.首先通过ForkJoinPool来执行fork/join任务.这个ForkJoinPool和线程池类似
2.创建一个fork/join任务,将这个放入ForkJoinPool中
3.然后通过ForkJoinPool提交任务
数据并行
数据并行是通过将数据集拆分为多个块并独立并行处理每个分区,将每块分配给单独的任务来实现。任务完成后,将重组数据集。就是拆分执行再重新组合的过程,这些数据块通过同一个函数(任务)执行。
任务并行是从另一个视角来并行执,对一个数据集或多个数据集同时执行多个不同的函数(任务)。
- Fork/Join模式
把大任务分割成若干小任务,最终归总每个小任务结果后得到大任务结果的模式,先Fork后Join。Java中也有此模式的应用ForkJoinTask。在.NET中可以通过Parallel类中的Parallel.For循环轻松完成Fork/Join。
(在将数据集拆分的过程中,优化性能时,可以考虑结构类型代替引用类型。值类型分配在堆栈上,引用类型分配在堆上,会带来内存分配和垃圾回收的开销)
- PLINQ
LINQ通过提高抽象级别并将引用程序简化为一组丰富的操作来转换任何实现了IEnumerable接口的对象以提供声明式编程风格。最常见的操作时映射、排序和筛选。可以通过向查询添加.AsParallel()扩展来将LINQ转换为PLINQ。
编写并行应用程序的最佳方法是不用考虑并行。即不需要你来拆分数据集分配线程任务。PLINQ完全适合这种开发模式,它底层自动实现Fork/Join模式,它比Parallel类库中的并行方法更抽象,基本一句话告诉计算机你想要什么结果,这也是声明式编程风格的简洁直观。
PLINQ默认并不保证绝对并行运行。调度程序会衡量当前环境是否适合并行执行。但你需要强制并行时,可以通过Partitioner分区器来手动调整区分大小和分区策略。
NET并发编程-反应式编程
1、反应式编程
就是将事件作为数据流异步地监听和处理的一种编程范式。类似编辑Excel表格时,某个单个元的值是由其他单元格值决定的公式,一旦其他单元格的值发生变化,此公式单个格也会随之更新。
如果以声明式描述操作,那么就是函数式反应型编程。
反应式编程的好处就是通过描述一种简单且可维护的方法来处理异步、无阻赛计算和IO,从而增加了对多核和多CPU硬件上计算资源的使用。事件的控制从“请求”变为了“等待”。大数据实时流处理环境,都会使用到反应式编程,例如推文系统处理,日志系统收集等。
2、.NET中的工具
.NET中是有基于委托的模型事件的。订阅者的事件处理程序注册一系列事件,并在调用时触发事件。类似桌面应用程序中经常使用Button的Click事件,通过使用+=来注册绑定事件。所以传统意义上的事件就是来处理GUI用户界面的交互。这种命令式编程难以处理多个复杂事件的编排,并且容易造成内存泄漏。
在F#中使用|>管道操作符可以连接多个事件。不熟悉F#,就不多做说明。主要看看C#如果处理。
3、Reactive Extensions(Rx)反应式扩展
LINQ 是 对 序 列 数 据 进 行 查 询 的 一 系 列 语 言 功 能。 内 置 的 LINQ to Objects( 基 于 IEnumerable) 和 LINQ to Entities( 基 于 IQueryable) 是 两 个 最 常 用 的 LINQ 提 供 者。另外还有很多提供者,并且大多数都采用相同的基本架构。查询是延后执行的,只有在需要时才会从序列中获取数据。从概念上讲,这是一种拉取模式。 在查询过程中数据项是被逐个拉取出来的。
Reactive Extensions(Rx)把事件看作是依次到达的数据序列。因此,将 Rx 认作是 LINQ to events( 基 于 IObservable) 也 是 可 以 的, 它 与 其 他 LINQ 提 供 者 的 主 要 区 别 在 于, Rx 采用推送模式。就是说,Rx 的查询规定了在事件到达时程序该如何响应。Rx 在 LINQ 的基础上构建,增加了一些功能强大的操作符,作为扩展方法。
通过nuget安装Rx
Observable和Enumerable是对偶存在,很多方法相似可以相互转换。像下面输出结果都是1-5
// Observable对象
Observable.Range(1, 5)
.Subscribe(x => Console.WriteLine(x));
// Enumerable对象
foreach (var x in Enumerable.Range(1, 5))
Console.WriteLine(x);
Rx里主要的接口有两个,IObervable和 IObserver,上面Observable遍历输出只是简化了IObserver对象的创建,Subscribe方法可以只传入OnNext委托,也可以传入完整的IObserver对象。
public interface IObservable<out T>
{
IDisposable Subscribe(IObserver<T> observer);
}
public interface IObserver<in T>
{
void OnCompleted();
void OnError(Exception error);
void OnNext(T value);
}
var observer = Observer.Create<int>(
x => Console.WriteLine(x), // onNext参数(delegate)
ex => { throw ex; }, // onError参数(delegate)
() => { }); // onCompleted参数(delegate)
Observable.Range(1, 5).Subscribe(observer);
4、Rx主要用处
Rx的有点主要在于能够将传统的异步编程方式从支离破碎的代码调用中解放出来,将各个事件的处理连接起来放在一个单独的方法中,增加代码可读和可维护。
GUI编程中合并事件
桌面编程中多个事件进行组合的情况,比如鼠标按下/移动/放开事件进行关联处理,一般可能需要定一个变量Flag来标记状态,管理比较混乱。Rx可以将它们合成一个事件。
var drag = from down in this.MouseDownAsObservable()
from move in this.MouseMoveAsObservable().TakeUntil(this.MouseUpAsObservable())
select new { move.X, move.Y };
// 利用扩展方法将Winform原有的事件变换为 IObservable<T> 对象
public static class FormExtensions
{
public static IObservable<MouseEventArgs> MouseMoveAsObservable(this Form form)
{
return Observable.FromEventPattern<MouseEventArgs>(form, "MouseMove").Select(e => e.EventArgs);
}
public static IObservable<MouseEventArgs> MouseDownAsObservable(this Form form)
{
return Observable.FromEventPattern<MouseEventArgs>(form, "MouseDown").Select(e => e.EventArgs);
}
public static IObservable<MouseEventArgs> MouseUpAsObservable(this Form form)
{
return Observable.FromEventPattern<MouseEventArgs>(form, "MouseUp").Select(e => e.EventArgs);
}
}
Timer通知事件
在一定的时间间隔监视某个值的场景。下面例子中就是每隔5s检查textbox值是否变化,变化了就更新label。比以往简介了很多
// 每隔1秒监视一下watchTarget.Value的值
var polling =
Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(5))
.Select(_ => textBox1.Text)
.DistinctUntilChanged(); // 只有在值发生变化时才引发事件(polling)
polling.Subscribe(msg => this.Invoke(new Action<string>((e) => this.label1.Text = e),msg));
NET并发编程-任务函数并行
本小节介绍一种简单的函数组合来并行执行任务方式,达到不阻塞程序提高性能的目的。
1、任务并行
回顾下什么是任务并行,任务并行是在相同或不同的数据集上用时执行多个不同的函数,区别于数据并行是在数据集的元素之间同时执行同一个函数。
生产中可能会涉及不同的任务函数,处理不同的复杂的结构数据,通过利用在.NET提供的一些模型工具箱我们可以较为简便的任务并行跑起来。
2、.NET中的任务并行化支持
由浅入深,.NET1.0开始就提供线程的访问控制。System.Thread,可以代码控制创建启动销毁现场。但线程的创建开销比较大,后面有提供了ThreadPool类,线程池有助于克服性能问题。在初始化期间就加载了一组线程,然后重用这些线程,避免了频繁创建销毁线程的开销。
Action<string> downloadSite = url => {
var content = new WebClient().DownloadString(url);
Console.WriteLine($"The size of the web site {url} is {content.Length}");
};
var threadA = new Thread(() => downloadSite("http://www.nasdaq.com"));
var threadB = new Thread(() => downloadSite("http://www.bbc.com"));
threadA.Start();
threadB.Start();
threadA.Join();
threadB.Join();
ThreadPool.QueueUserWorkItem(o => downloadSite("http://www.nasdaq.com"));
ThreadPool.QueueUserWorkItem(o => downloadSite("http://www.bbc.com"));
像上面所示传统的方式也很繁琐,而且有很多弊端,比如无法获取结果,没有内置通知等。因为又提供了TPL任务并行库。
3、.NET任务并行库
TPL在ThreadPool上实现了很多优化,简化了添加并行的过程,通过Task对象提供支持,以取消和管理状态,处理和传播异常,以及控制工程线程的执行。
TPL提供很多种调度任务的方式,Invoke是最简单的一种。类似的还有Parallel.ForEach
System.Threading.Tasks.Parallel.Invoke(
Action(fun () -> convertImageTo3D (pathCombine "MonaLisa.jpg") (pathCombine "MonaLisa3D.jpg")),
Action(fun () -> setGrayscale (pathCombine "LadyErmine.jpg") (pathCombine "LadyErmineRed.jpg")),
Action(fun () -> setRedscale (pathCombine "GinevraBenci.jpg") (pathCombine "GinevraBenciGray.jpg")))
此方法接收任意数量的action委托参数,并为每一个委托创建任务。但是,action委托没有输入参数,并且返回void,这样的函数会有副作用。当所有任务终止时,Invoke方法将控制权交回给主线程以继续执行后续流程。在并行执行独立的异构任务时,就是针对不同的结构数据,此方法挺有效的。
弊端也很明显,没有输入类型,返回为Void,也就限制了组合使用,执行顺序也无法保障。
4、C#void问题
和Null类似,Void也是一个头疼的问题。函数式编程语言中每一个函数都有返回值,包括与void类似情况的unit类型,但是与void不同的是该值被视为一个值,概念上与bool和int没多大区别。
unit是缺少其他特定值的表达式的类型,像打印日志到控制台,写入文件等,没有特定的内容需要返回,因为函数需要返回unit。unit就是C#的void在F#中的等价产物。
在FP的函数就是一个映射,一个输入映射一个输出,这样函数才是无副作用的。在命令式编程语言中丢失了这个概念。
可以参考F#unit自定义个C#中的unit
public struct Unit : IEquatable<Unit>
{
public static readonly Unit Default = new Unit();
public override int GetHashCode() => 0;
public override bool Equals(object obj) => obj is Unit;
public override string ToString() => "()";
public bool Equals(Unit other) => true;
public static bool operator ==(Unit lhs, Unit rhs) => true;
public static bool operator !=(Unit lhs, Unit rhs) => false;
}
这样可以让每个函数都有返回值来确认函数已完成,并且任何使用action委托的地方都可以使用func代替,只需要给func执行返回值为unit即可。return Unit.Default;
5、延续传递风格CPS
一种更新更好的机制是将剩余的计算传递给(在线程完成执行后运行的)回调函数以继续工作。这种技术在FP中被称为延续传递风格Continuation-Passing Style CPS。通过将当前函数的结果传递给下一个函数,以延续的形式为你提供执行控制。
.NET中Task类提供比Thread更高级别的抽象,以便于控制每个每个任务操作的生命周期。
Task monaLisaTask = Task.Factory.StartNew(() => convertImageTo3D("MonaLisa.jpg", "MonaLisa3D.jpg"));
Task ladyErineTask = new Task(() => setGrayscale("ladyErine.jpg", "ladyErine3D.jpg"));
ladyErineTask.Start();
Task ginevraBenciTask = Task.Run(() => setRedscale("ginevraBenci.jpg", "ginevraBenci3D.jpg"));
ask提供三种直接创建任务的方式,new Task方式可以控制在何处Start启动任务。
通过Task的ContinueWith可以延续任务。FromCurrentSynchronizationContext捕获当前不同上下文中运行,如果需要同步UI请使用,会自动选择合适的上下文去更新。
Task ginevraBenciTask = Task.Run<Bitmap>(() => setRedscale("ginevraBenci.jpg", "ginevraBenci3D.jpg"));
ginevraBenciTask.ContinueWith(bitmap => {
var bitmapImage = bitmap.Result;
}, TaskScheduler.FromCurrentSynchronizationContext());
6、组合策略
使用ContinueWith可以延续任务,但较多的延续,代码将比较繁琐,而且如果要添加错误处理或取消支持就不好添加了。所以要使用到函数闭包中说到的函数组合。
C#实现组合Compose函数如下
Func<A,C> Compose<A,B,C>(this Func<A.B> f ,Func<B,C> g)=>(n)=>g(f(n))
在并行Task中,f,g应该是独立运行的,当做两个任务,f任务返回Task(B),g任务返回Task©,所以改造如下
Func<A,Task<C>> Compose<A,B,C>(this Func<A.Task<B>> f ,Func<B,Task<C>> g)=>(n)=>g(f(n))
但是有问题的,f(n)返回类型Task(B),无法直接给函数g使用,输入类型不一致。
这个使用需要使用FP中常见的一种模式,单子Monad。对于命令式编程语言的程序员来说,压根没听过啊。其实也是一种设计模式,就像装饰器和适配器一样。单子是一种数学模式,它通过封装程序逻辑,保持函数式的纯粹性以及提供一个强大的组合工具以组合使用提供类型的计算来控制副作用的执行。比较晦涩难懂,还需要多看看官方文档才行。
我们定义一个Bind来提升类型,包装B,然后组合就像下面这样了。
static Task<C> Bind<B, C>(Task<B> b, Func<B, Task<C>> g)
{
return g(b.Result);
}