14、Scala并发编程:STM与Actor模型实践

Scala并发编程:STM与Actor模型实践

1. Scala中使用STM

1.1 选择STM方案

Scala是JVM上的静态类型语言,融合了面向对象和函数式编程风格。在Scala中使用软件事务内存(STM)有多种选择,可能会倾向于Akka解决方案,因为其提供的Scala API更流畅。不过,如果项目已经在使用Clojure STM,也可以在Scala中使用它。

1.2 使用Clojure STM

在Scala中使用Clojure STM与Java类似,可以使用 Ref 类和 LockingTransaction 类的 runInTransaction() 方法。以下是一个账户转账的示例:

import clojure.lang.Ref
import clojure.lang.LockingTransaction
import java.util.concurrent.Callable

class Account(val initialBalance : Int) {
  val balance = new Ref(initialBalance)
  def getBalance() = balance.deref

  def deposit(amount : Int) = {
    LockingTransaction.runInTransaction(new Callable[Boolean] {
      def call() = {
        if(amount > 0) {
          val currentBalance = balance.deref.asInstanceOf[Int]
          balance.set(currentBalance + amount)
          println("deposit " + amount + "... will it stay")
          true
        } else throw new RuntimeException("Operation invalid")
      }
    })
  }

  def withdraw(amount : Int) = {
    LockingTransaction.runInTransaction(new Callable[Boolean] {
      def call() = {
        val currentBalance = balance.deref.asInstanceOf[Int]
        if(amount > 0 && currentBalance >= amount) {
          balance.set(currentBalance - amount)
          true
        } else throw new RuntimeException("Operation invalid")
      }
    })
  }
}

def transfer(from : Account, to : Account, amount : Int) = {
  LockingTransaction.runInTransaction(new Callable[Boolean] {
    def call() = {
      to.deposit(amount)
      from.withdraw(amount)
      true
    }
  })
}

def transferAndPrint(from : Account, to : Account, amount : Int) = {
  try {
    transfer(from, to, amount)
  } catch {
    case ex => println("transfer failed " + ex)
  }
  println("Balance of from account is " + from.getBalance())
  println("Balance of to account is " + to.getBalance())
}

val account1 = new Account(2000)
val account2 = new Account(100)
transferAndPrint(account1, account2, 500)
transferAndPrint(account1, account2, 5000)

运行上述代码,输出如下:

deposit 500... will it stay
Balance of from account is 1500
Balance of to account is 600
deposit 5000... will it stay
transfer failed java.lang.RuntimeException: Operation invalid
Balance of from account is 1500
Balance of to account is 600

从输出可以看出,第一次转账成功,账户余额相应改变;第二次转账失败,账户余额不受影响。

1.3 使用Akka/Multiverse STM

Akka是一个不错的选择,它用Scala编写,其API对Scala程序员来说很自然。Scala作为混合函数式编程语言,允许创建可变( var )和不可变( val )变量。为了更好地实践,应尽量使用不可变变量,确保状态不可变,只有标识引用(Refs)是可变的。

2. 倾向于隔离可变性

2.1 并发编程的痛点

在并发编程中,共享可变性是导致问题的根源。使用JDK线程API创建线程很容易,但防止线程冲突和混乱却很困难。STM在一定程度上缓解了这个问题,但在Java等语言中,仍需小心避免未管理的可变变量和副作用。当共享可变性消失时,这些问题也会随之消失。

2.2 基于事件的消息传递

一种更好的方法是基于事件的消息传递。将任务视为应用程序或JVM内部的轻量级进程,不直接让它们访问数据,而是向它们传递不可变消息。任务完成后,将不可变结果传递给其他协调任务。通过设计协调的Actor来异步交换不可变消息,可以避免同步和共享可变性带来的问题。

2.3 Actor模型的优势

Actor模型在Erlang中非常成功和流行,2003年Scala引入时将其带到了JVM领域。在Java中,有多种提供基于Actor并发的库可供选择,如ActorFoundary、Actorom、Actors Guild、Akka等。本文主要使用Akka来介绍基于Actor的并发编程。

2.4 隔离可变性

Java将面向对象编程变成了基于可变性的开发,而函数式编程强调不可变性,这两种极端都存在问题。在实际应用中,不能让所有内容都不可变,但必须避免共享可变性。共享可变性是并发问题的根源,而隔离可变性是一种很好的折衷方案,它让只有一个线程(或Actor)可以访问可变变量。

