在.NET Workflo“.NET研究”w 3.5中使用多线程提高工作流性能

简介:   最近在工作上碰到一个性能问题,由于项目是基于SOA的架构,使得整个系统完全依赖于各种各样的Serv上海闵行企业网站制作ice,其中用于处理业务逻辑的Business Services全部都用.NET Workflow 3.5实现(历史原因,项目还没升级到Workflow 4)。

  最近在工作上碰到一个性能问题,由于项目是基于SOA的架构,使得整个系统完全依赖于各种各样的Serv上海闵行企业网站制作ice,其中用于处理业务逻辑的Business Services全部都用.NET Workflow 3.5实现(历史原因,项目还没升级到Workflow 4)。在众多的Business Service中,其中有一个的主要功能是,通过调用不同的Data Service来获取数据,然后根据业务逻辑来组织这些数据并返回给它的调用者。该Business Service的工作流(Workflow)主要包含三个活动组件(Activity),大致可以用下图表示:

image

  需要说明一下,在实际项目中,这个Workflow本身不仅仅只是简单地包含上面三个Activity,通过性能测试的数据分析,瓶颈就在这三个Activity上,而每个Activity的执行时间又主要消耗在反复调用Data Service上。在此,为了简化问题的描述,我把其它不相干的Activity剔除了,于是就得到了上图的结构。

  图中的三个Activity都会分别去调用不同的Data Service来获得数据,尤其在getNotesActivity中,Data Service会被循环调用,这使得系统性能大打折扣。原本有一个解决方案可以在一定程度上提高getNotesActivity的效率,就是修改被调用的Data Service,使得它能够一次性接收多个request的数据,处理完之后再将所有的结果一次性返回,这样就避免了Data Service的循环调用,有效地减少了数据在网络上的来回次数。但是,这种解决方案需要更改Data Service的Request Schema,这个改动是很大的,因为可能有很多其它的Business Service都在调用这个Data Service,牵涉的范围太广了。

  根据实际项目,稍加分析不难发现,这个Workflow中的Activity有如下几个特点:

  • 三个Activity的输入属性参数都来自于Workflow(即通过与Workflow中定义的DependencyProperty进行绑定而获得数据),并不存在下游的Activity的输入参数需要依赖上游Activity的输出参数的情况
  • 每个Activity的输出属性参数都只关注某一种类型的数据,在Workflow Runtime执行完某个Activity后,也会通过DependencyProperty将处理结果传递给Workflow,而这些输出属性参数之间也并没有任何关联
  • 三个Activity所调用的Data Service也比较独立,基本上可以说是在做三个完全不同的工作
  • 时间主要消耗在Data Service的调用上,不存在由于复杂的运算逻辑导致CPU利用率近似100%的情况,也不存在由于物理内存用完而需要频繁读写虚拟内存的情形

  上述的几个特点中,第四点为我们引入多线程或并行任务处理提供了主要依据。这里需要额外岔开一下。有很多软件人员认为,多线程一定能够提高系统性能,因为事务可以分派到多个线程中进行并行处理。我觉得,应该这样去看待这个问题:首先,根据Martin Fowler的《企业应用架构模式》(也就是著名的PoEAA)一书中有关性能的讨论认为,有很多术语可以描述性能,比如:响应时间、响应性、等待时间、吞吐率、负载、负载敏感度上海企业网站设计与制作等。假设完成某个任务需要的时间很长,比如需要5秒,那么其响应时间就是5秒,而如果让用户等待这5秒过去后,再将系统的控制权交给用户,就会让用户明显感觉很不顺上海网站建设,于是响应性就很差;但如果能将这个任务交给另一个执行体去处理,而程序本身直接将系统控制权交给用户,等那个执行体完成任务处理后,再将结果提供给用户,那么,同样处理这个任务需要5秒钟,这种方式的响应性就明显要好于前者,这也是我们以前做Windows Forms开发的时候,要把耗时的处理交给另一个线程处理,以不至于因为主线程的阻塞而导致界面冻结的尴尬局面。因此,多线程的引入,可以提高系统的响应性。

  其次,多线程是否能够提高系统的响应时间?这也未必,在单核处理器上,多线程是采用时间片轮循的方式实现的,也就是说,相同时间点上,只有一个线程在执行,只不过是时间片足够小,轮循频率足够高,才让我们感觉线程是并行执行的,在这样一种体系结构下,完成任务的处理还是需要那么长时间,甚至时间片的切换倒还会带来额外的开销。在多核系统中,或许真的可以提高响应时间,不过我目前没有实际的测试数据用来比较,因此在这个问题上,我还没有足够的发言权。

  而对于目前项目的情况,Data Service是分布在网络上不同位置的资源,如果能让这些Data Service同时处理数据请求,再让Business Service去组织分别来自这些Data Service的处理结果,那么整个Business Service的响应时间是可以明显提高的,响应时间提高了,响应性也同样提高了。假设第一个Activity耗时t1,第二个Activity耗时t2,第三个Activity耗时t3,那么,如果按上图中的顺序方式执行,Business Service的响应时间就是t1+t2+t3。但如果让这些Activity并行处理(也就相当于并行调用Data Service使其同时处理数据请求),那么Business Service的响应时间应该就是max(t1, t2, t3)。

  于是,我打算将上述的Workflow修改一下,采用多线程的方式来分别运行每个Activity,最后再将结果汇总。我修改后的Workflow如下所示:

img_1bb7ebf07d27c3a3b248e0c36c9a657f.png上海闵行企业网站设计与制作"0" alt="image" width="579" height="349" />

  在此需要对ParallelActivity说明一下。.NET Workflow 3.5的ParallelActivity并没有做到所谓的并行执行,因为Workflow Runtime是在单独的线程上执行Workflow Instance的,因此,要让多个Activity真正并行执行是做不到的。ParallelActivity的真正用意在于协调每个分支中的SequenceActivity(注意:ParallelActivity的每个分支只能接收一个SequenceActivity),使得其中的每个Activity都有一次执行的机会。

  某个分支中的一个活动执行过后,就会轮到下一个分支。当这个分支执行了一个活动后,执行又会转移到再下一个分支,以此类推。当所有分支都有了执行机会之后,又会从第一个(最左侧)分支开始这一过程,继续执行第一个分支的下一个活动(如果存在的话)。因此,在我们的这个例子中,完全可以不用ParallelActivity,而仍然选择原来的结构即可。之前我并没有完全清楚地了解ParallelActivity,开始一直以为ParallelActivity的意思是,让Workflow Runtime同时安排(Schedule)每个分支的执行,以便当每个分支都以异步方式运行时,所有的分支可以实现并行处理。

  不过也不要紧,在这里使用ParallelActivity,虽然没有有效地利用它的特性,但与原来的Workflow相比,从可读性上讲,这种结构更容易让人觉得这是一种并行的运行方式。

  另一个变化是,原本每个操作都是写在一个自定义的Activity中的,通过重写Activity的Execute方法来做业务处理,而现在则是用CodeActivity来代替原来的Activity,这样做的好处是,可以将业务处理的代码放在同一个Context中,这也为线程同步提供了便利,降低了使用线程的复杂度。

  以下是改进后的Workflow的代码,供参考。

 
 
1 . using System;
2 . using System.Collections.Generic;
3 . using System.Threading;
4 . using System.Workflow.Activities;
5 . namespace WorkflowConsoleApplication3
6 . {
7 . public sealed partial class Workflow1 : SequentialWorkflowActivity
8 . {
上海徐汇企业网站设计与制作>
9 . List < Thread > threads = new List < Thread > ();
10 . public Workflow1()
11 . {
12 . InitializeComponent();
13 . }
14 . private void getAdditionalInfoActivity_Execute( object sender, EventArgs e)
15 . {
16 . var t1 = new Thread(() =>
17 . {
18 . // Call Data Service 1 to implement business logic...
19 . });
20 . threads.Add(t1);
21 . t1.Start();
22 . }
23 . private void getNotesActivity_Execute( object sender, EventArgs e)
24 . {
25 . var t2 = new Thread(() =>
26 . {
27 . // Call Data Service 2 in a loop to implement business
28 . // logic...
29 . });
30 . threads.Add(t2);
上海徐汇企业网站制作31 . t2.Start();
32 . }
33 .
34 . private void getSpecialPointsActivity_Execute( object sender, EventArgs e)
35 . {
36 . var t3 = new Thread(() =>
上海企业网站制作an>37. {
38. // Call Data Service 3 to implement business logic...
39. });
40. threads.Add(t3);
41. t3.Start();
42. }
43.
44. private void syncCodeActivity_Execute(object sender, EventArgs e)
45. {
46. // Wait for all threads to terminate...
47. threads.ForEach(p => p.Join());
48. // TODO: Process with results and exceptions
49. }
50. }
51. }
52. 从上面的代码中可以看到,每个

  从上面的代码中可以看到,每个CodeActivity在执行的时候都会启动一个线程,这个线程会调用相应的Data Service来实现其业务逻辑,线程创建以后,会被保存在一个线程列表里,用来在syncCodeActivity中进行线程同步。syncCodeActivity则通过线程的Join方法来等待所有线程全部完成各自的工作,最后对运行结果和异常进行处理。

  此处线程的运用需要遵循.NET线程使用的最佳实践,应该尽量避免线程的阻塞,在访问临界资源的时候应作加锁处理以防止状态异常。由于在这个例子中,每个线程又会牵涉到其它Service的调用,因此在线程中捕获的异常,我建议还是先将其记录下来,然后温和地直接使用return语句终止线程执行,而不是随意抛出异常而使得线程进入一个不确定的状态。当然,读者朋友如果在多线程环境中有处理异常的经验,也恳请在本文留言指导。

  对Workflow进行调整之后,重新编译、部署并运行这个Business Service,然后用已经写好的Client程序进行测试,我们得到了如下的结果(几个明显的噪音数据已经被划掉,没有包含在统计中)。从这个报表可以看到,针对我们的这个案例,在Workflow中引入多线程的确可以明显地提高系统性能。

