解决GPar的常见并发问题

在并发时代,具有4、8和16个处理器内核的芯片变得越来越普遍,并且在不久的将来,我们将看到具有数百甚至数千个内核的芯片。 这种处理能力具有巨大的可能性,但是对于软件开发人员而言,这也带来了挑战。 最大限度地利用这些闪亮的新内核的需求激起了人们对并发,状态管理和为这两种结构构造的编程语言的兴趣。

诸如Groovy,Scala和Clojure之类的JVM语言可以满足这些需求。 这三种语言都是较新的语言,它们可以从在高度优化的JVM上运行而受益,同时还可以访问Java 1.5中添加的强大的Java并发库。 这些语言主动地启用并发编程,尽管每种语言都基于其原理采用不同的方法。

在本文中,我们将使用基于Groovy的并发库GPars来检查用于解决并发问题的模型,例如后台处理,并行处理,状态管理和线程协调。

为什么选择Groovy? 为什么选择GPar?

Groovy是一种在JVM上运行的动态类型的语言。 基于Java语言,Groovy删除了Java代码中的许多仪式性语法,并从其他编程语言中添加了有用的功能。 Groovy最强大的功能之一是它使程序员可以轻松创建基于Groovy的DSL。 (A DSL或特定领域的语言,是一种脚本语言,旨在解决特定编程问题。请参阅相关主题 ,以了解更多的DSL。)

GPars或Groovy并行系统是一个Groovy并发库,可将并发和协调模型捕获为DSL。 GPars从其他语言的一些最流行的并发和协调模型中汲取了想法,包括:

  • Java语言的执行器和fork / join
  • Erlang和Scala的演员
  • 来自Clojure的代理商
  • Oz的数据流变量

Groovy和GPar是展示各种并发方法的理想选择。 即使对于不熟悉Groovy的Java开发人员,讨论也应该很容易进行,因为Groovy的语法是基于Java语言的。 本文中的示例基于Groovy 1.7和GPars 0.10。

后台和并行处理

一个常见的性能挑战是需要等待I / O。 I / O可能涉及从磁盘,数据库,Web服务甚至用户读取数据。 当阻塞一个线程等待I / O时,将等待的线程与原始执行线程分开是很有用的,从而使其能够继续工作。 由于等待发生在后台,因此我们将此技术称为后台处理 。

例如,假设有一个程序调用Twitter API来找到几种JVM语言的最新推文,然后将它们打印出来。 Groovy使使用Java库twitter4j编写这样的程序变得容易,如清单1所示:

清单1.连续读取tweets(langTweets.groovy)
import twitter4j.Twitter
import twitter4j.Query

@Grab(group='net.homeip.yusuke', module='twitter4j', version='2.0.10')
def recentTweets(api, queryStr) {
  query = new Query(queryStr)
  query.rpp = 5		// tweets to return
  query.lang = "en"		// language
  tweets = api.search(query).tweets
  threadName = Thread.currentThread().name
  tweets.collect {
    "[${threadName}-${queryStr}] @${it.fromUser}: ${it.text}"
  }
}

def api = new Twitter()
['#erlang','#scala','#clojure'].each {
  tweets = recentTweets(api, it)
  tweets.each {
    println "${it}"
  }
}

清单1中,我第一次使用Groovy的葡萄(参见相关主题 )抢twitter4j库的依赖。 然后,我定义了一个recentTweets方法来获取查询字符串并执行该查询,并返回格式为字符串的tweets列表。 最后,我浏览了标签列表中的每个标签,获取了推文,然后将它们打印了出来。 因为没有使用线程,所以此代码按顺序执行每个搜索,如图1所示:

图1.连续阅读tweets
串行读取推文的程序模型。

并行处理

如果清单1中的程序要等待,那么它可能就在等待不止一件事。 如果将每个远程请求放入后台线程,则程序可以并行等待每个响应查询,如图2所示:

图2.并行阅读推文
一种改进的程序模型,用于并行读取推文。

GPars Executors DSL使清单1中的程序从串行处理转换为并行处理变得很容易,如清单2所示:

清单2.并行读取tweets(langTweetsParallel.groovy)
import twitter4j.Twitter
import twitter4j.Query
import groovyx.gpars.GParsExecutorsPool

