网站建设预算策划,鄂州seo厂家,自己做游戏app的网站,企业网站asp分支或多线程编程是编程时最难最对的事情之一。这是由于它们的并行性质所致#xff0c;即要求采用与使用单线程的线性编程完全不同的思维模式。对于这个问题#xff0c;恰当类比就是抛接杂耍表演者#xff0c;必须在空中抛接多个球#xff0c;而不要让它们相互干扰。这是一… 分支或多线程编程是编程时最难最对的事情之一。这是由于它们的并行性质所致即要求采用与使用单线程的线性编程完全不同的思维模式。对于这个问题恰当类比就是抛接杂耍表演者必须在空中抛接多个球而不要让它们相互干扰。这是一项重大挑战。然而通过正确的工具和思维模式这项挑战是能应对的。本文将深入介绍我为了简化多线程编程和避免争用条件、死锁等其他问题而编写的一些工具。可以说工具链以语法糖和神奇委托为依据。不过引用伟大的爵士音乐家 Miles Davis 的话“在音乐中没有声音比有声音更重要。” 声音间断就产生了奇迹。从另一个角度来说不一定是关乎可以编码什么而是关乎可以选择不编码什么因为你希望通过间断代码行产生一点奇迹。引用 Bill Gates 的一句话“根据代码行数来衡量工作质量就像通过重量来衡量飞机质量一样。” 因此我希望能帮助开发人员减少编码量而不是教导开发人员如何编写更多代码。同步挑战在多线程编程方面遇到的第一个问题是同步对共享资源的访问权限。当两个或多个线程共享对某个对象的访问权限且可能同时尝试修改此对象时就会出现这个问题。当 C# 首次发布时lock 语句实现了一种基本方法可确保只有一个线程能访问指定资源如数据文件且效果很好。C# 中的 lock 关键字很容易理解它独自颠覆了我们对这个问题的思考方式。不过简单的 lock 存在一个主要缺陷它不区分只读访问权限和写入访问权限。例如可能要从共享对象中读取 10 个不同的线程并且通过 System.Threading 命名空间中的 ReaderWriterLockSlim 类授权这些线程同时访问实例而不导致问题发生。与 lock 语句不同此类可便于指定代码是将内容写入对象还是只从对象读取内容。这样一来多个读取器可以同时进入但在其他所有读写线程均已完成自己的工作前拒绝任何写入代码访问。现在的问题是如果使用 ReaderWriterLock 类语法就会变得很麻烦大量的重复代码既降低了可读性又随时间变化增加了维护复杂性并且代码中通常会分散有多个 try 和 finally 块。即使是简单的拼写错误也可能会带来日后有时极难发现的灾难性影响。 通过将 ReaderWriterLockSlim 封装到简单的类中这个问题瞬间解决不仅重复代码不再会出现而且还降低了小拼写错误毁一天劳动成果的风险。图 1 中的类完全基于 lambda 技巧。可以说这就是对一些委托应用的语法糖假设存在几个接口。最重要的是它在很大程度上有助于实现避免重复代码原则 (DRY)。图 1封装 ReaderWriterLockSlim 1 public class SynchronizerTImpl, TIRead, TIWrite where TImpl : TIWrite, TIRead { 2 ReaderWriterLockSlim _lock new ReaderWriterLockSlim (); 3 TImpl _shared; 4 5 public Synchronizer (TImpl shared) { 6 _shared shared; 7 } 8 9 public void Read (ActionTIRead functor) {10 _lock.EnterReadLock ();11 try {12 functor (_shared);13 } finally {14 _lock.ExitReadLock ();15 }16 }17 18 public void Write (ActionTIWrite functor) {19 _lock.EnterWriteLock ();20 try {21 functor (_shared);22 } finally {23 _lock.ExitWriteLock ();24 }25 }26 }图 1 中只有 27 行代码但却精妙简洁地确保对象跨多个线程进行同步。此类假定类型中有读取接口和写入接口。如果由于某种原因而无法更改需要将访问权限同步到的基础类实现也可以重复模板类本身三次通过这种方式使用它。基本用法如图 2 所示。图 2使用 Synchronizer 类 1 interface IReadFromShared { 2 string GetValue (); 3 } 4 5 interface IWriteToShared { 6 void SetValue (string value); 7 } 8 9 class MySharedClass : IReadFromShared, IWriteToShared {10 string _foo;11 12 public string GetValue () {13 return _foo;14 }15 16 public void SetValue (string value) {17 _foo value;18 }19 }20 21 void Foo (SynchronizerMySharedClass, IReadFromShared, IWriteToShared sync) {22 sync.Write (x {23 x.SetValue (new value);24 });25 sync.Read (x {26 Console.WriteLine (x.GetValue ());27 })28 }在图 2 的代码中无论有多少线程在执行 Foo 方法只要执行另一个 Read 或 Write 方法就不会调用 Write 方法。不过可以同时调用多个 Read 方法而不必在代码中分散多个 try/catch/finally 语句也不必不断重复相同的代码。我在此郑重声明通过简单字符串来使用它是没有意义的因为 System.String 不可变。我使用简单的字符串对象来简化示例。基本思路是必须将所有可以修改实例状态的方法都添加到 IWriteToShared 接口中。同时应将所有只从实例读取内容的方法都添加到 IReadFromShared 接口中。通过将诸如此类的问题分散到两个不同的接口并对基础类型实现这两个接口可使用 Synchronizer 类来同步对实例的访问权限。这样一来将访问权限同步到代码的做法变得更简单并且基本上可以通过更具声明性的方式这样做。在多线程编程方面语法糖可能会决定成败。调试多线程代码通常极为困难并且创建同步对象的单元测试可能会是徒劳无功之举。如果需要可以创建只包含一个泛型参数的重载类型不仅继承自原始 Synchronizer 类还将它的一个泛型参数作为类型参数三次传递到它的基类。这样一来就不需要读取接口或写入接口了因为可以直接使用类型的具体实现。不过这种方法要求手动处理需要使用 Write 或 Read 方法的部分。此外虽然它的安全性稍差一点但确实可便于将无法更改的类包装到 Synchronizer 实例中。用于分支的 lambda 集合迈出第一步来使用神奇的 lambda或在 C# 中称为“委托”后不难想象可以利用它们完成更多操作。例如反复出现的常见多线程主题是让多个线程与其他服务器联系以提取数据并将数据返回给调用方。最简单的例子就是应用程序从 20 个网页读取数据并在完成后将 HTML 返回给一个根据所有网页的内容创建某种聚合结果的线程。除非为每个检索方法都创建一个线程否则此代码的运行速度比预期慢得多99% 的所有执行时间可能会花在等待 HTTP 请求返回上。在一个线程上运行此代码的效率很低并且线程创建语法非常容易出错。随着你支持多个线程及其助理对象挑战变得更严峻开发人员不得不在编写代码时使用重复代码。意识到可以创建委托集合和用于包装这些委托的类后便能使用一个方法调用来创建所有线程。这样一来创建线程就轻松多了。图 3 中的一段代码创建两个并行运行的此类 lambda。请注意此代码实际上来自我的第一版 Lizzie 脚本语言的单元测试 (bit.ly/2FfH5y8)。图 3创建 lambda 1 public void ExecuteParallel_1 () { 2 var sync new Synchronizerstring, string, string (initial_); 3 4 var actions new Actions (); 5 actions.Add (() sync.Assign ((res) res foo)); 6 actions.Add (() sync.Assign ((res) res bar)); 7 8 actions.ExecuteParallel (); 9 10 string result null;11 sync.Read (delegate (string val) { result val; });12 Assert.AreEqual (true, initial_foobar result || result initial_barfoo);13 }仔细看看这段代码便会发现计算结果并未假定我的两个 lambda 的执行存先后顺序。执行顺序并未明确指定并且这些 lambda 是在不同的线程上执行。这是因为使用图 3 中的 Actions 类可以向它添加委托这样稍后就能决定是要并行执行委托还是按顺序执行委托。为此必须使用首选机制创建并执行许多 lambda。在图 3 中可以看到前面提到的 Synchronizer 类用于同步对共享字符串资源的访问权限。不过它对 Synchronizer 使用了新方法 Assign我并未在图 1中的列表内为 Synchronizer 类添加此方法。Assign 方法使用前面 Write 和 Read 方法中使用的相同“lambda 技巧”。若要研究 Actions 类的实现请务必下载 Lizzie 版本 0.1因为我在后面推出的版本中完全重写了代码使之成为独立编程语言。C# 中的函数式编程大多数开发人员往往认为C# 几乎与面向对象的编程 (OOP) 同义或至少密切相关事实显然如此。不过通过重新思考如何使用 C#并深入了解它的各方面功能解决一些问题就变得更加简单了。目前形式的 OOP 不太易于重用原因很多是因为它是强类型。例如如果重用一个类就不得不重用初始类引用的每个类在两种情况下类都是通过组合和继承进行使用。此外类重用还会强制重用这些第三方类引用的所有类等。如果这些类是在不同的程序集中实现必须添加各种各样的程序集才能获得对一个类型上单个方法的访问权限。我曾经看过一个可以说明这个问题的类比“虽然想要的是香蕉但最终得到的是手拿香蕉的大猩猩以及大猩猩所居住的热带雨林。” 将这种情况与使用更动态的语言如 JavaScript进行重用做比较后者并不关心类型只要它实现函数本身使用的函数即可。通过略微宽松类型方法生成的代码更灵活、更易于重用。委托可以实现这一点。可使用 C# 来改善跨多个项目重用代码的过程。只需要理解函数或委托也可以是对象并且可以通过弱类型方式控制这些对象的集合。早在 2018 年 11 月发行的《MSDN 杂志》中我发表过一篇标题为“使用符号委托创建你自己的脚本语言”的文章 (msdn.com/magazine/mt830373)。本文中提到的有关委托的思路是在这篇文章的基础之上形成。本文还介绍了 Lizzie这是我的自制脚本语言它的存在归功于这种以委托为中心的思维模式。如果我使用 OOP 规则创建了 Lizzie我会认为它在大小上可能至少大一个数量级。当然如今 OOP 和强类型处于主导地位想要找到一个主要必需技能不要求它的职位描述几乎是不可能的。我在此郑重声明我创建 OOP 代码的时间已超过 25 年所以我与任何人一样都会因为对强类型有偏见而感到内疚。然而如今我在编码方法上更加务实对类层次结构的最终外观失去兴趣。并不是我不欣赏外观精美的类层次结构而是收益递减。添加到层次结构中的类越多它就变得越臃肿直到因不堪重压而崩溃。有时卓越的设计只用很少的方法、更少的类和大多数松散耦合的函数这样就可以轻松扩展代码也就不需要“引入大猩猩和热带雨林”了。回到本文反复出现的主题从 Miles Davis 的音乐方法中获得灵感少即是多“没有声音比有声音更重要”。 代码也不例外。间断代码行往往会产生奇迹最佳解决方案的衡量依据更多是不编码什么而不是编码什么。连傻瓜也可以将喇叭吹响但只有为数不多的人才能用喇叭吹奏出音乐。像 Miles 这样能创造出奇迹的人就更少了。 原文作者Thomas Hansen原文地址Minimize Complexity in Multithreaded C# Code