image

目录
相关文章
|
3月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
102 1
|
3月前
|
SQL 缓存 开发框架
分享一个 .NET EF6 应用二级缓存提高性能的方法
分享一个 .NET EF6 应用二级缓存提高性能的方法
|
5月前
|
分布式计算 并行计算 安全
在Python Web开发中,Python的全局解释器锁(Global Interpreter Lock,简称GIL)是一个核心概念,它直接影响了Python程序在多线程环境下的执行效率和性能表现
【6月更文挑战第30天】Python的GIL是CPython中的全局锁,限制了多线程并行执行,尤其是在多核CPU上。GIL确保同一时间仅有一个线程执行Python字节码,导致CPU密集型任务时多线程无法充分利用多核,反而可能因上下文切换降低性能。然而,I/O密集型任务仍能受益于线程交替执行。为利用多核,开发者常选择多进程、异步IO或使用不受GIL限制的Python实现。在Web开发中,理解GIL对于优化并发性能至关重要。
60 0
|
2月前
|
缓存 Java 应用服务中间件
Java虚拟线程探究与性能解析
本文主要介绍了阿里云在Java-虚拟-线程任务中的新进展和技术细节。
|
5月前
|
Java
Java Socket编程与多线程:提升客户端-服务器通信的并发性能
【6月更文挑战第21天】Java网络编程中,Socket结合多线程提升并发性能,服务器对每个客户端连接启动新线程处理,如示例所示,实现每个客户端的独立操作。多线程利用多核处理器能力,避免串行等待,提升响应速度。防止死锁需减少共享资源,统一锁定顺序,使用超时和重试策略。使用synchronized、ReentrantLock等维持数据一致性。多线程带来性能提升的同时,也伴随复杂性和挑战。
95 0
|
2月前
|
Java 数据库 开发者
探索研究Ruby 多线程
【9月更文挑战第4天】
36 2
|
3月前
|
开发者 C# Android开发
Xamarin 与 .NET:解锁现代化移动应用开发的超级武器——深入探讨C#与.NET框架如何赋能跨平台应用,实现高效编码与卓越性能
【8月更文挑战第31天】Xamarin 与 .NET 的结合为开发者提供了强大的平台,用于构建现代化移动应用。通过 C# 和 .NET 框架,Xamarin 可以实现一次编写、多平台运行,覆盖 iOS、Android 和 Windows。这种方式不仅节省了开发时间和成本,还保证了应用的一致性和高质量。Xamarin 是一个开源框架,专为跨平台移动应用开发设计,允许使用 C# 语言和 .NET 核心库构建原生应用,并访问各平台特定功能。微软维护的 Xamarin 是 Visual Studio 生态系统的一部分,极大地提高了开发效率。
79 0
|
3月前
|
开发框架 缓存 .NET
【App Service】在Azure App Service中分析.NET应用程序的性能的好帮手(Review Stack Traces)
【App Service】在Azure App Service中分析.NET应用程序的性能的好帮手(Review Stack Traces)
|
4月前
|
安全 云计算
云计算自旋锁问题之在线程安全地删除链表节点时,需要频繁加锁会影响性能如何解决
云计算自旋锁问题之在线程安全地删除链表节点时,需要频繁加锁会影响性能如何解决
49 2
|
3月前
分享一份 .NET Core 简单的自带日志系统配置,平时做一些测试或个人代码研究,用它就可以了
分享一份 .NET Core 简单的自带日志系统配置,平时做一些测试或个人代码研究,用它就可以了