@Grab(group='net.homeip.yusuke', module='twitter4j', version='2.0.10')
@Grab(group='org.codehaus.gpars', module='gpars', version='0.10')
def recentTweets(api, queryStr) {
  query = new Query(queryStr)
  query.rpp = 5		// tweets to return
  query.lang = "en"		// language
  tweets = api.search(query).tweets
  threadName = Thread.currentThread().name
  tweets.collect {
    "[${threadName}-${queryStr}] @${it.fromUser}: ${it.text}"
  }
}

def api = new Twitter()
GParsExecutorsPool.withPool {
  def retrieveTweets = { query ->
    tweets = recentTweets(api, query)
    tweets.each {
      println "${it}"
    }
  }

  ['#erlang','#scala','#clojure'].each {
    retrieveTweets.callAsync(it)
  }
}

通过使用Executors DSL,我为GParsExecutorsPool添加了import语句,并使用Groovy Grape依赖系统获取了Grab语句以获取GPars库。 然后,我引入了GParsExecutorsPool.withPool块,该块增强了该块中的代码以添加其他功能。 在Groovy中,可以使用call方法来调用闭包。 GParsExecutorsPool将执行使用call方法调用的闭包作为基础池中的任务。 此外, GParsExecutorsPool通过调用callAsync方法增强了关闭callAsync ,并立即返回而没有阻塞。 在示例中,我将推文搜索和打印操作包装到一个闭包中,然后针对每个查询异步调用它。

由于GPar将这些任务分配给工作人员池,因此我现在可以并行执行所有搜索(只要该池足够大)。 我还可以并行处理每个查询的结果,并在到达时将其打印到屏幕上。

此示例说明了后台处理可以改善性能和响应性的两种方式:I / O等待并行进行,依赖于该I / O的处理也可以并行进行。

执行者

您可能想知道什么是执行程序以及GParsExecutorsPool的后台处理如何工作。 Executor实际上是Java 5 java.util.concurrent引入的java.util.concurrent库的一部分(请参阅参考资料 )。 java.util.concurrent.Executor接口只有一个方法: execute(Runnable) 。 它可以使任务提交与执行Executor实现中任务的实际执行方式脱钩。

java.util.concurrent.Executors类提供了许多有用的方法来创建由各种不同线程池类型支持的Executor实例。 GParsExecutorsPool默认使用线程池,该线程池具有守护程序线程和固定数量的线程( Runtime.getRuntime().availableProcessors() + 1 )。 在不适合使用默认值的情况下,可以很容易地更改线程池大小,使用自定义ThreadFactory或指定整个现有的Executor池( withExistingPool )。

要更改线程数,我可以将GParsExecutorsPool线程计数传递给withPool方法,如清单3所示:

清单3.使用线程数启动执行器池
GParsExecutorsPool.withPool(8) {
  // ...
}

或者,如果我想传递一个具有特殊命名线程的定制线程工厂,则可以如清单4所示。

清单4.使用自定义线程工厂启动Executor池
def threadCounter = new AtomicLong(0)
def threadFactory = {Runnable runnable ->
  Thread thread = new Thread(runnable)
  id = threadCounter.getAndIncrement()
  thread.setName("thread ${id}")
  return thread
} as ThreadFactory

def api = new Twitter()
GParsExecutorsPool.withPool(2, threadFactory) {
  // ...
}

asynccallAsync方法不会阻塞并立即返回Future对象,该对象表示异步计算的将来结果。 接收者可以要求Future阻塞直到结果准备就绪,然后轮询以查看结果是否完成,取消计算或检查另一个线程是否已取消它。 像Executor接口一样, Future类也是基础java.util.concurrent包的一部分。

CPU的并行性

在后台处理示例中,您看到了让程序并行等待多个I / O绑定任务而不是顺序处理它们的好处。 利用工作人员池并行执行多个任务对于CPU绑定任务也很有帮助。

应用程序的两个重要方面会影响它可以接受的并行度,并因此影响您拥有的编程选项范围:

  • 任务粒度 ,表示任务在时间或数据方面的范围
  • 任务依赖性 ,意味着任务之间通常存在的依赖性数量

这两个方面都存在于一个连续体上,在设计解决方案之前考虑问题在该连续体上的位置非常有用。 例如,程序的任务粒度可以由大型事务大小的工作来定义,或者可以由较大数据集的一小部分(整个图像的几个像素)上的许多简短计算组成。 由于在上下文中切换到线程或进程以进行任何工作量会产生开销,因此,具有小范围粒度的系统通常效率低下,并且性能低下。 基于任务粒度的批处理是一种为系统找到最佳效率点的方法。

