通过实际的例子,介绍编译器的工作过程

Posted JerryWangSAP

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过实际的例子,介绍编译器的工作过程相关的知识,希望对你有一定的参考价值。

本文详细介绍下面这张图。

编译器是将高级语言代码翻译为机器语言代码的工具。编译器的工作可以划分为多个重要阶段,以下是其中几个常见的阶段,并给出了具体例子:

  1. 词法分析(Lexical Analysis):
    在词法分析阶段,编译器将源代码分解成词法单元(Token)序列。词法单元是语法上具有意义的最小单位,例如标识符、关键字、运算符、常数等。编译器使用正则表达式和有限自动机等技术来扫描源代码并生成词法单元序列。

    例如,对于下面的C语言代码片段:

    int x = 5 + 3;
    

    词法分析器将把它分解为如下的词法单元序列:

    <int> <x> <=> <5> <+> <3> <;>
    
  2. 语法分析(Syntax Analysis):
    在语法分析阶段,编译器根据词法单元序列构建抽象语法树(Abstract Syntax Tree,AST)。抽象语法树表示程序的结构和语法关系,它反映了代码中的表达式、语句和语法规则等。编译器使用上下文无关文法和语法分析算法(如LL(1)、LR(1)等)来进行语法分析。

    以同样的C语言代码为例,语法分析器将根据词法单元序列构建如下的抽象语法树:

           =
          / \\
         /   \\
       int    +
            / \\
           5   3
    
  3. 语义分析(Semantic Analysis):
    在语义分析阶段,编译器检查代码中的语义错误和类型一致性。它进行符号表的管理,确保变量、函数和类型的正确使用。编译器还执行类型推导、类型转换和常量折叠等优化操作,以确保代码的语义正确性。

    例如,在C语言中,对于以下代码片段:

    int x = 5;
    char y = x;
    

    语义分析器将检测到赋值语句中的类型不匹配错误,因为将整型值赋给了字符型变量。

  4. 中间代码生成(Intermediate Code Generation):
    在中间代码生成阶段,编译器将抽象语法树转换为中间表示(Intermediate Representation,IR)或中间代码。中间代码是一种高级的、与机器无关的代码,通常采用三地址码、四地址码等形式表示。中间代码作为连接前后端的桥梁,方便进行优化和目标代码生成。

前面的C语言代码为例,中间代码生成器可以生成如下的三地址码表示:

t1 = 5
t2 = 3
t3 = t1 + t2
x = t3

贯穿于编译器工作始终的行为是符号表的管理。符号表是编译器在词法分析、语法分析和语义分析过程中维护的数据结构,用于存储标识符(如变量、函数名)和其属性(如类型、作用域等)的信息。符号表的管理是为了确保代码中的标识符被正确声明和使用,并提供语义分析和后续阶段所需的信息。

在上述的阶段中,编译器会使用符号表来进行标识符的声明、引用和解析,以及类型检查和作用域的判定等操作。符号表的管理使得编译器能够对代码进行正确的语义分析,并在后续阶段生成正确的中间代码和目标代码。

使用Akka构建集群

前言

在《使用Akka构建集群(一)》一文中通过简单集群监听器的例子演示了如何使用Akka搭建一个简单的集群,但是这个例子“也许”离我们的实际业务场景太远,你基本不太可能去做这样的工作,除非你负责运维、监控相关的工作(但实际上一个合格的程序员在实现功能的同时,也应当考虑监控的问题,至少应当接入一些监控系统或框架)。

本文将介绍一个相对看来更符合我们对于集群使用的业务需求的例子——将客户端请求的字符串转换为大写(假如客户端真的没有这个能力的话)。

服务端

本文的Akka配置继续沿用《使用Akka构建集群(一)》一文中所展示的配置,但在正式编码之前我们需要在配置中增加一个新的配置项akka.cluster.roles指定集群中服务端的角色,重新编辑过后的application.conf如下:

 