在面向对象编程中,通过封装让实例方法操作对象状态,但不同线程调用这些方法会导致并发问题。在基于Actor的编程模型中,只允许一个Actor操作对象状态,应用程序虽然是多线程的,但每个Actor是单线程的,因此不存在可见性和竞争条件问题。

以下是Actor编程的设计流程:

graph LR
    A[将问题分解为异步计算任务] --> B[将任务分配给不同Actor]
    B --> C[每个Actor专注执行指定任务]
    C --> D[将可变状态限制在最多一个Actor内]
    D --> E[确保Actor间传递的消息不可变]
    E --> F[Actor接收不可变数据执行任务]
    F --> G[完成任务后将不可变结果传递给其他Actor]

2.5 Actor的特性

  • 消息队列 :每个Actor都有内置的消息队列,类似于手机的消息队列。多个Actor可以同时发送消息,发送者默认是非阻塞的,发送消息后继续处理自己的业务。Actor库会让指定的Actor顺序处理消息。
  • 生命周期 :Actor创建后可以启动或停止。启动后准备接收消息,处于活动状态时,要么正在处理消息,要么等待新消息。停止后不再接收消息。
  • 线程解耦 :Actor库通常将Actor与线程解耦,线程对于Actor就像食堂座位对于办公室员工。Actor有消息要处理时会被分配一个可用线程,不处理任务时会释放线程,这样可以让更多的Actor处于不同状态并高效利用有限的线程资源。

2.6 创建Actor

2.6.1 在Java中创建Actor

在Java中使用Akka创建Actor,需要继承 akka.actor.UntypedActor 类并实现 onReceive() 方法。以下是一个示例:

import akka.actor.ActorRef;
import akka.actor.Actors;
import akka.actor.UntypedActor;

public class HollywoodActor extends UntypedActor {
  public void onReceive(final Object role) {
    System.out.println("Playing " + role +
      " from Thread " + Thread.currentThread().getName());
  }
}

public class UseHollywoodActor {
  public static void main(final String[] args) throws InterruptedException {
    final ActorRef johnnyDepp = Actors.actorOf(HollywoodActor.class).start();
    johnnyDepp.sendOneWay("Jack Sparrow");
    Thread.sleep(100);
    johnnyDepp.sendOneWay("Edward Scissorhands");
    Thread.sleep(100);
    johnnyDepp.sendOneWay("Willy Wonka");
    Actors.registry().shutdownAll();
  }
}

运行上述代码,需要使用 javac 编译,并指定Akka库文件的类路径:

javac -d . -classpath $AKKA_JARS HollywoodActor.java UseHollywoodActor.java
java -classpath $AKKA_JARS com.agiledeveloper.pcj.UseHollywoodActor

其中, AKKA_JARS 的定义如下:

export AKKA_JARS="$AKKA_HOME/lib/scala-library.jar:\
$AKKA_HOME/lib/akka/akka-stm-1.1.3.jar:\
$AKKA_HOME/lib/akka/akka-actor-1.1.3.jar:\
$AKKA_HOME/lib/akka/multiverse-alpha-0.6.2.jar:\
$AKKA_HOME/lib/akka/akka-typed-actor-1.1.3.jar:\
$AKKA_HOME/lib/akka/aspectwerkz-2.2.3.jar:\
$AKKA_HOME/config:\
."

运行结果如下:

Playing Jack Sparrow from Thread akka:event-driven:dispatcher:global-1
Playing Edward Scissorhands from Thread akka:event-driven:dispatcher:global-2
Playing Willy Wonka from Thread akka:event-driven:dispatcher:global-3

如果需要在创建Actor时传递参数,可以通过实现 UntypedActorFactory 接口来实现:

import akka.actor.ActorRef;
import akka.actor.Actors;
import akka.actor.UntypedActor;
import akka.actor.UntypedActorFactory;

public class HollywoodActor extends UntypedActor {
  private final String name;
  public HollywoodActor(final String theName) { name = theName; }
  public void onReceive(final Object role) {
    if(role instanceof String)
      System.out.println(String.format("%s playing %s", name, role));
    else
      System.out.println(name + " plays no " + role);
  }
}