几乎没有依赖性的任务通常被描​​述为“令人尴尬的并行”,这意味着将它们拆分成多个并行任务几乎太容易了。 经典示例包括图形处理,蛮力搜索,分形和粒子模拟。 任何处理或转换大量订单或文件的业务处理程序也可能属于此类别。

执行器队列争用

我们已经探讨了如何使用GPar将任务推入Executor池的机制。 请记住,这很好,但是Executor在2005年左右的某个时候被添加到Java 2 Platform Standard Edition(J2SE)中。因此,它们针对较少的核心(两个到八个)进行了调整。细粒度的,可能阻塞的,几乎没有任务间依赖性的事务性任务。 Executor由所有工作线程共享的单个传入工作队列实现。

该模型的关键问题在于,增加工作线程的数量会增加工作队列上的争用(如图3所示)。 随着线程和核心数量的增加,这种竞争最终成为可伸缩性的瓶颈。

图3.执行程序队列争用
该图显示了执行程序队列争用导致的可伸缩性瓶颈。

以另一种Executor队列是fork / join框架,目前下JSR 166y维护更新生活(见相关信息 ),将正式引入到JDK 7叉Java平台/加入被调整为大量并发运行细粒度计算任务的线程。

叉/加入GPar

Fork / join支持定义任务之间的依赖关系并生成新任务。 这些特性使它成为分治式算法的理想选择,在分叉式算法中,任务会分叉到子任务中,然后将子计算重新组合在一起(请参阅参考资料 )。 Fork / join通过每个线程有一个工作队列来解决队列争用的问题。 在每种情况下使用的队列实际上是双端队列 (双端队列,发音为“ deck”),它允许线程从另一个队列的后端窃取工作,从而平衡进入池的工作。

考虑在列表中查找最大值的任务。 最明显的策略是简单地浏览所有数字,并在浏览时始终查看最高价值。 但是,这是一种固有的串行策略,没有利用所有那些昂贵的内核。

相反,请考虑如果将最大值函数实现为并行的分治算法会发生什么。 分而治之是一种递归算法; 每个步骤都具有清单5所示的结构:

清单5.分而治之算法
IF problem is small enough to solve directly
THEN solve it directly
ELSE {
  Divide the problem in two or more sub-problems 
  Solve each sub-problem
  Combine the results
}

IF条件使我可以更改每个任务的粒度。 这种算法样式将生成一棵树,其叶子由采用THEN分支的任务定义。 树中的内部节点是执行ELSE分支的任务。 每个内部节点必须等待(取决于)其两个(或多个)子任务。 fork / join模型正是针对这些算法而设计的,在这些算法中,依赖树中有大量等待任务。 fork / join中的等待任务实际上不会阻塞线程。

GPar允许我们通过运行fork / join任务来创建和执行fork / join算法,如清单6所示:

清单6. max()的并行fork / join实现(computeMax.groovy)
import static groovyx.gpars.GParsPool.runForkJoin
import groovyx.gpars.GParsPool
import groovyx.gpars.AbstractForkJoinWorker

@Grab(group='org.codehaus.gpars', module='gpars', version='0.10')
class Config {
	static DATA_COUNT = 2**14
	static GRANULARITY_THRESHHOLD = 128
	static THREADS = 4
}

items = [] as List<Integer>
items.addAll(1..Config.DATA_COUNT)
Collections.shuffle(items)

GParsPool.withPool(Config.THREADS) {
  computedMax = runForkJoin(1, Config.DATA_COUNT, items.asImmutable()) 
    {begin, end, items ->
      int size = end - begin
      if (size <= Config.GRANULARITY_THRESHHOLD) {
        return items[begin..<end].max()
      } else { // divide and conquer
        leftEnd = begin + ((end + 1 - begin) / 2)
        forkOffChild(begin, leftEnd, items)
        forkOffChild(leftEnd + 1, end, items)
        return childrenResults.max()
      }
    }
		
	println "expectedMax = ${Config.DATA_COUNT}"
	println "computedMax = ${computedMax}"
}