akka {
  actor {
    provider = "akka.cluster.ClusterActorRefProvider"
  }
  remote {
    log-remote-lifecycle-events = off
    netty.tcp {
      hostname = "127.0.0.1"
      port = 2551
    }
  }

  cluster {
    seed-nodes = [
      "akka.tcp://metadataAkkaSystem@127.0.0.1:2551",
      "akka.tcp://metadataAkkaSystem@127.0.0.1:2552"]

    #//#snippet
    # excluded from snippet
    auto-down-unreachable-after = 10s
    #//#snippet
    # auto downing is NOT safe for production deployments.
    # you may want to use it during development, read more about it in the docs.
    #
    # auto-down-unreachable-after = 10s
    roles = [backend]
    # Disable legacy metrics in akka-cluster.
	metrics.enabled=off
  }

}

你仍然不需要过多产生于集群直接相关的细节。如果你已经阅读了《使用Akka构建集群(一)》一文,本文介绍的内容应该不会花费你太多的时间。

 

客户端与服务端通信需要一些pojo,它们的实现如下:

 

public interface TransformationMessages {

	public static class TransformationJob implements Serializable {
		private final String text;

		public TransformationJob(String text) {
			this.text = text;
		}

		public String getText() {
			return text;
		}
	}

	public static class TransformationResult implements Serializable {
		private final String text;

		public TransformationResult(String text) {
			this.text = text;
		}

		public String getText() {
			return text;
		}

		@Override
		public String toString() {
			return "TransformationResult(" + text + ")";
		}
	}

	public static class JobFailed implements Serializable {
		private final String reason;
		private final TransformationJob job;

		public JobFailed(String reason, TransformationJob job) {
			this.reason = reason;
			this.job = job;
		}

		public String getReason() {
			return reason;
		}

		public TransformationJob getJob() {
			return job;
		}

		@Override
		public String toString() {
			return "JobFailed(" + reason + ")";
		}
	}

	public static final String BACKEND_REGISTRATION = "BackendRegistration";

}

TransformationJob代表待转换的任务,其text属性是需要处理的字符串文本;TransformationResult是任务处理的结果,其text属性是转换完成的字符串文本;JobFailed是任务失败,其reason属性代表失败原因;字符串常量BACKEND_REGISTRATION用于服务端向客户端注册,以便于客户端知道有哪些服务端可以提供服务。

 

服务端用于将字符串转换为大写的Actor(正如我之前的文章所言,真正的处理应当从Actor中分离出去,只少通过接口解耦)的实现见代码清单1所示。

代码清单1

 

@Named("TransformationBackend")
@Scope("prototype")
public class TransformationBackend extends UntypedActor {
	
	private static Logger logger = LoggerFactory.getLogger(TransformationBackend.class);

	Cluster cluster = Cluster.get(getContext().system());

	// subscribe to cluster changes, MemberUp
	@Override
	public void preStart() {
		cluster.subscribe(getSelf(), MemberUp.class);
	}

	// re-subscribe when restart
	@Override
	public void postStop() {
		cluster.unsubscribe(getSelf());
	}

	@Override
	public void onReceive(Object message) {
		if (message instanceof TransformationJob) {
			TransformationJob job = (TransformationJob) message;
			logger.info(job.getText());
			getSender().tell(new TransformationResult(job.getText().toUpperCase()), getSelf());

		} else if (message instanceof CurrentClusterState) {
			CurrentClusterState state = (CurrentClusterState) message;
			for (Member member : state.getMembers()) {
				if (member.status().equals(MemberStatus.up())) {
					register(member);
				}
			}

		} else if (message instanceof MemberUp) {
			MemberUp mUp = (MemberUp) message;
			register(mUp.member());

		} else {
			unhandled(message);
		}
	}

	void register(Member member) {
		if (member.hasRole("frontend"))
			getContext().actorSelection(member.address() + "/user/transformationFrontend").tell(BACKEND_REGISTRATION, getSelf());
	}
}

