软件事务内存导论(十一)-STM的局限性

简介:

1.1    STM的局限性

STM消除了显式的同步操作,所以我们在写代码时就无需担心自己是否忘了进行同步或是否在错误的层级上进行了同步。然而STM本身也存在一些问题,比如在跨越内存栅栏失败或遭遇竞争条件时我们捕获不到任何有用的信息。我似乎可以听到你内心深处那个精明的程序员在抱怨“怎么会这样啊?”。确实,STM是有其局限性的,否则本书写到这里就应该结束了。STM只适用于写冲突非常少的应用场景,如果你的应用程序存在很多写操作竞争,那么我们就需要在STM之外寻找解决方案了。

下面让我们进一步讨论STM的局限性。STM提供了一种显式的锁无关编程模型,这种模型允许多个事务并发地运行,并且在没有发生冲突时所有事务都能毫无滞碍地运行,所以相对其他编程模型而言STM可以提供更好的并发性和线程安全方面的保障。当事务对相同对象或数据的写访问发生冲突时,只有一个事务能够顺利完成,其他事务都会被自动重做。这种重做机制延缓了写操作冲突时竞争失败的那些写者的执行,但却提升了读者和竞争操作的胜利者的执行速度。当对于相同对象的并发写操作不频繁时,其性能就不会受到太大影响。但是随着冲突的增多,程序整体性能将因此变得越来越差。

如果对相同数据有很高的写冲突概率,那么我们的应用程序轻则写操作变慢,重则会因为重试太多次而导致失败。目前在本章我们所看到的例子都是在展示STM的优势,但是在下面的例子中我们将会看到,虽然STM是易于使用的,但也并非在所有应用场景下都能得到理想的结果。

在4.2节的示例中,当多个线程同时访问多个目录时,我们使用AtomicLong来对文件大小的并发更新操作进行同步。此外,如果需要同时更新多个变量,我们也必须依赖同步才能完成。虽然表面看起来使用STM对这段代码进行重构似乎是个不错的选择,但大量的写冲突却使得STM不适用于这个应用场景。下面就让我们将上述计算目录大小的程序改用STM实现,并观察其运行结果是否如我们所预料的那么差。

在下面的代码中,我们没有使用AtomicLong,而是采用了Akka托管引用作为FileSizeWSTM的属性字段。


1 public class FileSizeWSTM {
2 private ExecutorService service;
3 final private Ref<Long> pendingFileVisits = new Ref<Long>(0L);
4 final private Ref<Long> totalSize = new Ref<Long>(0L);
5 final private CountDownLatch latch = new CountDownLatch(1);

为了保证安全性,pendingFileVisits的增减都需要在事务内完成。而在之前使用AtomicLong时,我们只需要简单调用incrementAndGet()函数和decrementAndGet()函数就行了。但是由于托管引用都是通用的(generic),没有专门针对数字类型的处理方法,所以我们还需要针对pendingFileVisits进行一些额外的加工,即把对于pendingFileVisits的操作封装到一个单独的函数里。


1 private long updatePendingFileVisits(final int value) {
2 return new Atomic<Long>() {
3 public Long atomically() {
4 pendingFileVisits.swap(pendingFileVisits.get() + value);
5 return pendingFileVisits.get();
6 }
7 }.execute();
8 }

在完成上述定义之后,访问目录和计算文件大小的函数就相对容易多了,我们只需要把程序中的AtomicLong替换成托管引用就好。


01 private void findTotalSizeOfFilesInDir(final File file) {
02 try {
03 if (!file.isDirectory()) {
04 new Atomic() {
05 public Object atomically() {
06 totalSize.swap(totalSize.get() + file.length());
07 return null;
08 }
09 }.execute();
10 } else {
11 final File[] children = file.listFiles();
12 if (children != null) {
13 for(final File child : children) {
14 Limitations of STM • 137
15 updatePendingFileVisits(1);
16 service.execute(new Runnable() {
17 public void run() {
18 findTotalSizeOfFilesInDir(child); }
19 });
20 }
21 }
22 }
23 if(updatePendingFileVisits(-1) == 0) latch.countDown();
24 } catch(Exception ex) {
25 System.out.println(ex.getMessage());
26 System.exit(1);
27 }
28 }

最后,我们还需要写一些创建executor服务池和使程序运行起来的代码:


01 private long getTotalSizeOfFile(final String fileName)
02 throws InterruptedException {
03 service = Executors.newFixedThreadPool(100);
04 updatePendingFileVisits(1);
05 try {
06 findTotalSizeOfFilesInDir(new File(fileName));
07 latch.await(100, TimeUnit.SECONDS);
08 return totalSize.get();
09 } finally {
10 service.shutdown();
11 }
12 }
13 public static void main(final String[] args) throws InterruptedException {
14 final long start = System.nanoTime();
15 final long total = new FileSizeWSTM().getTotalSizeOfFile(args[0]);
16 final long end = System.nanoTime();
17 System.out.println("Total Size: " + total);
18 System.out.println("Time taken: " + (end - start)/1.0e9);
19 }
20 }