public class UseHollywoodActor {
  public static void main(final String[] args) throws InterruptedException {
    final ActorRef tomHanks = Actors.actorOf(new UntypedActorFactory() {
      public UntypedActor create() { return new HollywoodActor("Hanks"); }
    }).start();
    tomHanks.sendOneWay("James Lovell");
    tomHanks.sendOneWay(new StringBuilder("Politics"));
    tomHanks.sendOneWay("Forrest Gump");
    Thread.sleep(1000);
    tomHanks.stop();
  }
}

运行结果如下:

Hanks playing James Lovell
Hanks plays no Politics
Hanks playing Forrest Gump
2.6.2 在Scala中创建Actor

在Scala中创建Akka Actor,需要继承 Actor 特质并实现 receive() 方法。以下是一个示例:

import akka.actor.Actor
import akka.actor.Actor.actorOf
import akka.actor.Actors

class HollywoodActor extends Actor {
  def receive = {
    case role =>
      println("Playing " + role +
        " from Thread " + Thread.currentThread().getName())
  }
}

object UseHollywoodActor {
  def main(args : Array[String]) :Unit = {
    val johnnyDepp = actorOf[HollywoodActor].start()
    johnnyDepp ! "Jack Sparrow"
    Thread.sleep(100)
    johnnyDepp ! "Edward Scissorhands"
    Thread.sleep(100)
    johnnyDepp ! "Willy Wonka"
    Actors.registry.shutdownAll
  }
}

编译和运行代码时,需要使用 scalac 编译器并指定Akka库文件的类路径:

scalac -classpath $AKKA_JARS HollywoodActor.scala UseHollywoodActor.scala
java -classpath $AKKA_JARS com.agiledeveloper.pcj.UseHollywoodActor

运行结果与Java版本类似:

Playing Jack Sparrow from Thread akka:event-driven:dispatcher:global-1
Playing Edward Scissorhands from Thread akka:event-driven:dispatcher:global-2
Playing Willy Wonka from Thread akka:event-driven:dispatcher:global-3

如果需要传递参数,在Scala中实现起来更简单:

import akka.actor.Actor
import akka.actor.Actor.actorOf
import akka.actor.Actors

class HollywoodActor(val name : String) extends Actor {
  def receive = {
    case role : String => println(String.format("%s playing %s", name, role))
    case msg => println(name + " plays no " + msg)
  }
}

object UseHollywoodActor {
  def main(args : Array[String]) :Unit = {
    val tomHanks = actorOf(new HollywoodActor("Hanks")).start()
    tomHanks ! "James Lovell"
    tomHanks ! "Politics"
    tomHanks ! "Forrest Gump"
    Actors.registry.shutdownAll
  }
}

综上所述,Scala提供了多种并发编程的方式,STM和基于Actor的并发编程都有各自的优势。在实际应用中,可以根据具体需求选择合适的方式。对于频繁读取和合理写入冲突的应用,可以使用STM;当写入冲突较大时,基于Actor的模型可能更合适。

3. STM与Actor模型的对比与总结

3.1 STM与Actor模型的对比

特性 STM Actor模型
适用场景 适用于频繁读取和合理写入冲突的应用,能自动处理并发冲突,保证事务的原子性和一致性。 适用于写入冲突较大的场景,通过隔离可变性和消息传递避免共享可变带来的并发问题。
编程复杂度 编程相对简单,事务由系统自动管理,但需要确保值的不可变和事务的幂等性。 编程模型需要重新理解和设计,要将问题分解为异步任务并分配给不同Actor,确保消息的不可变性。
并发控制 基于事务机制,通过系统自动检测和解决冲突。 基于单线程的Actor,每个Actor内部顺序处理消息,避免了内部的竞争条件。
可扩展性 在处理大量并发写入时可能性能下降,因为冲突处理会带来额外开销。 可以通过增加Actor数量来扩展系统,Actor与线程解耦,能高效利用系统资源。

3.2 总结与选择建议

STM和Actor模型都是解决并发编程问题的有效手段,但它们的适用场景和编程方式有所不同。在选择使用哪种模型时,需要考虑应用的具体需求和特点。

如果应用中存在大量的并发读取操作,并且写入冲突相对较少,STM是一个不错的选择。它可以让开发者专注于业务逻辑,而不必过多担心并发冲突的处理。例如,在一个数据库查询系统中,多个用户可能同时查询数据,偶尔有少量的写入操作,使用STM可以很好地保证数据的一致性。