TransformationBackend在preStart方法中订阅了集群的MemberUp事件,这样当它发现新注册的集群成员节点的角色是frontend(前端)时,将向此节点发送BACKEND_REGISTRATION消息,后者将会知道前者提供了服务。TransformationBackend所在的节点在刚刚加入集群时,TransformationBackend还会收到CurrentClusterState消息,从中可以解析出集群中的所有前端节点(即roles为frontend的),并向其发送BACKEND_REGISTRATION消息。经过以上两步可以确保集群中的前端节点和后端节点无论启动或加入集群的顺序怎样变化,都不会影响后端节点通知所有的前端节点及前端节点知道哪些后端节点提供了服务。

 

客户端

客户端除了监听端口不同外,也需要增加akka.cluster.roles配置项,我们指定为frontend。客户端的配置如下:

 

akka {
  actor {
    provider = "akka.cluster.ClusterActorRefProvider"
  }
  remote {
    log-remote-lifecycle-events = off
    netty.tcp {
      hostname = "127.0.0.1"
      port = 0
    }
  }

  cluster {
    seed-nodes = [
      "akka.tcp://metadataAkkaSystem@127.0.0.1:2551",
      "akka.tcp://metadataAkkaSystem@127.0.0.1:2552"]

    #//#snippet
    # excluded from snippet
    auto-down-unreachable-after = 10s
    #//#snippet
    # auto downing is NOT safe for production deployments.
    # you may want to use it during development, read more about it in the docs.
    #
    # auto-down-unreachable-after = 10s
    roles = [frontend]
  }

}

客户端用于处理转换任务的Actor见代码清单2所说。

 

代码清单2

 

@Named("TransformationFrontend")
@Scope("prototype")
public class TransformationFrontend extends UntypedActor {

  List<ActorRef> backends = new ArrayList<ActorRef>();
  int jobCounter = 0;

  @Override
  public void onReceive(Object message) {
    if ((message instanceof TransformationJob) && backends.isEmpty()) {
      TransformationJob job = (TransformationJob) message;
      getSender().tell(
          new JobFailed("Service unavailable, try again later", job),
          getSender());

    } else if (message instanceof TransformationJob) {
      TransformationJob job = (TransformationJob) message;
      jobCounter++;
      backends.get(jobCounter % backends.size())
          .forward(job, getContext());

    } else if (message.equals(BACKEND_REGISTRATION)) {
      getContext().watch(getSender());
      backends.add(getSender());

    } else if (message instanceof Terminated) {
      Terminated terminated = (Terminated) message;
      backends.remove(terminated.getActor());

    } else {
      unhandled(message);
    }
  }

}

可以看到TransformationFrontend处理的消息分为以下三种:

 

 

  • BACKEND_REGISTRATION:收到此消息说明有服务端通知客户端,TransformationFrontend首先将服务端的ActorRef加入backends列表,然后对服务端的ActorRef添加监管;
  • Terminated:由于TransformationFrontend对服务端的ActorRef添加了监管,所以当服务端进程奔溃或者重启时,将收到Terminated消息,此时TransformationFrontend将此服务端的ActorRef从backends列表中移除;
  • TransformationJob:此消息说明有新的转换任务需要TransformationFrontend处理,处理分两种情况:
  1. backends列表为空,则向发送此任务的发送者返回JobFailed消息,并告知“目前没有服务端可用,请稍后再试”;
  2. backends列表不为空,则通过取模运算选出一个服务端,将TransformationJob转发给服务端进一步处理;

运行展示

初始化服务端TransformationBackend的代码如下:

		logger.info("Start transformationBackend");
		final ActorRef transformationBackend = actorSystem.actorOf(springExt.props("TransformationBackend"), "transformationBackend");
		actorMap.put("transformationBackend", transformationBackend);
		logger.info("Started transformationBackend");

