Actor模式理解與使用

     最近學習ThingsBoard,其中大量使用了Actor設計模式,再這裏做個Actor模式理解與使用的筆記

     Actor模式是一種併發模型,與另一種模型共享內存完全相反,Actor模型share nothing。所有的線程(或進程)通過消息傳遞的方式進行合作,這些線程(或進程)稱爲Actor。

      共享內存更適合單機多核的併發編程,而且共享帶來的問題很多,編程也困難。隨着多核時代和分佈式系統的到來,共享模型已經不太適合併發編程,因此幾十年前就已經出現的Actor模型又重新受到了人們的重視。

多線程編程-共享內存


到了多核時代,有多個工人,這些工人共同使用一個倉庫和車間,幹什麼都要排隊。比如我要從一塊鋼料切出一塊來用,我得等別人先用完。有個扳手,另一個人在用,我得等他用完。兩個人都要用一個切割機從一塊鋼材切一塊鋼鐵下來用,但是一個人拿到了鋼材,一個人拿到了切割機,他們互相都不退讓,結果誰都幹不了活。

wKiom1WOKpHgGDoXAAFY8MuvOgA842.jpg

 

多線程/分佈式編程-Actor模型


到了分佈式系統時代,工廠已經用流水線了,每個人都有明確分工,這就是Actor模式。每個線程都是一個Actor,這些Actor不共享任何內存,所有的數據都是通過消息傳遞的方式進行的。

wKiom1WOKyPBDRjpAAGSeStUphQ651.jpg

如果用Actor模型實現統計素數個數,那麼我們需要1個actor做原料的分發,就是提供要處理的整數,然後10個actor加工,每次從分發actor那裏拿一個整數進行加工,最終把加工出來的半成品發給組裝actor,組裝actor把10個加工actor的結果彙總輸出。

 Actor模式的Springboot實現

瞭解過Akka或者Actor的人應該知道,這的確是一個很不錯的框架,按照Akka官網的描述——使用Akka使得構建強有力的併發與分佈式應用將更加容易。

與springboot集成,是因爲我們想把ActorSystem、Actor等組件的創建納入SpringBoot容器中,方便管理。大家都知道,ActorSystem的創建不是依賴new方式,而是通過create方法,所以我們需要寫一個Bean來生產ActorSystem。另外Actor,它也是通過actorOf()方法創建的,所以我們也需要寫生產Actor引用的方法,Akka提供了IndirectActorProducer接口,通過實現該接口,我們就可以實現DI(依賴注入)。集成springboot之後,ActorSystem範圍內的依賴都會交給SpringBoot來管理,並且每個ActorSystem都會持有一個ApplicationContext。

下面我們開始來看看集成是怎樣實現的:

在實現之前,我們需要加入相關依賴,如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.3.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>io.kunalk.spring</groupId>
	<artifactId>springakka</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springakka</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>com.typesafe.akka</groupId>
			<artifactId>akka-actor_2.11</artifactId>
			<version>2.4.3</version>
		</dependency>

		<dependency>
			<groupId>com.typesafe.akka</groupId>
			<artifactId>akka-slf4j_2.11</artifactId>
			<version>2.4.3</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>


第一步,實現IndirectActorProducer,用於生產Actor,既然是交給Spring管理,肯定少不了ApplicationContext對象和bean名稱:

/**
 * 實現IndirectActorProducer,用於生產Actor
 *
 * <p/>
 * Date 2019年8月8日 下午6:44:05
 * <p/>
 * @author ieflex
 */
public class SpringActorProducer implements IndirectActorProducer {

	final private ApplicationContext applicationContext;
	final private String actorBeanName;

	public SpringActorProducer(ApplicationContext applicationContext, String actorBeanName) {
		this.applicationContext = applicationContext;
		this.actorBeanName = actorBeanName;
	}

	@Override
	public Actor produce() {
		return (Actor) applicationContext.getBean(actorBeanName);
	}

	@Override
	public Class<? extends Actor> actorClass() {
		return (Class<? extends Actor>) applicationContext.getType(actorBeanName);
	}
}


第二步,實現了DI之後,我們就需要構造Props對象,用來創建ActorRef:

/**
 * 擴展組件,ApplicationContext會在SpringBoot初始化的時候加載進來 <br/>
 * 構造Props,用於生產ActorRef
 */
@Component
public class SpringExtension implements Extension {

	private ApplicationContext applicationContext;

	public void initialize(ApplicationContext applicationContext) {
		this.applicationContext = applicationContext;
	}

	/**
	 * 該方法用來創建Props對象,依賴前面創建的SpringActorProducer,DI組件,獲取到Props對象,我們就可以創建Actor bean對象
	 *
	 * @param beanName actor bean 名稱
	 * @return props
	 */
	public Props props(String actorBeanName) {
		return Props.create(SpringActorProducer.class, applicationContext, actorBeanName);
	}
}