如果应用中写入冲突较为频繁,或者需要处理大量的异步任务,Actor模型可能更合适。通过将任务分配给不同的Actor,每个Actor独立处理消息,可以避免共享可变带来的并发问题,提高系统的可扩展性和稳定性。例如,在一个实时消息处理系统中,需要处理大量的用户消息,使用Actor模型可以让每个消息处理任务独立运行,提高系统的吞吐量。

以下是选择并发模型的决策流程:

graph LR
    A[开始] --> B{写入冲突是否频繁?}
    B -- 是 --> C[选择Actor模型]
    B -- 否 --> D{读取操作是否频繁?}
    D -- 是 --> E[选择STM]
    D -- 否 --> F[根据其他需求选择合适模型]
    C --> G[设计异步任务和Actor]
    E --> H[确保值的不可变和事务幂等性]
    F --> I[综合考虑性能、复杂度等因素]
    G --> J[实现Actor逻辑和消息传递]
    H --> K[编写事务代码]
    I --> L[确定最终方案]
    J --> M[测试和优化系统]
    K --> M
    L --> M
    M --> N[结束]

3.3 最佳实践建议

无论是使用STM还是Actor模型,都有一些最佳实践可以遵循,以提高代码的质量和系统的性能。

3.3.1 STM最佳实践
  • 确保值的不可变 :在事务中使用不可变对象,避免因对象状态的改变导致并发问题。
  • 保证事务的幂等性 :事务应该可以多次执行而不产生额外的影响,这样可以提高事务的可靠性。
  • 减少事务的粒度 :尽量将事务的操作范围缩小,减少冲突的可能性。
3.3.2 Actor模型最佳实践
  • 设计合理的Actor结构 :将问题分解为合适的Actor,每个Actor专注于一个特定的任务,提高系统的可维护性。
  • 确保消息的不可变 :Actor之间传递的消息应该是不可变的,避免因消息状态的改变导致并发问题。
  • 避免Actor之间的过度依赖 :Actor应该尽量独立,减少相互之间的依赖,提高系统的可扩展性。

通过遵循这些最佳实践,可以让并发编程更加高效和可靠,避免常见的并发问题。在实际开发中,还需要不断地测试和优化系统,根据实际情况调整模型和代码,以达到最佳的性能和稳定性。

总之,Scala提供的STM和Actor模型为并发编程提供了强大的工具,开发者可以根据具体需求灵活选择和使用,以应对不同的并发场景。通过合理的设计和实践,可以开发出高效、稳定的并发应用程序。

内容概要:该文档详细介绍了由JEDEC固态技术协会发布的Universal Flash Storage(UFS)4.1标准(编号JESD220G),旨在消除制造商采购商之间的误解,促进产品的互换性和改进,同时协助用户选择和获取适当的产品。主要内容包括了UFS应用层的SCSI命令、命令描述符块(CDB)、读写错误恢复模式页面及其默认值设置。此外,还涵盖了一系列SCSI命令(如INQUIRY、MODE SELECT、READ/WRITE以及FORMAT UNIT等)。文档进一步探讨了UFS安全机制,尤其是设备数据保护机制及基于硬件的安全防护措施,如RPMB操作流程和安全协议的输入/输出指令。还提到了启动配置初始化流程以及相关的查询函数传输协议服务。最后涉及一些关键性的更新变化如新增的队列深度定义、动态容量资源管理策略、逻辑单元的可选功能变为必选项等特性。 适合人群:具备一定电子工程或计算机硬件背景的专业人士及研发工程师,特别是从事嵌入式系统开发或者存储设备研究方向的技术人员。 使用场景及目标:本标准适用于希望深入了解高速闪存设备的工作机制和技术细节的研发团队;可用于指导设计兼容性强且性能优越的新一代移动终端内部存储解决方案;帮助硬件开发商确保所生产产品符合国际通行的标准规范从而更容易进入全球市场。 其他说明:此文件由JEDEC标准化组织官方公布并经过董事会审议批准。虽然可以在网站免费下载但请注意引用时保持尊重版权。如果您有任何疑问或意见可以发送至JEDEC官方网站查询更多联系信息。该文档不仅仅是规定了一套新的行业准则它更代表着整个行业内达成共识的技术发展方向并且可能会对未来的技术发展产生深远的影响。文中提供的信息不仅限于当前版本也可能成为未来美国国家标准的一部分。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值