请注意,fork / join在groovyx.gpars.GParsPool类中有其自己的特殊池。 GParsPoolGParsExecutorsPool共享许多常见功能,但具有特定于fork / join的特殊功能。 要直接使用fork / join,必须将runForkJoin()方法与任务闭包一起使用,或者将子类AbstractForkJoinWorker的任务类使用。

平行馆藏

Fork / join提供了一种很好的方式来定义和执行并行任务结构,尤其是分治算法中的任务结构。 但是,您可能已经注意到,前面的示例中涉及大量的仪式。 我必须定义任务关闭,确定适当的任务粒度,拆分子问题,合并结果等等。

理想情况下,我们希望在更高的抽象层次上工作,在此我们定义一个数据结构,然后对其并行执行通用操作,而无需定义在每种情况下都管理细节的低级任务。

为此,JSR 166y维护更新指定了一个高级接口,称为ParallelArrayParallelArray在数组结构上提供常见的函数编程操作,并且使用fork / join池并行执行那些函数。

由于API的功能性质,有必要将函数(方法)传递给许多此类操作,以便可以在ParallelArray每个项目上执行该函数。 JDK 7仍在开发中的一项功能是lambda支持,这将允许开发人员定义代码块并将其传递。 目前, ParallelArray在JDK 7中的包含状态尚在等待lambda项目的结果(请参阅参考资料 )。

GPar中的ParallelArray

Groovy完全支持将代码块定义为闭包并将它们作为一流对象传递,因此使用Groovy和GPar以这种功能风格工作非常自然。 ParallelArray和GPar支持一组核心的功能运算符:

  • map
  • reduce
  • filter
  • size
  • sum
  • min
  • max

此外,GPars在GParsPool块内扩展了集合,从而为我们提供了基于原语的其他并行方法:

  • eachParallel
  • collectParallel
  • findAllParallel
  • everyParallel
  • groupByParallel

可以使并行收集方法透明,以便默认情况下标准收集方法将并行运行。 这使您可以将并行集合传递给现有(非并行)代码库,并有可能按原样使用该代码。 但是,考虑那些并行方法使用的状态仍然很重要,因为外部代码可能无法提供必要的同步保证。

再来看清单6中的示例,请注意max()是并行集合中已经提供的一种方法,因此不需要直接定义和调用fork / join任务,如清单7所示:

清单7.使用GPars ParallelArray函数(computeMaxPA.groovy)
import static groovyx.gpars.GParsPool.runForkJoin
import groovyx.gpars.GParsPool

@Grab(group='org.codehaus.gpars', module='gpars', version='0.10')
class Config {
	static DATA_COUNT = 2**14
	static THREADS = 4
}

items = [] as List<Integer>
items.addAll(1..Config.DATA_COUNT)
Collections.shuffle(items)

GParsPool.withPool(Config.THREADS) {
	computedMax = items.parallel.max()
	println "expectedMax = ${Config.DATA_COUNT}"
	println "computedMax = ${computedMax}"
}

使用ParallelArray进行功能编程

现在,假设我正在为库存系统中的订单编写报告,计算过期的订单数量以及这些订单的平均过期天数。 我可以通过定义以下方法来做到这一点:首先确定订单是否延迟(通过将到期日与今天的日期进行比较),然后计算今天与到期日之间的天数差。

该代码的核心是使用核心ParallelArray方法计算必要数字的部分,如清单8所示:

清单8.充分利用并行函数运算符(orders.groovy)
GParsPool.withPool {
  def data = createOrders().parallel.filter(isLate).map(daysOverdue)
  println("# overdue = " + data.size())
  println("avg overdue by = " + (data.sum() / data.size()))
}

在这里,我获取了一个订单列表,将其转换为ParallelArray ,仅保留isLate返回true的那些订单,并为每个订单计算一个映射函数,以将其转换为过期天数。 然后,我可以在数组上使用内置的聚合函数来获取过期日期的大小和总和,并计算平均值。 该代码与您在函数式编程语言中可能看到的代码非常相似,并具有自动并行执行的额外好处。

管理状态

每当您使用将由多个线程读取或写入的数据时,都必须考虑如何管理该数据并协调更改。 用于管理Java语言和其他语言中的共享状态的统治范式包括由锁或其他关键节标记保护的可变状态。