第三步,通過SpringExtProvider我們可以獲取到SpringExt,通過SpringExt我們可以使用Props創建ActorRef對象,那麼現在我們怎麼來初始化ActorSystem,並掃描到納入到容器的Actor呢?我們可以通過@Configuration來創建一個配置類:

/**
 * 創建ActorSystem,並將其放入到spring管理,初始化ApplicationContext
 *
 * <p/>
 * Date 2019年8月8日 下午6:50:20
 * <p/>
 * 
 * @author ieflex
 */
@Configuration
class ApplicationConfiguration {

	@Autowired
	private ApplicationContext applicationContext;

	@Autowired
	private SpringExtension springExtension;

	@Bean
	public ActorSystem actorSystem() {
		ActorSystem actorSystem = ActorSystem.create("actor-system", akkaConfiguration());
		springExtension.initialize(applicationContext);
		return actorSystem;
	}

	@Bean
	public Config akkaConfiguration() {
		return ConfigFactory.load();
	}
}


下面,我們就來創建一個Actor示例,來驗證集成是否成功,如下:


/**
 * 一般情況下我們的Actor都需要繼承自UntypedActor,並實現其onReceive方法。onReceive用於接收消息,你可以在其中實現對消息的匹配並做不同的處理。
 * 
 */
@Component("workerActor")
@Scope("prototype")
public class WorkerActor extends UntypedActor {

	@Autowired
	private BusinessService businessService;

	private int count = 0;

	@Override
	public void onReceive(Object message) throws Exception {
		if (message instanceof Request) {
			businessService.perform(this + " " + (++count));
		} else if (message instanceof Response) {
			getSender().tell(count, getSelf());
		} else {
			unhandled(message);
		}
	}

	public static class Request {
	}

	public static class Response {
	}
}


特別注意,使用依賴注入框架時,Actor不能配置爲單例的,否則程序會出現問題。這裏我們簡單輸出接受到的消息。

測試:

@Component
class Runner implements CommandLineRunner {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private ActorSystem actorSystem;

    @Autowired
    private SpringExtension springExtension;

    @Override
    public void run(String[] args) throws Exception {
        try {
        	//Actor,它也是通過actorOf()方法創建
            ActorRef workerActor = actorSystem.actorOf(springExtension.props("workerActor"), "worker-actor");

            workerActor.tell(new WorkerActor.Request(), null);
            workerActor.tell(new WorkerActor.Request(), null);
            workerActor.tell(new WorkerActor.Request(), null);

            FiniteDuration duration = FiniteDuration.create(1, TimeUnit.SECONDS);
            Future<Object> awaitable = Patterns.ask(workerActor, new WorkerActor.Response(), Timeout.durationToTimeout(duration));

            logger.info("Response: " + Await.result(awaitable, duration));
        } finally {
            actorSystem.terminate();
            Await.result(actorSystem.whenTerminated(), Duration.Inf());
        }
    }
}


啓動測試結果:
 

08/08 18:49:19.246 INFO [main] i.k.s.s.SpringakkaApplication - Starting SpringakkaApplication on lihui with PID 61208 (D:\eclipse-workspace\workspace\springboot-akka\target\classes started by lily in D:\eclipse-workspace\workspace\springboot-akka)
08/08 18:49:19.249 INFO [main] i.k.s.s.SpringakkaApplication - No active profile set, falling back to default profiles: default
08/08 18:49:20.091 INFO [actor-system-akka.actor.default-dispatcher-4] a.e.s.Slf4jLogger - Slf4jLogger started
08/08 18:49:20.296 INFO [main] i.k.s.s.SpringakkaApplication - Started SpringakkaApplication in 1.244 seconds (JVM running for 2.218)
08/08 18:49:20.299 INFO [actor-system-akka.actor.default-dispatcher-5] i.k.s.s.s.BusinessService - Perform: io.kunalk.spring.springakka.actor.WorkerActor@70b82aef 1
08/08 18:49:20.300 INFO [actor-system-akka.actor.default-dispatcher-5] i.k.s.s.s.BusinessService - Perform: io.kunalk.spring.springakka.actor.WorkerActor@70b82aef 2
08/08 18:49:20.300 INFO [actor-system-akka.actor.default-dispatcher-5] i.k.s.s.s.BusinessService - Perform: io.kunalk.spring.springakka.actor.WorkerActor@70b82aef 3
08/08 18:49:20.300 INFO [main] i.k.s.s.Runner - Response: 3

例子地址:

https://github.com/ieflex/springboot-akka

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章