2

以下示例显示可以使用可变变量作为发送给Actor的消息。Scala Actor 处理可变消息和不可变消息的方式不同吗?如果不是,为什么人们说不变性是 Scala 擅长并发的原因?

import scala.io._
import scala.actors._
import Actor._

def getPageSize(url : String) = Source.fromURL(url).mkString.length

var abc = "http://www.verycd.com"

def getPageSizeConcurrently() = {
  abc = "http://www.twitter.com"  // changing value

  val caller = self
  actor { caller ! (abc, getPageSize(abc)) }
  receive {
    case (url, size) =>
      println("Size for " + url + ": " + size)            
  }
}

getPageSizeConcurrently

更新:我在这里得到了很好的答复。

4

3 回答 3

3

正如其他人指出的那样,您提出问题的方式使回答有点困难。可能是因为你关注的是Actor,你做的例子有点奇怪。

底线是 Scala 连同它的 Actor 实现,以及 Actor 的 Akka 实现,将很高兴地让你用可变性打自己的脑袋。这似乎是您要问的核心问题,所以如果这就是您想要的,您现在可以停止阅读:)

然而,不变性确实以多种方式提高了并发程序的质量,Scala 的可变性与不变性与 OO 和函数式编程的结合为您提供了很多实现事物的灵活性。

人们似乎没有说的是,一成不变的编程概念需要一种不同于大多数命令式程序员所习惯的编程风格(当我开始学习它时,我当然不习惯它)。许多人似乎认为他们可以像往常一样编写代码,用不可变对象替换可变对象,然后从中获利。不是这样。

如果我们List举个例子(实际上是看似无处不在的例子),我们可以看到如何编写一些代码来使用它。

我将使用 Akka 的 Future 实现,并用ListInts 做一些非常愚蠢的事情。算法并不是那么重要。重要的是要认识到存在的东西——并发保护。我什至不需要考虑并发访问的问题,因为它是完全不可变的。我在下面添加了类型注释,以便清楚地知道发生了什么——Scala 实际上会推断出这一切。

import akka.dispatch.{ExecutionContext, Future, Await}
import akka.util.duration._
import java.util.concurrent.Executors

object Main {
  val execService = Executors.newCachedThreadPool()
  implicit val execContext = ExecutionContext.fromExecutorService(execService)

  def main(args: Array[String]) {
    // A Future list of 1's
    val flist: Future[List[Int]] = Future { (1 to 5) map { _: Int => 1 } toList }

    // The goal is to create a new list of Ints in 25 "iterations" across
    // multiple threads
    val result: Future[List[Int]] = (1 to 5).foldLeft(flist) { (acc, _) =>

      // "Loop" 5 times, creating 5 new lists of Ints, but these 'new' lists
      // share most of their content with the previous list
      val fs: IndexedSeq[Future[List[Int]]] = (1 to 5) map { i =>
        acc map { numlist =>
          (i * numlist.sum) :: numlist
        }
      }

      // Reduce the 5 lists we just created back down to one list again, by 
      // performing a pairwise sum across them all... in the Future
      Future.reduce(fs) { (a, l) =>
        (a zip l) map { case (i, j) => i + j }
      }
    }

    // Wait for the concurrency to complete and print out the final list
    println(Await.result(result, 1 second))

    // Shut down the execution system that was running our stuff concurrently
    execContext.shutdown()
  }
}
// Prints: List(12000000, 3000000, 750000, 187500, 46875, 3125, 3125, 3125, 3125, 3125)

(i * numlist.sum) :: numlist使用来自 的所有使用位创建的中间列表numlist。从该循环中出来的 5 个新列表实际上是作为 5 个新值创建的,它们具有指向 的指针numlist,而不是 5 个全新的列表。即list1 = "newvalue" + oldlistlist2 = "another new value" + oldlist等...

编程一成不变地意味着转换模型。获取一个数据结构,将其提供给某物,然后对其进行转换并将其传递给其他人,依此类推。可变编程可以更快、更可靠,也可以更慢,这取决于你在做什么,但最重要的是,正如@mhs 所说,在安全性方面更容易推理。

这如何映射到您通常的命令式编程模式并不明显,因为通常情况下它不会。这也是为什么它对您没有直接意义的原因。就像编码中的其他任何事情一样 - 需要时间才能真正理解新范式。

于 2012-07-14T16:17:49.230 回答
2

我认为您的问题有点奇怪,也许您想知道为什么 Scala 仍然具有可变变量/数据结构,如果不可变的结构更适合并发性?

是的,不变性有利于并发性,并且就像其他人回答的那样更容易推理您的代码。

但这并不意味着 mutable 是无用的或本质上是坏的,有时使用 mutablevar或数据结构的实现更容易,或者在某些特定情况下运行得更快。

Scala不会像 Haskell 那样强迫您做所有不可变的事情,而是提供并鼓励您使用不可变val和数据结构。

但它也为你提供了可变的,如果你真的需要它,请确保它在那里。

所以,是的。Scala/Akka actor 将像处理不可变消息一样处理可变消息,但如果没有充分的理由,您不应该这样做。

如果你使用var,你也应该有充分的理由,并且应该限制他们的范围。

于 2012-07-15T00:16:53.567 回答
1

不变性有助于推理并发程序的正确性。假设您在参与者之间共享的所有对象都是不可变的,您可以安全地保留对它们的假设,而无需了解其他参与者(可能由第三方实施)实际上做了什么。例如,如果您知道您已经实例化了一个包含十个f字段都包含正值的对象的不可变列表,那么即使您与其他人共享该列表,您也可以安全地继续使用此假设,并且您不会遇到您的代码依赖于假设列表中仍有十个带有正f字段的元素的情况,但实际上是另一个参与者修改了列表,使其仅包含一个带有负f字段的元素。

一般来说,不变性使得对程序的推理更容易,也就是说,在顺序情况下也是如此。这就是为什么证明 Haskell 程序的正确性比证明 Java 或 C# 的正确性容易得多的原因之一。对于后者,需要专门的逻辑,例如分离逻辑隐式动态框架,它们使您能够推理共享可变状态。如果您有兴趣,请查看VeriFast(用于 SL)和 Chalice(项目页面Web 界面,对于 IDF),它们都是涉及可变数据结构和别名的程序的实验验证器。但请注意,这是一个正在进行的研究领域,这些验证器通常无法处理现实世界的程序。

底线是:如果您知道您的数据是不可变的,那么您将其提供给谁都没关系,他们将永远无法修改它并打破您的工作假设。

于 2012-07-14T09:31:58.300 回答