由于我怀疑这段代码跑起来之后可能有问题,所以如果在程序中抓到事务失败所导致的异常,我就会结束掉整个应用程序。

根据事务的定义,如果变量的值在事务提交之前发生了改变,那么事务将会自动重做。在本例中,多个线程会同时竞争修改这两个可变变量,从而导致程序运行变慢或失败。我们可以在多个不同的目录上分别运行上述示例代码来进行观察,下面就列出了该示例程序在我的电脑上计算/etc和/usr这两个目录的输出结果:


1 Total file size for /etc
2 Total Size: 2266408
3 Time taken: 0.537082
4 Total file size for /usr
5 Too many retries on transaction 'DefaultTransaction', maxRetries = 1000
6 Too many retries on transaction 'DefaultTransaction', maxRetries = 1000
7 Too many retries on transaction 'DefaultTransaction', maxRetries = 1000
8 ...

从输出结果来看,STM版本对于/etc目录的计算结果与之前使用AtomicLong的那个版本是完全相同的。但是由于会产生过多的重试操作,所以STM版本的运行时间要比后者慢一个数量级。而遍历/usr目录的运行情况则更为糟糕,有相当多的事务超过了默认的最大重试限制。虽然我们的逻辑是一抓到异常就会立即终止整个程序,但由于多个事务是并发运行的,所以在程序真正停止之前我们还是能看到多条错误信息输出到控制台。

有个别评论家曾建议说是否用commute代替alter会对解决这个问题有所帮助。请回忆我们在6.4节中曾讨论过的在Clojure中用来修改托管引用的那三个函数。由于在事务失败之后不会进行重试,所以commute可以提供比alter更高的并发度。此外,commute也不会在没有hold住调用方事务的情况下就单独执行提交操作。然而单纯就计算目录大小这个程序而言,使用commute对性能的提升十分有限。在面对结构复杂的大型目录时,使用该函数也无法在提供良好性能的前提下获得一致性的结果。除了将alter换成commute之外,我们还可以尝试将atom与swap!函数一起使用。虽然atom是不可调整并且同步的操作,但其优点是不需要使用事务。此外,atom仅能在对单个变量(例如计算目录大小示例中用于记录目录大小的变量)的变更时使用,并且变更期间不会遇到任何事务性重试。然而,由于在对atom做变更时会产生对用户透明的同步操作,所以我们依然会遇到同步操作所导致的延迟问题。

由于大量线程会同时尝试更新totalSize变量,所以计算目录大小示例在实际执行过程中会产生非常频繁的写冲突,这也就意味着STM不适合于解决此问题。事实上,当读操作十分频繁且写冲突被控制在合理范围内时,STM的性能还是不错的,同时还能帮程序员免除显式同步的负担。但是在不考虑一般程序中常见的其他导致延时问题的前提下,如果待解决问题中含有大量写冲突,那就请不要使用STM,而是考虑采用我们在第8章中将会讨论的actor模型来避免同步操作。

1.1    小结

STM是一个针对并发问题的非常强大的编程模型,该模型有很多优点:

  • STM可以根据应用程序的行为来充分挖掘出其最大的并发潜力。也就是说,用了STM之后,我们可以无需使用过度保守的、需要预先定义的同步操作,而是让STM动态地管理竞争冲突。
  • STM是一种锁无关的编程模型,该模型可以提供良好的线程安全性和很高的并发性能。
  • STM可以保证实体仅能在事务内被更改。
  • STM没有显式锁意味着我们从此无需担心加锁顺序及其他相关问题。
  • STM可以帮助我们减轻前期设计的决策负担,有了它我们就无需关心谁对什么东西上了锁,而只需放心地把这些工作交给动态隐式组合锁(implicit lock composition)。

