actor network theory语法

当前位置: >>
>> 浏览文章
2016同等学力英语语法精讲:九大强调句型分析
  同等学力英语考试中,是考生们通过同等学力申硕考试的拦路虎。许多考生都对同等学力英语语法谈之色变,其实英语法也不是那么的难,只要用心去学,去背一些句子个文章,在去理解其中的语法含义。这样就不会觉同等学力英语语法难了。下面新阳光教育老师来给大家讲下2016同等学力英语语法精讲:九大强调句型。
  It+be&&that(who) &&这一强调结构,这种强调结构用来突出强调句子的某一部分。该结构中的it (the emphatic it),无词汇意义,只用于改变句子的结构,使某一部分得以被强调。要正确使用这一结构应注意以下几方面的问题:
  1.这种强调结构除了不能强调动词外,其他成分如主语、宾语、宾补、状语都可以强调。
  例如: It was my sister who first had the idea to cycle along the Mekong River from where it begins to where it ends。(强调主语)
  It was in Greece that Olympic competitions firstly started。(强调状语)
  2.被强调部分指人时用that,who均可,指事物或情况时用that(被强调的是时间或地点状语时一般不用when,where)。
  例: It was because of bad weather that the football match bad to be put off。
  It was last year at the Olympics that Liu Xiang got the medal for hurdling race。是去年在奥运会上刘翔赢得了跨栏金牌。
  3.如果强调双宾语中的一个,不论是直接宾语还是间接宾语,根据动词的搭配关系,要加上一个介词to或for。
  例:Mr。 Green gave Mary a birthday present。&It was Mary that Mr。 Green gave a birthday present to。
  It was a birthday present that Mr。 Green gave to Mary。
  4.如果原句是一般疑问句,强调结构用&be+it&&that (who)&&?&形式;如果原句是特殊疑问句,则用&特殊疑问词+be+it&&that(who) &&?&形式。
  Is it an the end of 2001 that China joined WTO?
  是2001年底中国加入了世贸组织吗?
  When was it that the tsunami struck the countries around the India Ocean? 是何时海啸袭击了印度洋沿岸各国?
  5.如果原句中含有&not&&until&,在强调时间状语时,将主句中的否定词not连同状语一期提前。
  例: It was not until she took off her dark glasses that I realized she was a famous film star。
  6.强调结构中的谓语动词除用be外,还可以用might be 或must have been。
  例:It might be tomorrow that she will come。
  It must have been an actor that came yesterday。
  7.强调结构不但可独立成句,也可用在从句中。
  例: Further reading made me realize that it was hard work and determination as well as her good nature that had got her into medical school。
  8.被强调部分句子可能较长或有一个以上的强调成分,对此就应注意识别。
  例:It was not the fish but the vegetable, which grows only in Sichuan, that made the hot dish sour and aromatic。
  It was she who was lying in the corridor and the stranger who bent over her。是她躺在过道上,是那个陌生人俯视她。(此句有两个被强调部分:一是she,一是 the stranger)
  It is that intensity, along with the low cost and low risk, that causes the real danger of cyber lover。
  9.强调结构中的时态一般应一致,即主句与从句中的时态应都用现在时,或都用将来时。
  例: It was my two sisters who knew her best。(都用过去时)
  It will not be you who will have to take the blame for this。(都用将来时)对此受谴责的将不是你。
  微信&每日一练&订阅方法:
  1、打开微信,直接&添加朋友&;
  ①搜索xygtdxlks(新阳光教育同等学力微信号)
  ②点击查找微信公众账号:新阳光教育同等学力
  2、点击&扫一扫&,扫描以下二维码:
& & 更多相关阅读:
& & 请点击&
& & 请点击&
& & 请点击&
新阳光免费为考生提供2016年同等学力报名时间提醒服务,最新动态将第一时间告知考生!提交成功即可免费获得新阳光内部编写的学习资料和复习方法各一份,发送至大家邮箱,机不可失哦!
没有科研成果能申请硕士学位吗?...
获得学士学位后,工作不满三年,或获得学士学位后虽工作不满三年,但在获得学士...
考生可否在异地参加同等学力人员申请硕士学位考试?...
Copyright ©
深圳市新阳光教育发展有限公司
All Rights Reserved.
地址:深圳市福田区深南中路2010号东风大厦14楼
电话:5 备案:粤ICP备号让并发和容错更容易:Akka示例教程
发表于 10:48|
来源hongbinzuo.github.io|
作者左洪斌
摘要:Akka用Scala语言写成,为开发高并发、分布式和容错式应用提供了便利,对开发者隐藏了很大程度的复杂性。把Akka用好肯定需要了解比这个教程更多的内容,但是希望这里的介绍和示例能够引起你的注意并继续了解Akka。
写并发程序很难。程序员不得不处理线程、锁和竞态条件等等,这个过程很容易出错,而且会导致程序代码难以阅读、测试和维护。
所以,很多人不倾向于使用多线程编程。取而代之的是,他们使用单线程进程(译者注:只含有一个线程的进程),依赖外部服务(如数据库、队列等)处理所需的并发或异步操作。虽然这种方法在有些情况下是可行的,但还有很多其他情况不能奏效。很多实时系统——例如交易或银行业务应用,或实时游戏——等待一个单线程进程完成就太奢侈了(他们需要立即应答!)。其他的一些对于计算或资源要求非常高的系统,如果在程序中不引入并行机制就会耗时很久(有些情况下可以达到几个小时或数天)。
常用的一种单线程方法(例如,在
里广泛应用)是使用基于事件的、非阻塞模式(Event-Based, NON-Blocking Paradigm,其中Paradigm也有译作成例)。虽然这种方法可以避免上下文切换、锁和阻塞,的确能提高性能,但还是不能解决并发使用多个处理器(需要启动和协调多个独立的处理器)的问题。
那么,这是不是意味着为了构建一个并发程序,除了深入到线程、锁和竞态条件之外没有别的选择呢?
感谢Akka框架,它为我们提供了一种选择。本教程介绍了Akka的示例,并仔细研究它如何帮助并简化分布式并发应用的实现。
Akka框架是什么
这篇文章介绍了Akka并仔细研究它如何帮助并简化分布式并发应用的实现。
是JVM(JAVA虚拟机,下同)平台上构建高并发、分布式和容错应用的工具包和运行时。Akka用
写成,同时提供了Scala和JAVA的开发接口。
Akka处理并发的方法基于
。在基于Actor的系统里,所有的事物都是Actor,就好像在面向对象设计里面所有的事物都是对象一样。但是有一个重要区别——特别是和我们的讨论相关的——那就是Actor模型是作为一个并发模型设计和架构的,而面向对象模式则不是。更具体一点,在Scala的Actor系统里,Actor互相交互并共享信息但并不对交互顺序作出预设。Actor之间共享信息和发起任务的机制是消息传递。
创建和调度线程、接收和分发消息以及处理竞态条件和同步的所有复杂性,都委托给框架,框架的处理对应用来说是透明的。
Akka在多个Actor和下面的系统之间建立了一个层次(Layer),这样一来,Actor只需要处理消息就可以了。创建和调度线程、接收和分发消息以及处理竞态条件和同步的所有复杂性,都委托给框架,框架的处理对应用来说是透明的。
Actor严格遵守
。响应式应用的目标是通过满足以下一个或多个条件来代替传统的多线程应用:
事件驱动。使用Actor,代码可以异步处理请求并用独占的方式执行非阻塞操作。
可伸缩性。在Akka里,不修改代码就增加节点是可能的,感谢消息传递和本地透明性(Location Transparency)。
高弹性。任何应用都会碰到错误并在某个时间点失败。Akka的“监管”(容错)策略为实现自愈系统提供了便利。
响应式。今天的高性能和快速响应应用需要对用户快速反馈,因此对于事件的响应需要非常及时。Akka的非阻塞、基于消息的策略可以帮助达成这个目标。
Akka中的Actor是什么
Actor本质上就是接收消息并采取行动处理消息的对象。它从消息源中解耦出来,只负责正确识别接收到的消息类型,并采取相应的行动。
收到一条消息之后,一个Actor可能会采取以下一个或多个行动:
执行一些本身的操作(例如进行计算、持久化数据、调用外部的Web服务等)
把消息或衍生消息转发给另外一个Actor
实例化一个新的Actor并把消息转发给它
或者,如果这个Actor认为合适的话,可能会完全忽略这条消息(也就是说,它可能选择不响应)。
为了实现一个Actor,需要继承Akka.Actor.Actor这个Trait(一般译为“特征”,译法有一定争议,文中保留原词)并实现Receive方法。当一个消息发送给Actor时,它的Receive方法会被(Akka)调用。典型的实现包括使用模式匹配(Pattern
Matching)来识别消息类型并作出响应,参见下面的Akka示例:import akka.actor.Actor
import akka.actor.Props
import akka.event.Logging
class MyActor extends Actor {
def receive = {
case value: String =& doSomething(value)
case _ =& println("received unknown message")
模式匹配是一种相对优雅的处理消息的技术,相比基于回调的实现,更倾向于产生“更整洁”以及更容易浏览的代码。例如,考虑一个简化版的HTTP请求/响应实现。
首先,我们使用JavaScript中基于回调的方式实现:route(url, function(request){
var query = buildQuery(request);
dbCall(query, function(dbResponse){
var wsRequest = buildWebServiceRequest(dbResponse);
wsCall(wsRequest, function(wsResponse) {
sendReply(wsResponse);
});现在,我们把它和基于模式匹配的实现做个比较:
msg match {
case HttpRequest(request) =& {
val query = buildQuery(request)
dbCall(query)
case DbResponse(dbResponse) =& {
var wsRequest = buildWebServiceRequest(dbResponse);
wsCall(dbResponse)
case WsResponse(wsResponse) =& sendReply(wsResponse)
}虽然基于回调的JavaScript代码更紧凑,但确实更难以阅读和浏览。相比而言,基于模式匹配的代码对于需要考虑哪些情况、每种情况都是怎么处理的写法更加清晰。
把一个复杂的问题不断分解成更小规模的子问题通常是一种可靠的解决问题的技术。这个方法对于计算机科学特别有效(和
一致),因为这样容易产生整洁的、模块化的代码,产生的冗余很少甚至没有,而且维护起来相对容易。
在基于Actor的设计里,使用这种技术有助于把Actor的逻辑组织变成一个层级结构,也就是所谓的
。Actor系统提供了一个基础框架,通过这个系统Actor之间可以进行交互。& & &
在Akka里面,和Actor通信的唯一方式就是通过ActorRef。ActorRef代表Actor的一个引用,可以阻止其他对象直接访问或操作这个Actor的内部信息和状态。消息可以通过一个ActorRef以下面的语法协议中的一种发送到一个Actor:
-!(“告知”) —— 发送消息并立即返回
-?(“请求”) —— 发送消息并返回一个Future对象,代表一个可能的应答
每个Actor都有一个收件箱,用来接收发送过来的消息。收件箱有多种实现方式可以选择,缺省的实现是先进先出(FIFO)队列。
在处理多条消息时,一个Actor包含多个实例变量来保持状态。Akka确保Actor的每个实例都运行在自己的轻量级线程里,并保证每次只处理一条消息。这样一来,开发者不必担心同步或竞态条件,而每个Actor的状态都可以被可靠地保持。
Akka的Actor API中提供了每个Actor执行任务所需要的有用信息:
sender:当前处理消息的发送者的一个ActorRef引用
context:Actor运行上下文相关的信息和方法(例如,包括实例化一个新Actor的方法ActorOf)
supervisionStrategy:定义用来从错误中恢复的策略
self:Actor本身的ActorRef引用
Akka确保Actor的每个实例都运行在自己的轻量级线程里,并保证每次只处理一条消息。这样一来,开发者不必担心同步或竞态条件,而每个Actor的状态都可以被可靠地保持。
为了把这些教程组织起来,让我们来考虑一个简单的例子:统计一个文本文件中单词的数量。
为了达到演示Akka示例的目的,我们把这个问题分解为两个子任务;即(1)统计每行单词数量的“孩子”任务和(2)汇总这些单行单词数量、得到文件里单词总数的“父亲”任务。
父Actor会从文件中装载每一行,然后委托一个子Actor来计算某一行的单词数量。当子Actor完成之后,它会把结果用消息发回给父Actor。父Actor会收到(每一行的)单词数量的消息并维持一个整个文件单词总数的计数器,这个计数器会在完成后返回给调用者。
(注意以下提供的Akka教程的例子只是为了教学目的,所以没有顾及所有的边界条件、性能优化等。同时,完整可编译版本的代码示例可以在这个GIST中找到)
让我们首先看一个子类StringCounterActor的示例实现:
case class ProcessStringMsg(string: String)
case class StringProcessedMsg(words: Integer)
class StringCounterActor extends Actor {
def receive = {
case ProcessStringMsg(string) =& {
val wordsInLine = string.split(" ").length
sender ! StringProcessedMsg(wordsInLine)
case _ =& println("Error: message not recognized")
这个Actor有一个非常简单的任务:接收ProcessStringMsg消息(包含一行文本),计算这行文本中单词的数量,并把结果通过一个StringProcessedMsg消息返回给发送者。请注意我们已经实现了我们的类,使用!(“告知”)方法发出StringProcessedMsg消息(发出消息并立即返回)。
好了,现在我们来关注父WordCounterActor类:
case class StartProcessFileMsg()
class WordCounterActor(filename: String) extends Actor {
private var running = false
private var totalLines = 0
private var linesProcessed = 0
private var result = 0
private var fileSender: Option[ActorRef] = None
def receive = {
case StartProcessFileMsg() =& {
if (running) {
// println just used
// Akka logger should be used instead
println("Warning: duplicate start message received")
running = true
fileSender = Some(sender) // save reference to process invoker
import scala.io.Source._
fromFile(filename).getLines.foreach { line =&
context.actorOf(Props[StringCounterActor]) ! ProcessStringMsg(line)
totalLines += 1
case StringProcessedMsg(words) =& {
result += words
linesProcessed += 1
if (linesProcessed == totalLines) {
fileSender.map(_ ! result)
// provide result to process invoker
case _ =& println("message not recognized!")
这里面有很多细节,我们来逐一考察(注意讨论中所引用的行号基于以上代码示例)。
首先,请注意要处理的文件名被传给了WordCounterActor的构造方法(第3行)。这意味着这个Actor只会用来处理一个单独的文件。这样通过避免重置状态变量(running,totalLines,linesProcessed和result)也简化了开发者的编码工作,因为这个实例只使用一次(也就是说处理一个单独的文件),然后就丢弃了。
接下来,我们看到WordCounterActor处理了两种类型的消息:
StartProcessFileMsg(第12行)
从最初启动WordCounterActor的外部Actor接收到的消息
收到这个消息之后,WordCounterActor首先检查它收到的是不是一个重复的请求
如果这个请求是重复的,那么WordCounterActor生成一个警告,然后就不做别的事了(第16行)
如果这不是一个重复的请求:
WordCounterActor在FileSender实例变量(注意这是一个Option[ActorRef]而不是一个Option[Actor])中保存发送者的一个引用。当处理最终的StringProcessedMsg(从一个StringCounterActor子类中接收,如下文所述)时,为了以后的访问和响应,这个ActorRef是必需的。
然后WordCounterActor读取文件,当文件中每行都装载之后,就会创建一个StringCounterActor,需要处理的包含行文本的消息就会传递给它(第21-24行)。
StringProcessedMsg(第27行)
当处理完成分配给它的行之后,从StringCounterActor处接收到的消息
收到此消息之后,WordCounterActor会把文件的行计数器增加,如果所有的行都处理完毕(也就是说,当totalLines和linesProcessed相等),它会把最终结果发给原来的FileSender(第28-31行)。
再次需要注意的是,在Akka里,Actor之间通信的唯一机制就是消息传递。消息是Actor之间唯一共享的东西,而且因为多个Actor可能会并发访问同样的消息,所以为了避免竞态条件和不可预期的行为,消息的不可变性非常重要。
因为Case Class默认是不可变的并且可以和模式匹配无缝集成,所以用Case Class的形式来传递消息是很常见的。(Scala中的Case
Class就是正常的类,唯一不同的是通过模式匹配提供了可以递归分解的机制)。
让我们通过运行整个应用的示例代码来结束这个例子。object Sample extends App {
import akka.util.Timeout
import scala.concurrent.duration._
import akka.pattern.ask
import akka.dispatch.ExecutionContexts._
implicit val ec = global
override def main(args: Array[String]) {
val system = ActorSystem("System")
val actor = system.actorOf(Props(new WordCounterActor(args(0))))
implicit val timeout = Timeout(25 seconds)
val future = actor ? StartProcessFileMsg()
future.map { result =&
println("Total number of words " + result)
system.shutdown
}请注意这里的?方法是怎样发送一条消息的。用这种方法,调用者可以使用返回的
对象,当完成之后可以打印出最后结果并最终通过停掉Actor系统退出程序。
Akka的容错和监管者策略
在Actor系统里,每个Actor都是其子孙的监管者。如果Actor处理消息时失败,它就会暂停自己及其子孙并发送一个消息给它的监管者,通常是以异常的形式。
在Akka里面,监管者策略是定义你的系统容错行为的主要并且直接的机制。
在Akka里面,一个监管者对于从子孙传递上来的异常的响应和处理方式称作监管者策略。
是定义你的系统容错行为的主要并且直接的机制。
当一条消息指示有一个错误到达了一个监管者,它会采取如下行动之一:
恢复孩子(及其子孙),保持内部状态。&当孩子的状态没有被错误破坏,还可以继续正常工作的时候,可以使用这种策略。
重启孩子(及其子孙),清除内部状态。&这种策略应用的场景和第一种正好相反。如果孩子的状态已经被错误破坏,在它可以被用到Future之前有必须要重置其内部状态。
永久地停掉孩子(及其子孙)。&这种策略可以用在下面的场景中:错误条件不能被修正,但是并不影响后面执行的操作,这些操作可以在失败的孩子不存在的情况下完成。
停掉自己并向上传播错误。&适用场景:当监管者不知道如何处理错误,就把错误传递给自己的监管者。
而且,一个Actor可以决定是否把行动应用在失败的子孙上抑或是应用到它的兄弟上。有两种预定义的策略:
OneForOneStrategy:只把指定行动应用到失败的孩子上
AllForOneStrategy:把指定行动应用到所有子孙上
下面是一个使用OneForOneStrategy的简单例子:import akka.actor.OneForOneStrategy
import akka.actor.SupervisorStrategy._
import scala.concurrent.duration._
override val supervisorStrategy =
OneForOneStrategy() {
case _: ArithmeticException
case _: NullPointerException
=& Restart
case _: IllegalArgumentException =& Stop
case _: Exception
=& Escalate
如果没有指定策略,那么就使用如下默认的策略:
如果在初始化Actor时出错,或者Actor被结束(Killed),那么Actor就会停止(Stopped)
如果有任何类型的异常出现,Actor就会重启
Akka提供的默认策略的实现如下:final val defaultStrategy: SupervisorStrategy = {
def defaultDecider: Decider = {
case _: ActorInitializationException => Stop
case _: ActorKilledException
case _: Exception
=> Restart
OneForOneStrategy()(defaultDecider)
}Akka也考虑到对
的实现,但正如Akka文档也提出了警告,这么做要小心,因为错误的实现会产生诸如Actor系统被阻塞的问题(也就是说,其中的多个Actor被永久挂起了)。
本地透明性
Akka架构支持
,使得Actor完全不知道他们接受的消息是从哪里发出来的。消息的发送者可能驻留在同一个JVM,也有可能是存在于其他的JVM(或者运行在同一个节点,或者运行在不同的节点)。Akka处理这些情况对于Actor(也即对于开发者)来说是完全透明的。唯一需要说明的是跨越节点的消息必须要被序列化。
Akka架构支持本地透明性,使得Actor完全不知道他们接受的消息是从哪里发出来的。
Actor系统设计的初衷,就是不需要任何专门的代码就可以运行在分布式环境中。Akka只需要一个配置文件(Application.Conf),用以说明发送消息到哪些节点。下面是配置文件的一个例子:
provider = "akka.remote.RemoteActorRefProvider"
transport = "akka.remote.netty.NettyRemoteTransport"
hostname = "127.0.0.1"
port = 2552
最后的一些提示
我们已经了解了Akka框架帮助完成并发和高性能的方法。然而,正如这篇教程指出的,为了充分发挥Akka的能力,在设计和实现系统时,有些要点值得考虑:
我们应尽最大可能为每个Actor都分配最小的任务(如上面讨论的,遵守单一职责原则)
Actor应该异步处理事件(也就是处理消息),不应该阻塞,否则就会发生上下文切换,影响性能。具体来说,最好是在一个Future对象里执行阻塞操作(例如IO),这样就不会阻塞Actor,如:
case evt =& blockingCall() // BAD
case evt =& Future {
blockingCall()
要确认你的消息都是不可变的,因为互相传递消息的Actor都在它们自己的线程里并发运行。可变的消息很有可能导致不可预期的行为。
由于在节点之间发送的消息必须是可序列化的,所以必须要记住消息体越大,序列化、发送和反序列化所花费的时间就越多,这也会降低性能。
Akka用Scala语言写成,简化并为开发高并发、分布式和容错式应用提供了便利,对开发者隐藏了很大程度的复杂性。把Akka用好肯定需要了解比这个教程更多的内容,但是希望这里的介绍和示例能够引起你的注意并继续了解Akka。
Amazon、VMWare和CSC只是现在积极使用Akka的一部分领军企业。可以访问
学到更多的知识,并多花点时间研究Akka是否适合你的项目。
原文链接:(责编/刘亚琼)
免费订阅“CSDN云计算(左)和CSDN大数据(右)”微信公众号,实时掌握第一手云中消息,了解最新的大数据进展!
CSDN发布虚拟化、Docker、OpenStack、CloudStack、数据中心等相关云计算资讯, & & 分享Hadoop、Spark、NoSQL/NewSQL、HBase、Impala、内存计算、流计算、机器学习和智能算法等相关大数据观点,提供云计算和大数据技术、平台、实践和产业信息等服务。
推荐阅读相关主题:
CSDN官方微信
扫描二维码,向CSDN吐槽
微信号:CSDNnews
相关热门文章并发之痛 Thread,Goroutine,Actor - 推酷
并发之痛 Thread,Goroutine,Actor
本文基于我在2月27日Gopher北京聚会演讲整理而成,进行了一些补充以及调整。投稿给《高可用架构》公众号首发。
聊这个话题之前,先梳理下两个概念,几乎所有讲并发的文章都要先讲这两个概念:
并发(concurrency) 并发的关注点在于任务切分。举例来说,你是一个创业公司的CEO,开始只有你一个人,你一人分饰多角,一会做产品规划,一会写代码,一会见客户,虽然你不能见客户的同时写代码,但由于你切分了任务,分配了时间片,表现出来好像是多个任务一起在执行。
并行(parallelism) 并行的关注点在于同时执行。还是上面的例子,你发现你自己太忙了,时间分配不过来,于是请了工程师,产品经理,市场总监,各司一职,这时候多个任务可以同时执行了。
所以总结下,并发并不要求必须并行,可以用时间片切分的方式模拟,比如单核cpu上的多任务系统,并发的要求是任务能切分成独立执行的片段。而并行关注的是同时执行,必须是多(核)cpu,要能并行的程序必须是支持并发的。本文大多数情况下不会严格区分这两个概念,默认并发就是指并行机制下的并发。
为什么并发程序这么难?
We believe that writing correct concurrent , fault-tolerant and scalable applications is too hard. Most of the time it’s because we are using the wrong tools and the wrong level of abstraction. —— Akka
Akka官方文档开篇这句话说的好,之所以写正确的并发,容错,可扩展的程序如此之难,是因为我们用了错误的工具和错误的抽象。(当然该文档本来的意思是Akka是正确的工具,但我们可以独立的看待这句话)。
那我们从最开始梳理下程序的抽象。开始我们的程序是面向过程的,数据结构+func。后来有了面向对象,对象组合了数结构和func,我们想用模拟现实世界的方式,抽象出对象,有状态和行为。但无论是面向过程的func还是面向对象的func,本质上都是代码块的组织单元,本身并没有包含代码块的并发策略的定义。于是为了解决并发的需求,引入了Thread(线程)的概念。
线程(Thread)
系统内核态,更轻量的进程
由系统内核进行调度
同一进程的多个线程可共享资源
线程的出现解决了两个问题,一个是GUI出现后急切需要并发机制来保证用户界面的响应。第二是互联网发展后带来的多用户问题。最早的CGI程序很简单,将通过脚本将原来单机版的程序包装在一个进程里,来一个用户就启动一个进程。但明显这样承载不了多少用户,并且如果进程间需要共享资源还得通过进程间的通信机制,线程的出现缓解了这个问题。
线程的使用比较简单,如果你觉得这块代码需要并发,就把它放在单独的线程里执行,由系统负责调度,具体什么时候使用线程,要用多少个线程,由调用方决定。但由此带来便利的同时也带来复杂度:
竞态条件(race conditions) 如果每个任务都是独立的,不需要共享任何资源,那线程也就非常简单。但世界往往是复杂的,总有一些资源需要共享,比如前面的例子,开发人员和市场人员同时需要和CEO商量一个方案,这时候CEO就成了竞态条件。
依赖关系以及执行顺序 如果线程之间的任务有依赖关系,需要等待以及通知机制来进行协调。比如前面的例子,如果产品和CEO讨论的方案依赖于市场和CEO讨论的方案,这时候就需要协调机制保证顺序。
为了解决上述问题,我们引入了许多复杂机制来保证:
Mutex(Lock) (Go里的sync包, Java的concurrent包)通过互斥量来保护数据,但有了锁,明显就降低了并发度。
semaphore 通过信号量来控制并发度或者作为线程间信号(signal)通知。
volatile Java专门引入了volatile关键词来,来降低只读情况下的锁的使用。
compare-and-swap 通过硬件提供的CAS机制保证原子性(atomic),也是降低锁的成本的机制。
如果说上面两个问题只是增加了复杂度,我们通过深入学习,严谨的CodeReview,全面的并发测试(比如Go语言中单元测试的时候加上-race参数),一定程度上能解决(当然这个也是有争议的,有论文认为当前的大多数并发程序没出问题只是并发度不够,如果CPU核数继续增加,程序运行的时间更长,很难保证不出问题)。但最让人头痛的还是下面这个问题:
系统里到底需要多少线程?
这个问题我们先从硬件资源入手,考虑下线程的成本:
内存(线程的栈空间)
每个线程都需要一个栈(Stack)空间来保存挂起(suspending)时的状态。Java的栈空间(64位VM)默认是1024k,不算别的内存,只是栈空间,启动1024个线程就要1G内存。虽然可以用-Xss参数控制,但由于线程是本质上也是进程,系统假定是要长期运行的,栈空间太小会导致稍复杂的递归调用(比如复杂点的正则表达式匹配)导致栈溢出。所以调整参数治标不治本。
调度成本(context-switch)我在个人电脑上做的一个非严格测试,模拟两个线程互相唤醒轮流挂起,线程切换成本大约6000纳秒/次。这个还没考虑栈空间大小的影响。国外一篇论文专门分析线程切换的成本,基本上得出的结论是切换成本和栈空间使用大小直接相关。
我们搞并发/并行最主要的一个目标就是我们有了多核,想提高CPU利用率,最大限度的压榨硬件资源,从这个角度考虑,我们应该用多少线程呢?
这个我们可以通过一个公式计算出来,100/(15+5)*4=20,用20个线程最合适。但一方面网络的时间不是固定的,另外一方面,如果考虑到其他瓶颈资源呢?比如锁,比如数据库连接池,就会更复杂。
作为一个1岁多孩子的父亲,认为这个问题的难度好比你要写个给孩子喂饭的程序,需要考虑『给孩子喂多少饭合适?』,这个问题有以下回答以及策略:
孩子不吃了就好了(但孩子贪玩,不吃了可能是想去玩了)
孩子吃饱了就好了(废话,你怎么知道孩子吃饱了?孩子又不会说话)
逐渐增量,长期观察,然后计算一个平均值(这可能是我们调整线程常用的策略,但增量增加到多少合适呢?)
孩子吃吐了就别喂了(如果通过逐渐增量的模式,通过外部观察,可能会到达这个边界条件。系统性能如果因为线程的增加倒退了,就别增加线程了)
没控制好边界,把孩子给给撑坏了 (这熊爸爸也太恐怖了。但调整线程的时候往往不小心可能就把系统搞挂了)
通过这个例子我们可以看出,从外部系统来观察,或者以经验的方式进行计算,都是非常困难的。于是结论是: & 让孩子会说话,吃饱了自己说,自己学会吃饭,自管理是最佳方案。
然并卵,计算机不会自己说话,如何自管理?
但我们从以上的讨论可以得出一个结论:
线程的成本较高(内存,调度)不可能大规模创建
应该由语言或者框架动态解决这个问题
线程池方案
Java1.5后,Doug Lea的Executor系列被包含在默认的JDK内,是典型的线程池方案。
线程池一定程度上控制了线程的数量,实现了线程复用,降低了线程的使用成本。但还是没有解决数量的问题,线程池初始化的时候还是要设置一个最小和最大线程数,以及任务队列的长度,自管理只是在设定范围内的动态调整。另外不同的任务可能有不同的并发需求,为了避免互相影响可能需要多个线程池,最后导致的结果就是Java的系统里充斥了大量的线程池。
从前面的分析我们可以看出,如果线程是一直处于运行状态,我们只需设置和CPU核数相等的线程数即可,这样就可以最大化的利用CPU,并且降低切换成本以及内存使用。但如何做到这一点呢?
陈力就列,不能者止
这句话是说,能干活的代码片段就放在线程里,如果干不了活(需要等待,被阻塞等),就摘下来。通俗的说就是不要占着茅坑不拉屎,如果拉不出来,需要酝酿下,先把茅坑让出来,因为茅坑是稀缺资源。
要做到这点一般有两种方案:
异步回调方案 典型如NodeJS,遇到阻塞的情况,比如网络调用,则注册一个回调方法(其实还包括了一些上下文数据对象)给IO调度器(linux下是libev,调度器在另外的线程里),当前线程就被释放了,去干别的事情了。等数据准备好,调度器会将结果传递给回调方法然后执行,执行其实不在原来发起请求的线程里了,但对用户来说无感知。但这种方式的问题就是很容易遇到callback hell,因为所有的阻塞操作都必须异步,否则系统就卡死了。还有就是异步的方式有点违反人类思维习惯,人类还是习惯同步的方式。
GreenThread/Coroutine/Fiber方案 这种方案其实和上面的方案本质上区别不大,关键在于回调上下文的保存以及执行机制。为了解决回调方法带来的难题,这种方案的思路是写代码的时候还是按顺序写,但遇到IO等阻塞调用时,将当前的代码片段暂停,保存上下文,让出当前线程。等IO事件回来,然后再找个线程让当前代码片段恢复上下文继续执行,写代码的时候感觉好像是同步的,仿佛在同一个线程完成的,但实际上系统可能切换了线程,但对程序无感。
GreenThread
用户空间 首先是在用户空间,避免内核态和用户态的切换,导致的成本。
由语言或者框架层调度
更小的栈空间允许创建大量实例(百万级别)
Continuation 这个概念不熟悉FP编程的人可能不太熟悉,不过这里可以简单的顾名思义,可以理解为让我们的程序可以暂停,然后下次调用继续(contine)从上次暂停的地方开始的一种机制。相当于程序调用多了一种入口。
Coroutine 是Continuation的一种实现,一般表现为语言层面的组件或者类库。主要提供yield,resume机制。
Fiber 和Coroutine其实是一体两面的,主要是从系统层面描述,可以理解成Coroutine运行之后的东西就是Fiber。
Goroutine其实就是前面GreenThread系列解决方案的一种实现。
首先,它内置了Coroutine机制。因为要用户态的调度,必须可以让代码片段可以挂起/继续的机制。
其次,它内置了一个调度器,实现了Coroutine的多线程并行调度,同时通过对网络等库的封装,对用户屏蔽了调度细节。
最后,提供了Channel机制,用于Goroutine之间通信,实现CSP并发模型(Communicating Sequential Processes)。因为Go的Channel是通过语法关键词提供的,对用户屏蔽了许多细节。其实Go的Channel和Java中的SynchronousQueue是一样的机制,如果有buffer其实就是ArrayBlockQueue。
Goroutine调度器
这个图一般讲Goroutine调度器的地方都会引用,想要仔细了解的可以看看原博客。这里只说明几点:
Go实现了M:N的调度,也就是说线程和Goroutine之间是多对多的关系。这点在许多GreenThread/Coroutine的调度器并没有实现。比如Java1.1版本之前的线程其实是GreenThread(这个词就来源于Java),但由于没实现多对多的调度,也就是没有真正实现并行,发挥不了多核的优势,所以后来改成基于系统内核的Thread实现了。
某个系统线程如果被阻塞,排列在该线程上的Goroutine会被迁移。
具体的实现策略和我们前面分析的机制类似。系统启动时,会启动一个独立的后台线程,运行sysmon,其中就包括netpoll的轮询。当用户的Goroutine发起网络请求时,网络库会将fd(文件描述符)和pollDesc(用于描述netpoll的结构体,包含因为读/写这个fd而阻塞的Goroutine)关联起来,然后调用runtime.gopark方法,挂起当前的Goroutine。当后台的netpoll轮询收到epoll的event,会将event中的pollDesc取出来,找到关联的阻塞Goroutine,并进行恢复。
Goroutine是银弹么?
Goroutine很大程度上降低了并发的开发成本,是不是我们所有需要并发的地方直接go func就搞定了呢?
Go通过Goroutine的调度解决了CPU利用率的问题,实现了单机最高效率。但遇到其他的瓶颈资源如何处理?比如带锁的共享资源,比如数据库连接等。互联网在线应用场景下,如果每个请求都扔到一个Goroutine里,当资源出现瓶颈的时候,会导致大量的Goroutine阻塞,最后用户请求超时。这时候就需要用Goroutine池来进行控流,同时问题又来了:池子里设置多少个Goroutine合适?
Actor对没接触过这个概念的人可能不太好理解,Actor的概念其实和OO里的对象类似,是一种抽象。面对对象编程对现实的抽象是对象=属性+行为(method),但当使用方调用对象行为(method)的时候,其实占用的是调用方的CPU时间片,是否并发也是由调用方决定的。这个抽象其实和现实世界是有差异的。现实世界更像Actor的抽象,互相都是通过消息通信的。比如你对一个美女say hi,美女是否回应,如何回应是由美女自己决定的,运行在美女自己的大脑里,并不会占用发送者的大脑。
所以Actor有以下特征:
Processing – actor可以做计算的,不需要占用调用方的CPU时间片,并发策略也是由自己决定。
Storage – actor可以保存状态
Communication – actor之间可以通过发送消息通讯
Actor遵循以下规则:
发送消息给其他的Actor
创建其他的Actor
接受并处理消息,修改自己的状态
Actor的目标:
Actor可独立更新,实现热升级。因为Actor互相之间没有直接的耦合,是相对独立的实体,可能实现热升级。
无缝弥合本地和远程调用 因为Actor使用基于消息的通讯机制,无论是和本地的Actor,还是远程Actor交互,都是通过消息,这样就弥合了本地和远程的差异。
容错 Actor之间的通信是异步的,发送方只管发送,不关心超时以及错误,这些都由框架层接管。
易扩展,天然分布式 因为Actor的通信机制弥合了本地和远程调用,本地Actor处理不过来的时候,可以在远程节点上启动Actor然后转发消息过去。
Actor的实现:
Erlang/OTP Actor模型的标杆,其他的实现基本上都一定程度参照了Erlang的模式,实现了热升级以及分布式。
Akka(Scala,Java)基于线程和异步回调模式实现。由于Java中没有Fiber,所以是基于线程的。为了避免线程被阻塞,Akka中所有的阻塞操作都需要异步化。要么是Akka提供的异步框架,要么通过Future-callback机制,转换成回调模式。实现了分布式,但还不支持热升级。
Quasar (Java) 为了解决Akka的阻塞回调问题,Quasar通过字节码增强的方式,在Java中实现了Coroutine/Fiber。同时通过ClassLoader的机制实现了热升级。缺点是系统启动的时候要通过javaagent机制进行字节码增强。
Golang CSP VS Actor
二者的格言都是: & Don’t communicate by sharing memory, share memory by communicating
通过消息通信的机制来避免竞态条件,但具体的抽象和实现上有些差异。
CSP模型里消息和Channel是主体,处理器是匿名的。
也就是说发送方需要关心自己的消息类型以及应该写到哪个Channel,但不需要关心谁消费了它,以及有多少个消费者。Channel一般都是类型绑定的,一种Channel只写一种类型的消息,所以CSP需要支持alt/select机制,同时监听多个Channel。Channel是同步的模式(虽然Golang的Channel支持buffer,支持一定数量的异步),背后的逻辑是发送方非常关心消息是否被处理,CSP要保证每个消息都被正常处理了,没被处理就阻塞着。
Actor模型里Actor是主体,Mailbox(类似于CSP的Channel)是透明的。
也就是说它假定发送方会关心消息发给谁消费了,但不关心消息类型以及通道。所以Mailbox是异步模式,发送者不能假定发送的消息一定被收到和处理,同时必须支持强大的模式匹配机制,因为无论什么类型的消息都会通过同一个通道发送过来,需要通过模式匹配机制做分发。它背后的逻辑是现实世界本来就是异步的,不确定(non-deterministic)的,所以程序也要适应面对不确定的机制编程。自从有了并行之后,原来的确定编程思维模式已经受到了挑战,而Actor直接在模式中蕴含了这点。
从这样看来,CSP的模式比较适合Boss-Worker模式的任务分发机制,它的侵入性没那么强,可以在现有的系统中通过CSP解决某个具体的问题。它并不试图解决通信的超时容错问题,这个还是需要发起方进行处理。同时由于Channel是显式的,虽然可以通过netchan(原来Go提供的netchan机制由于过于复杂,被废弃,在讨论新的netchan)实现远程Channel,但很难做到对使用方透明。而Actor则是一种全新的抽象,使用Actor要面临整个应用架构机制和思维方式的变更。它试图要解决的问题要更广一些,比如容错,比如分布式。但Actor的问题在于以当前的调度效率,哪怕是用Goroutine这样的机制,也很难达到直接方法调用的效率。当前要像OO的『一切皆对象』一样实现一个『一切皆Actor』的语言,效率上肯定有问题。所以折中的方式是在OO的基础上,将系统的某个层面的组件抽象为Actor。
再扯一下Rust
本人没有Rust的开发经验,只是通过文档了解。Rust解决并发问题的思路是首先承认现实世界的资源总是有限的,想彻底避免资源共享是很难的,不试图完全避免资源共享,它认为并发的问题不在于资源共享,而在于错误的使用资源共享。比如我们前面提到的,大多数语言定义类型的时候,并不能限制调用方如何使用,只能通过文档或者标记的方式(比如Java中的@ThreadSafe ,@NotThreadSafe annotation),但也只能仅仅做到提示的作用,不能阻止调用方误用。虽然Go提供了-race机制,可以通过运行单元测试的时候带上这个参数来检测竞态条件,但如果你的单元测试并发度不够,覆盖面不到也检测不出来。所以Rust的解决方案就是:
定义类型的时候要明确指定该类型是否是并发安全的
引入了变量的所有权(Ownership)概念 非并发安全的数据结构在多个线程间转移,也不一定就会导致问题,导致问题的是多个线程同时修改,也就是说是因为这个变量的所有权不明确导致的。有了所有权的概念后,变量只能由拥有所有权的作用域代码修改,而变量传递会导致所有权变更,从语言层面限制了这种情况的出现。
有了这两套机制,Rust可以在编译期而不是运行期对竞态条件做检查和限制。虽然开发的时候增加了心智成本,但降低了调用方以及排查并发问题的心智成本,也是一种有特色的解决方案。
革命尚未成功 同志任需努力
本文带大家一起回顾了并发的问题,和各种解决方案。虽然各家有各家的优势以及使用场景,但并发/并行带来的问题还远远没到解决的程度。所以还需努力,大家也有机会啊。
最后抛个砖 构想:在Goroutine上实现Actor?
分布式 解决了单机效率问题,是不是可以尝试解决下分布式效率问题?
和容器集群融合 当前的自动伸缩方案基本上都是通过监控服务器或者LoadBalancer,设置一个阀值来实现的。类似于我前面提到的喂饭的例子,是基于经验的结果,但如果系统内和外部集群结合,这个事情就可以做的更细致和智能。
自管理 前面的两点最终的目标都是实现一个可以自管理的系统。做过系统运维的同学都知道,我们照顾我们的系统就像照顾孩子一样,时刻要监控系统的状态,系统的各种报警,然后排查问题,进行处理。孩子有长大的一天,那能不能让系统也自己成长,做到自管理呢?虽然这个目标现在看来还比较远,但我觉得是可以期待的。
引用以及扩展阅读
已发表评论数()
已收藏到推刊!
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
没有分页内容
图片无法显示
视频无法显示
与原文不一致

我要回帖

更多关于 actor 的文章

 

随机推荐