初始化客户端TransformationFrontend的代码如下:

		logger.info("Start transformationFrontend");
		final ActorRef transformationFrontend = actorSystem
				.actorOf(springExt.props("TransformationFrontend"), "transformationFrontend");
		actorMap.put("transformationFrontend", transformationFrontend);
		logger.info("Started transformationFrontend");
		final FiniteDuration interval = Duration.create(2, TimeUnit.SECONDS);
		final Timeout timeout = new Timeout(Duration.create(5, TimeUnit.SECONDS));
		final ExecutionContext ec = actorSystem.dispatcher();
		final AtomicInteger counter = new AtomicInteger();
		actorSystem.scheduler().schedule(interval, interval, new Runnable() {
			public void run() {
				ask(transformationFrontend, new TransformationJob("hello-" + counter.incrementAndGet()), timeout)
						.onSuccess(new OnSuccess<Object>() {
							public void onSuccess(Object result) {
								logger.info(result.toString());
							}
						}, ec);
			}
		}, ec);

可以看到我们在客户端每2秒将发送一个新的消息,这个消息以“hello-”开头,后边是一个不断自增的数字。当收到处理结果后,客户端还会将结果打印出来。

我们以3个服务端节点(host相同,端口分别为2551、2552及随机)、1个客户端节点(端口随机)组成的集群为例,我们首先启动第一个种子节点,然后以任意顺序启动其它服务端或者客户端节点(启动顺序问题在《使用Akka构建集群(一)》一文中已介绍,此处不再赘述),集群成员变化的日志如下图:

从上面展示的日志中可以看到集群的3个服务端节点和1个客户端节点先后加入集群的信息。

我们再来看看端口为57222的角色为frontend的节点的日志信息,如下图:

从frontend的日志看出,它已经打印了大写得HELLO-3到HELLO-10十条任务处理结果。那么这些任务分别是由集群中的哪些节点负责处理的?我们首先来看看端口为2551的backend节点,其处理任务的日志如下图:

看来2551节点处理了hello-4、hello-7及hello-10三条任务。我们再来看看端口为2552的backend节点,其处理任务的日志如下图:

 

可以看到2552节点处理了hello-2、hello-5及hello-8三条任务。最后看看端口为57211的backend节点,其处理任务的日志如下图:

可以看到从hello-3到hello-10这8条处理任务被均衡的分配给了3个不同的后端节点处理。奇怪的是hello-1这条消息居然没有任何显示,那是因为前端节点刚开始处理消息时,backends列表里还没有缓存好任何backend的ActorRef。我们向上查找frontend节点的日志,在相隔很远的日志中发现了下面的输出:

这也印证了我们的猜测。

总结

根据本文的例子,大家应当看到使用Akka构建集群,开发人员只需要关注消息的发送与接收,而无需过多涉及集群的细节。无论前端还是后端节点都可以加入同一个集群,而且多个后端节点处理消息也能达到负载均衡的功效。

 

其它Akka应用的博文如下:

 

  1. Spring与Akka的集成》;
  2. 使用Akka的远程调用》;
  3. 使用Akka构建集群(一)》;
  4. 使用Akka构建集群(二)》;
  5. 使用Akka持久化——持久化与快照》;
  6. 使用Akka持久化——消息发送与接收》;

 

 

 

后记:个人总结整理的《深入理解Spark:核心思想与源码分析》一书现在已经正式出版上市,目前京东、当当、天猫等网站均有销售,欢迎感兴趣的同学购买。

 

京东:http://item.jd.com/11846120.html 

当当:http://product.dangdang.com/23838168.html 

 

 

 

以上是关于通过实际的例子,介绍编译器的工作过程的主要内容,如果未能解决你的问题,请参考以下文章

使用Akka构建集群

使用Akka构建集群

Linux相识相知yum的配置使用和程序包的编译安装

转载Spring Cache介绍

用一个实际例子理解Docker volume工作原理

一个 Spring AOP 例子