如何最大限度地降低多线程C#代码的复杂性

分支或多线程编程是编程时最难最对的事情之一。这是由于它们的并行性质所致,即要求采用与使用单线程的线性编程完全不同的思维模式。对于这个问题,恰当类比就是抛接杂耍表演者,必须在空中抛接多个球,而不要让它们相互干扰。这是一项重大挑战。然而,通过正确的工具和思维模式,这项挑战是能应对的。

本文将深入介绍我为了简化多线程编程和避免争用条件、死锁等其他问题而编写的一些工具。可以说,工具链以语法糖和神奇委托为依据。不过,引用伟大的爵士音乐家 Miles Davis 的话:“在音乐中,没有声音比有声音更重要。” 声音间断就产生了奇迹。

从另一个角度来说,不一定是关乎可以编码什么,而是关乎可以选择不编码什么,因为你希望通过间断代码行产生一点奇迹。引用 Bill Gates 的一句话:“根据代码行数来衡量工作质量就像通过重量来衡量飞机质量一样。” 因此,我希望能帮助开发人员减少编码量,而不是教导开发人员如何编写更多代码。

同步挑战

在多线程编程方面遇到的第一个问题是,同步对共享资源的访问权限。当两个或多个线程共享对某个对象的访问权限且可能同时尝试修改此对象时,就会出现这个问题。当 C# 首次发布时,lock 语句实现了一种基本方法,可确保只有一个线程能访问指定资源(如数据文件),且效果很好。C# 中的 lock 关键字很容易理解,它独自颠覆了我们对这个问题的思考方式。

不过,简单的 lock 存在一个主要缺陷:它不区分只读访问权限和写入访问权限。例如,可能要从共享对象中读取 10 个不同的线程,并且通过 System.Threading 命名空间中的 ReaderWriterLockSlim 类授权这些线程同时访问实例,而不导致问题发生。与 lock 语句不同,此类可便于指定代码是将内容写入对象,还是只从对象读取内容。这样一来,多个读取器可以同时进入,但在其他所有读写线程均已完成自己的工作前,拒绝任何写入代码访问。

现在的问题是:如果使用 ReaderWriterLock 类,语法就会变得很麻烦,大量的重复代码既降低了可读性,又随时间变化增加了维护复杂性,并且代码中通常会分散有多个 try 和 finally 块。即使是简单的拼写错误,也可能会带来日后有时极难发现的灾难性影响。

通过将 ReaderWriterLockSlim 封装到简单的类中,这个问题瞬间解决,不仅重复代码不再会出现,而且还降低了小拼写错误毁一天劳动成果的风险。图 1 中的类完全基于 lambda 技巧。可以说,这就是对一些委托应用的语法糖(假设存在几个接口)。最重要的是,它在很大程度上有助于实现避免重复代码原则 (DRY)。

1:封装 ReaderWriterLockSlim

public class Synchronizer<TImpl, TIRead, TIWrite> where TImpl : TIWrite, TIRead {
  ReaderWriterLockSlim _lock = new ReaderWriterLockSlim ();
  TImpl _shared;

  public Synchronizer (TImpl shared) {
    _shared = shared;
  }

  public void Read (Action<TIRead> functor) {
    _lock.EnterReadLock ();
    try {
      functor (_shared);
    } finally {
      _lock.ExitReadLock ();
    }
  }

  public void Write (Action<TIWrite> functor) {
    _lock.EnterWriteLock ();
    try {
      functor (_shared);
    } finally {
      _lock.ExitWriteLock ();
    }
  }
}

1 中只有 27 行代码,但却精妙简洁地确保对象跨多个线程进行同步。此类假定类型中有读取接口和写入接口。如果由于某种原因而无法更改需要将访问权限同步到的基础类实现,也可以重复模板类本身三次,通过这种方式使用它。基本用法如 2 所示。

2:使用 Synchronizer 类

interface IReadFromShared {
  string GetValue ();
}

interface IWriteToShared {
  void SetValue (string value);
}

class MySharedClass : IReadFromShared, IWriteToShared {
  string _foo;

  public string GetValue () {
    return _foo;
  }

  public void SetValue (string value) {
    _foo = value;
  }
}

void Foo (Synchronizer<MySharedClass, IReadFromShared, IWriteToShared> sync) {
  sync.Write (x => {
    x.SetValue ("new value");
  });
  sync.Read (x => {
    Console.WriteLine (x.GetValue ());
  })
}

在 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