出于多种原因,可变状态和锁定存在问题。 锁表示代码中的顺序依赖关系,使开发人员可以推断执行路径和预期结果。 但是,由于未强制执行锁定的许多方面,因此通常会看到质量差的代码,其中包含可见性,安全发布,竞争条件,死锁和其他常见的并发错误。

更为严重的问题是,即使您从针对并发性正确编写的两个组件开始,也有可能(甚至有可能)以产生新的令人惊讶的错误的方式来组合它们。 因此,很难编写基于可变状态和锁的并发系统,随着系统的增长,这些锁将继续保持可靠。

在以下各节中,我将演示用于管理和共享系统中线程间状态的三种范例。 从根本上讲,这些范式可以(并且)建立在线程和锁的基础之上,但是它们创建了更高级别的抽象,从而大大降低了这种复杂性。

我将演示的三种方法是参与者,代理和数据流变量,所有这些均由GPar支持。

演员们

演员范例最初在Erlang中流行,由于在Scala中的使用,演员范例最近又臭名昭著(请参阅参考资料,以了解有关这两种语言的更多信息)。 Erlang于1980年代和1990年代在爱立信设计,用于诸如AXD301电信交换机之类的设备。 此类交换机的设计注意事项具有挑战性:极高的可靠性,无停机时间(热代码升级)和大量并发。

Erlang假定有一个“进程”的世界,它们就像轻量级线程(不像操作系统进程)一样,但是不能直接映射到本机线程。 进程执行由基础虚拟机调度。 假定Erlang中的进程内存较小,启动速度快且上下文切换速度快。

Erlang参与者只是在流程上执行的功能。 Erlang没有共享内存,并且所有状态都是不可变的。 不变数据是许多关注并发的语言的关键方面,因为它具有如此出色的属性。 不变的数据无法更改,因此即使读取多个线程,读取不变的数据也不需要锁。 修改不可变数据包括构建数据的新版本并从新版本开始工作。 对于主要具有共享可变状态语言(例如Java语言)背景的开发人员,这种观点上的转变可能需要一些调整。

通过传递不可变的消息,参与者之间可以“共享”状态。 每个角色都有一个邮箱,并且通过在其邮箱中接收消息来重复执行actor功能。 消息发送通常是异步的,尽管也很容易建立同步调用,并且某些actor实现将这些作为功能提供。

GPar中的演员

GPars使用来自Erlang和Scala的许多概念来实现参与者模型。 GPar中的参与者是轻量级进程,它们消耗来自邮箱的消息。 根据消息是由receive()还是react()方法使用,可以放弃或保留线程绑定。

在GPar中,可以从采用闭包的工厂方法或通过将groovyx.gpars.actor.AbstractPooledActor子类groovyx.gpars.actor.AbstractPooledActor来创建groovyx.gpars.actor.AbstractPooledActor 。 在actor中,应该有一个act()方法。 通常, act()方法包含一个循环,该循环将永远重复,然后调用react (对于轻量级actor)或receive (对于仍绑定到其线程的重量级actor)。

清单9.“ Rock,Paper,剪刀”的Actor实现(rps.groovy)
package puredanger.gparsdemo.rps;

import groovyx.gpars.actor.AbstractPooledActor

enum Move { ROCK, PAPER, SCISSORS }

@Grab(group='org.codehaus.gpars', module='gpars', version='0.10')
class Player extends AbstractPooledActor {
  String name
  def random = new Random()
  
  void act() {
    loop {
      react {
        // player replies with a random move 
        reply Move.values()[random.nextInt(Move.values().length)]
      }
    }
  }
}

class Coordinator extends AbstractPooledActor {
  Player player1
  Player player2
  int games
  
  void act() {
    loop {
      react {
	// start the game
        player1.send("play")
        player2.send("play")
        
        // decide the winner
        react {msg1 ->
          react {msg2 ->          
            announce(msg1.sender.name, msg1, msg2.sender.name, msg2) 

            // continue playing
            if(games-- > 0) 
              send("start")
            else 
              stop()
          }
        }
      }
    }
  }
  
  void announce(p1, m1, p2, m2) {
    String winner = "tie"
    if(firstWins(m1, m2) && ! firstWins(m2, m1)) {
      winner = p1
    } else if(firstWins(m2, m1) && ! firstWins(m1, m2)) {
      winner = p2
    } // else tie
    
    if(p1.compareTo(p2) < 0) {
      println toString(p1, m1) + ", " + toString(p2, m2) + ", winner = " + winner      
    } else {
      println toString(p2, m2) + ", " + toString(p1, m1) + ", winner = " + winner 
    }
  }
  