该模型适用于对相同数据存在并发读且写冲突不频繁的应用场景。

如果应用程序的数据访问方式符合STM的适用范畴,则STM就为我们提供了一种处理共享可变性的高效解决方案。而如果我们的应用场景里写冲突非常多,我们可能就会更倾向于使用将在第8章中讨论的基于角色(actor)的模型。但在下一章,还是让我们先学习一下如何在其他JVM上的语言中使用STM编程模型。 

目录
相关文章
|
6月前
|
存储 Linux 程序员
Linux内存管理宏观篇(二):不同角度去看内存(软件)
Linux内存管理宏观篇(二):不同角度去看内存(软件)
94 0
|
2月前
|
C语言 Android开发 C++
基于MTuner软件进行qt的mingw编译程序的内存泄漏检测
本文介绍了使用MTuner软件进行Qt MinGW编译程序的内存泄漏检测的方法,提供了MTuner的下载链接和测试代码示例,并通过将Debug程序拖入MTuner来定位内存泄漏问题。
基于MTuner软件进行qt的mingw编译程序的内存泄漏检测
|
3月前
|
设计模式 uml
在电脑主机(MainFrame)中只需要按下主机的开机按钮(on()),即可调用其它硬件设备和软件的启动方法,如内存(Memory)的自检(check())、CPU的运行(run())、硬盘(Hard
该博客文章通过一个电脑主机启动的示例代码,展示了外观模式(Facade Pattern)的设计模式,其中主机(MainFrame)类通过调用内部硬件组件(如内存、CPU、硬盘)和操作系统的启动方法来实现开机流程,同时讨论了外观模式的优缺点。
|
4月前
|
Linux 调度
部署02-我们一般接触的是Mos和Wimdows这两款操作系统,很少接触到Linux,操作系统的概述,硬件是由计算机系统中由电子和机械,光电元件所组成的,CPU,内存,硬盘,软件是用户与计算机接口之间
部署02-我们一般接触的是Mos和Wimdows这两款操作系统,很少接触到Linux,操作系统的概述,硬件是由计算机系统中由电子和机械,光电元件所组成的,CPU,内存,硬盘,软件是用户与计算机接口之间
|
5月前
|
监控 Rust 安全
Rust代码在公司电脑监控软件中的内存安全监控
使用 Rust 语言开发的内存安全监控软件在企业中日益重要,尤其对于高安全稳定性的系统。文中展示了如何用 Rust 监控内存使用:通过获取向量长度和内存大小来防止泄漏和溢出。此外,代码示例还演示了利用 reqwest 库自动将监控数据提交至公司网站进行实时分析,以保证系统的稳定和安全。
212 2
|
Rust 监控 并行计算
用Rust构建电脑网络监控软件:内存安全性和多线程编程
在当今数字化世界中,网络安全一直是至关重要的问题。电脑网络监控软件是确保网络系统安全和高效运行的关键工具。然而,编写电脑网络监控软件需要处理复杂的多线程编程和内存安全性问题。Rust编程语言提供了一种强大的方式来构建安全的电脑网络监控软件,同时避免了许多常见的编程错误。
344 0
|
6月前
|
监控 算法 搜索推荐
C++内部监控软件:内存管理与性能调优的完美结合
在当今高度竞争的软件开发领域,内存管理和性能调优是构建高效应用的两个关键方面。本文将介绍一种基于C++的内部监控软件,通过结合精细的内存管理和有效的性能调优,实现了出色的应用性能。我们将深入探讨一些示例代码,演示如何在代码层面实现内存管理和性能优化,最后介绍如何将监控到的数据自动提交到网站。
312 1
|
6月前
|
存储 缓存 安全
从软件和硬件角度去看内存
从软件和硬件角度去看内存
95 0
|
存储 数据库 C语言
Hawkeyes: x86软件迁移Arm的弱内存序问题解决方案
本文介绍了x86软件迁移到Arm过程中可能遇到的弱内存序问题的解决方案,解析了弱内存序问题的根因,介绍了Hawkeyes的架构和实现原理。欢迎有需求的团队发送邮件咨询
1246 0
|
存储 Unix Shell
软件运行机制及内存管理
软件运行机制及内存管理
173 0