public void ExecuteParallel_1 () {
  var sync = new Synchronizer<string, string, string> ("initial_");

  var actions = new Actions ();
  actions.Add (() => sync.Assign ((res) => res + "foo"));
  actions.Add (() => sync.Assign ((res) => res + "bar"));

  actions.ExecuteParallel ();

  string result = null;
  sync.Read (delegate (string val) { result = val; });
  Assert.AreEqual (true, "initial_foobar" == result || result == "initial_barfoo");
}

仔细看看这段代码便会发现,计算结果并未假定我的两个 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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • c#使用多线程的几种方式示例详解

    (1)不需要传递参数,也不需要返回参数 ThreadStart是一个委托,这个委托的定义为void ThreadStart(),没有参数与返回值. 复制代码 代码如下: class Program { static void Main(string[] args) { for (int i = 0; i < 30; i++) { ThreadStart threadStart = new ThreadStart(Calculate); Thread thread = new Thread(thr

  • C#实现多线程下载文件的方法

    本文实例讲述了C#实现多线程下载文件的方法.分享给大家供大家参考.具体实现方法如下: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Threading; using System.Net; namespace WfpApp { public class MultiDownload { #region 变量 pri

  • 浅解关于C#多线程的介绍

    多线程的相关概念--------------------------------------------------------------------------------1.进程:是操作系统结构的基础:是一个正在执行的程序:计算机中正在运行的程序实例:可以分配给处理器并由处理器执行的一个实体:由单一顺序的执行显示,一个当前状态和一组相关的系统资源所描述的活动单元.2.线程:线程是程序中一个单一的顺序控制流程.是程序执行流的最小单元.另外,线程是进程中的一个实体,是被系统独立调度和分派的基本

  • c#多线程编程基础

    无论您是为具有单个处理器的计算机还是为具有多个处理器的计算机进行开发,您都希望应用程序为用户提供最好的响应性能,即使应用程序当前正在完成其他工作.要使应用程序能够快速响应用户操作,同时在用户事件之间或者甚至在用户事件期间利用处理器,最强大的方式之一是使用多线程技术. 多线程:线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程.如果某个线程进行一次长延迟操作, 处理器就切换到另一个线程执行.这样,多个线程的并行(并发)执行隐藏了长延迟,提高了处理器资源利用率

  • c#多线程的应用全面解析

    1.使用多线程的几种方式 (1)不需要传递参数,也不需要返回参数 ThreadStart是一个委托,这个委托的定义为void ThreadStart(),没有参数与返回值. 复制代码 代码如下: class Program { static void Main(string[] args) { for (int i = 0; i < 30; i++) { ThreadStart threadStart = new ThreadStart(Calculate); Thread thread = n

  • 浅谈C#多线程简单例子讲解

    .NET将关于多线程的功能定义在System.Threading名字空间中.因此,要使用多线程,必须先声明引用此名字空间(using System.Threading;). a.启动线程 顾名思义,"启动线程"就是新建并启动一个线程的意思,如下代码可实现: Thread thread1 = new Thread(new ThreadStart( Count)); 其中的 Count 是将要被新线程执行的函数. b.杀死线程 "杀死线程"就是将一线程斩草除根,为了不白

  • c# 多线程编程 入门篇

    开始本应该是一篇洋洋洒洒的文字, 不过我还是提倡先做起来, 在尝试中去理解. 先试试这个: procedure TForm1.Button1Click(Sender: TObject);var i: Integer;begin for i := 0 to 500000 do begin Canvas.TextOut(10, 10, IntToStr(i)); end;end; 上面程序运行时, 我们的窗体基本是 "死" 的, 可以在你在程序运行期间拖动窗体试试... Delphi 为我

  • 如何最大限度地降低多线程C#代码的复杂性

    分支或多线程编程是编程时最难最对的事情之一.这是由于它们的并行性质所致,即要求采用与使用单线程的线性编程完全不同的思维模式.对于这个问题,恰当类比就是抛接杂耍表演者,必须在空中抛接多个球,而不要让它们相互干扰.这是一项重大挑战.然而,通过正确的工具和思维模式,这项挑战是能应对的. 本文将深入介绍我为了简化多线程编程和避免争用条件.死锁等其他问题而编写的一些工具.可以说,工具链以语法糖和神奇委托为依据.不过,引用伟大的爵士音乐家 Miles Davis 的话:"在音乐中,没有声音比有声音更重要.&

  • ASP.NET:一段比较经典的多线程学习代码

    一段比较经典的多线程学习代码. 1.用到了多线程的同步问题. 2.用到了多线程的顺序问题. 如果有兴趣的请仔细阅读下面的代码.注意其中代码段的顺序,思考一下,这些代码的顺序能否互相调换,为什么?这应该对学习很有帮助的.为了演示,让所有的线程都Sleep了一段时间. using System.Net;using System;using System.IO;using System.Text;using System.Threading;using System.Diagnostics; name

  • java多线程中断代码详解

    一.java中终止线程主要有三种方法: ①线程正常退出,即run()方法执行完毕了 ②使用Thread类中的stop()(已过期不推荐使用)方法强行终止线程. ③使用中断机制 t.stop()调用时,终止线程,会导致该线程所持有的锁被强制释放,从而被其他线程所持有,因此有可能导致与预期结果不一致.下面使用中断信号量中断非阻塞状态的线程中: public class TestStopThread { public static void main(String[] args) throws Int

  • Java多线程同步器代码详解

    同步器 为每种特定的同步问题提供了解决方案,同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作.最常用的同步器是CountDownLatch和Semaphore,不常用的是Barrier 和Exchanger Semaphore Semaphore[信号标:旗语],通过计数器控制对共享资源的访问. 测试类: package concurrent; import concurrent.thread.SemaphoreThread; import java.util.concurrent.

  • python多线程实现代码(模拟银行服务操作流程)

    1.模拟银行服务完成程序代码 目前,在以银行营业大厅为代表的窗口行业中大量使用排队(叫号)系统,该系统完全模拟了人群排队全过程,通过取票进队.排队等待.叫号服务等功能,代替了人们站队的辛苦. 排队叫号软件的具体操作流程为: 顾客取服务序号 当顾客抵达服务大厅时,前往放置在入口处旁的取号机,并按一下其上的相应服务按钮,取号机会自动打印出一张服务单.单上显示服务号及该服务号前面正在等待服务的人数. 服务员工呼叫顾客 服务员工只需按一下其柜台上呼叫器的相应按钮,则顾客的服务号就会按顺序的显示在显示屏上

  • 在IntelliJ IDEA中多线程并发代码的调试方法详解

    通常来说,多线程的并发及条件断点的debug是很难完成的,或许本篇文章会给你提供一个友好的调试方法.让你在多线程开发过程中的调试更加的有的放矢. 我们将通过一个例子来学习.在这里,我编写了一个多线程程序来计算此数学问题:100! + 100000!.即:100的阶乘 + 100000的阶乘. 数学不好的同学看这里,100 阶乘就是:1 * 2 * 3 * -- * 100 = ? ,简写为100! import java.math.BigInteger; public class MathPro

  • pandas apply多线程实现代码

    一.多线程化选择 并行化一个代码有两大选择:multithread 和 multiprocess. Multithread,多线程,同一个进程(process)可以开启多个线程执行计算.每个线程代表了一个 CPU 核心,这么多线程可以访问同样的内存地址(所谓共享内存),实现了线程之间的通讯,算是最简单的并行模型. Multiprocess,多进程,则相当于同时开启多个 Python 解释器,每个解释器有自己独有的数据,自然不会有数据冲突. 二.并行化思想 并行化的基本思路是把 dataframe

  • Python 线程池模块之多线程操作代码

    1.线程池模块 引入 from concurrent.futures import ThreadPoolExecutor 2.使用线程池 一个简单的线程池使用案例 from concurrent.futures import ThreadPoolExecutor import time pool = ThreadPoolExecutor(10, 'Python') def fun(): time.sleep(1) print(1, end='') if __name__ == '__main__

  • 批处理程序中的“多线程”处理代码

    如下内容将简单举例,在WINDOWS下使用批处理做多进程并发,以达到"多线程"的效果. 例:需要PING 192.168.0.1到192.168.0.254的所有IP地址,并将是否能PING通的结果输出到log.txt 实现如下: rem 主程序.bat,只需一行代码,调用checkip.bat,经测试,CPU2.6G MEM1G可并发40个左右的进程 复制代码 代码如下: for /l %%i in (1,1,254) do (start /min cmd /c checkip.ba

  • android downsample降低音频采样频率代码

    使用Android AudioRecord 录制PCM文件,android SDK保证在所有设备上都支持的采样频率只有44100HZ,所以如果想得到其他采样频率的PCM数据,有几种方式:1.在设备上尝试可用的采样频率,2.使用44.1K采样后转换采样频率. 其中第二种转换采样频率的操作,有很多种方法.目前我使用的是SSRC,效果很好. 复制代码 代码如下: private void simpleDownSample() {        File BeforeDownSampleFile = n

随机推荐