  String toString(player, move) {
    return player + " (" + move + ")"
  }
  
  boolean firstWins(Move m1, Move m2) {
    return (m1 == Move.ROCK && m2 == Move.SCISSORS) || 
        (m1 == Move.PAPER && m2 == Move.ROCK) ||
        (m1 == Move.SCISSORS && m2 == Move.PAPER)
  }
}


final def player1 = new Player(name: "Player 1") 
final def player2 = new Player(name: "Player 2")  
final def coordinator = new Coordinator(player1: player1, player2: player2, games: 10)

[player1,player2,coordinator]*.start()
coordinator << "start"
coordinator.join()
[player1,player2]*.terminate()

清单9包含使用Coordinator actor和两个Player actor通过GPar实现的“ Rock,Paper,剪刀”游戏的完整表示。 Coordinatorplay消息发送给两个Player ,然后等待接收响应。 收到两个响应后, Coordinator打印比赛结果并向自身发送一条消息以开始新的游戏。 Player演员等待被要求移动,然后每个人以任意移动做出响应。

GPars提供了所有关键参与者功能的良好实现以及一些其他功能。 参与者方法可能不是所有并发问题的最佳解决方案,但它确实提供了一种很好的方式来建模涉及消息传递的问题。

代理商

剂在使用的Clojure(参见相关主题 )协调到可识别的片改变状态的多线程访问。 代理将身份(您所指代的名称)与该身份所引用的当前值的不必要耦合分开。 在大多数语言中,这两个方面有着千丝万缕的联系,因此拥有名称意味着您还可以更改值。 这种关系如图4所示:

图4.代理管理状态
该图显示了代理与状态的关系。

代理在变量的持有者和可变状态本身之间提供了一个间接层。 要更改状态,您可以将一个函数传递给代理,然后它评估这些函数的流,用每个函数的输出替换状态。 由于代理会序列化对数据的访问,因此不会出现争用情况或数据损坏的风险。

此外,读取数据包括查看当前快照,由于快照不会更改,因此只需很少的并发开销即可完成。

更改将异步发送到代理。 如有必要,线程可以在代理上阻塞直到应用了其更改,或者您可以指定应用更改后要执行的操作。

GPar中的代理

GPars实现了Clojure中的许多代理功能。 在清单10中,我对僵尸入侵进行了建模,并管理了代理中的世界状态。 有两个线程:一个假设,在每个时间单位中,僵尸都会吞噬人类的大脑,然后将其转化为僵尸。 另一个线索假定剩余的僵尸中有5%被5弹枪的幸存者杀死。 主线程监视世界并报告入侵的进展。

清单10.保护世界免受僵尸启示
(zombieApocalypse.groovy)
import groovyx.gpars.agent.Agent
import groovyx.gpars.GParsExecutorsPool

public class World {
	int alive = 1000
	int undead = 10
	
	public void eatBrains() {
		alive = alive - undead
		undead = undead * 2		
		if(alive <= 0) {
			alive = 0
			println "ZOMBIE APOCALYPSE!"
		}
	}
	
	public void shotgun() {
		undead = undead * 0.95
	}	
	
	public boolean apocalypse() {
		alive <= 0
	}
	
	public void report() {
		if(alive > 0) {
			println "alive=" + alive + " undead=" + undead
		}
	}
}

@Grab(group='org.codehaus.gpars', module='gpars', version='0.10')
def final world = new Agent<World>(new World())

final Thread zombies = Thread.start {
	while(! world.val.apocalypse()) {
		world << { it.eatBrains() }
		sleep 200
	}
}

final Thread survivors = Thread.start {
	while(! world.val.apocalypse()) {
		world << { it.shotgun() } 
		sleep 200
	}
}

while(! world.instantVal.apocalypse()) {
	world.val.report()
	sleep 200
}

代理是Clojure中的重要功能,很高兴看到它们出现在GPar中。 GPars实现缺少一些功能(例如修改操作和观察程序),但这些都是次要的遗漏,将来可能会添加。

数据流变量

数据流变量与Oz编程语言最显着相关(请参阅参考资料 ),但是实现是在Clojure,Scala和GPars中构建的。

数据流变量的最常见类比是它们就像电子表格中的单元格一样-它们专注于指定必须进行的计算以及必须提供值以执行该计算的变量。 然后,底层的调度程序负责执行可以取得进展的线程,因为它们的输入可用。 数据流系统仅专注于数据如何流经系统,然后由线程调度程序决定如何有效利用多个内核。

数据流具有一些不错的属性,因为某些类型的问题是不可能的(竞赛条件),某些类型的问题是可能的,但是确定性的(死锁)。 因此,您可以保证,如果您的代码在测试过程中不会产生死锁,那么它将不会在生产中遇到死锁。

数据流变量只能绑定一次,因此它们的使用受到限制。 数据流流充当值的有界队列,因此可以通过代码定义的结构注入数据,从而保持许多相同的有益属性。 实际上,数据流变量提供了一种将值从一个线程传递到另一个线程的极好方法,并且它们通常用于在多线程单元测试中传递结果。 GPar还定义了逻辑数据流任务,这些任务是在线程池(如参与者)上调度的,并通过数据流变量进行通信。

数据流

返回清单2 ,您看到了每个后台线程都接收并打印检索特定主题的tweet的结果。 清单11是此程序的变体,而是创建一个DataFlowStream 。 后台任务将使用DataFlowStream将结果推文流式传输回主线程,该主线程从流中读取它们。

清单11.通过DataFlowStream流式传输结果
(langTweetsDataflow.groovy)
import twitter4j.Twitter
import twitter4j.Query
import groovyx.gpars.GParsExecutorsPool
import groovyx.gpars.dataflow.DataFlowStream

@Grab(group='net.homeip.yusuke', module='twitter4j', version='2.0.10')
@Grab(group='org.codehaus.gpars', module='gpars', version='0.10')
def recentTweets(api, queryStr, resultStream) {
  query = new Query(queryStr)
  query.rpp = 5		// tweets to return 
  query.lang = "en"		// language
  tweets = api.search(query).tweets
  threadName = Thread.currentThread().name
  tweets.each {
	resultStream << "[${threadName}-${queryStr}] @${it.fromUser}: ${it.text}"
  }
  resultStream << "DONE"
}
 
def api = new Twitter()
def resultStream = new DataFlowStream()
def tags = ['#erlang','#scala','#clojure']
GParsExecutorsPool.withPool {
  tags.each { 
    { query -> recentTweets(api, query, resultStream) }.callAsync(it)
  }
}

int doneMessages = 0
while(doneMessages < tags.size()) {
	msg = resultStream.val
	if(msg == "DONE") {
		doneMessages++
	} else {
		println "${msg}"
	}
}

注意,每个后台线程通过流将“ DONE ”消息发送到结果侦听器。 消息传递文献中有时将这种表示线程已完成发送结果的指示称为“毒药消息”。 它充当生产者和消费者之间的信号。

更多状态管理方法

本文未介绍的两个重要的并发模型是通信顺序过程(CSP)和软件事务存储(STM)。

CSP基于Tony Hoare在规范并发程序行为方面的经典工作,并且基于流程和通道的核心概念。 过程在某些方面类似于已经讨论的参与者模型,并且通道与数据流流具有许多相似之处。 CSP流程与参与者相比,其输入和输出通道集要比单个邮箱丰富得多。 GPar包含用于CSP思想的JCSP实现的API。

在过去的几年中,STM一直是研究的活跃领域,诸如Haskell,Clojure和Fortress之类的语言包括(有些不同)该概念的实现。 STM要求程序员在源代码中划分事务边界,然后系统将每个事务应用于基础状态。 如果检测到冲突,则可以重试该事务,但是会自动进行处理。 GPars尚未包含STM的实现,但将来可能会包含。

结论

当前,并发是一个热门领域,不同语言和库采用的方法之间正在发生大量的混合。 GPars采用了当今一些最流行的并发模型,并使它们可以通过Groovy在Java平台上进行访问。

拥有大量内核的计算机无处不在,实际上,芯片上可用的内核数量将成倍增加。 因此,并发将继续成为探索和创新的领域; 据我们了解,JVM将成为许多最持久的替代品的所在地。


翻译自: https://www.ibm.com/developerworks/java/library/j-gpars/index.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值