异步通讯
重要提示 :在继续之前,请阅读本文的第一部分 。
在第一部分中,我们研究了如何使用Erlport从Elixir调用Python函数!
ErlPort是一个Elixir库,可以使用Erlang端口协议更轻松地将Erlang连接到许多其他编程语言。
但是,您可能已经注意到,调用过程必须等待Python完成处理并返回结果。 这适用于即时操作,无需花费很多时间。 否则,当您运行密集的Python函数时,它就不会很有趣。 因为您的Elixir程序将挂起,等待Python代码完成。
对于需要一段时间才能完成的操作,我们需要一种方法来调用Python函数,并在函数完成时在elixir代码中得到通知-类似于我们编写elixir程序时,进程与每个异步对话的过程。 这很有用,尤其是在您希望Elixir进程控制/监督多个Python操作的情况下。 我们可以同时运行Python函数,这会使生活变得更好。
在本文中,我们将探讨如何实现这一目标-Python和Elixir之间的异步通信!
我们可以推论的挑战,我们在药剂进程间通信的方式相同-通过使用cast
-这使我们能够异步进程间通信。
以前,我们在从Python调用函数时主要使用erlport的 :python.call/4
。 这是同步的,并且elixir程序必须等待python函数完成并发送响应。
在这篇文章中,我们将使用:python.cast/2
了解:python.cast / 2
:python.cast/2
与GenServer.cast/2
工作原理相同。 您给它提供进程ID(或注册的进程名称)以及要发送给该进程的消息。 与:python.call
变体不同, :python.cast
用于发送消息,而不是调用Python函数。 您的Elixir进程发送消息的方式相同。 Erlport为我们提供了一种接收发送到python实例的消息的机制。
Erlport Python模块
Erlport提供了一个python库 ,可帮助您处理elixir数据类型和过程。 您可以使用pip安装erlport。
pip install erlport #don't run this yet!
但是,Erlport Elixir存储库已随附Python库,该库会在运行时自动加载。 因此,如果您通过Elixir使用Erlport,则不必安装Erlport Python库。
下面是如何在Erlang / Elixir和Python之间映射数据
处理从Elixir发送到Python的消息
对于Elixir进程,您有一个接收块,在其中处理所有消息。 如果使用的是GenServer,则可以使用handle_call
, handle_cast
和handle_info
函数。 Erlport提供了一种类似的机制来处理发送到Python进程的消息。 set_message_handler
是函数。 在erlport.erlang
模块中。 它采用带有一个参数消息(即传入消息)的Python函数。 每当收到消息时, set_message_handler
就会调用传递给set_message_handler
的函数。 这就像一个回调函数。
从Python发送消息到Elixir
由于Elixir进程不知道是谁发送了强制转换消息,因此我们必须找到一种方法来知道python函数完成后将结果发送给哪个进程。 一种方法是注册要将结果发送到的Elixir进程的进程ID。
所述Erlport蟒模块提供cast
功能用于发送消息给药剂处理。 使用此功能,我们可以在函数完成后将消息异步发送回Elixir进程!
放在一起
打开终端并使用mix创建一个新的Elixir项目
$ mix new elixir_python
添加依赖
将erlport
添加到依赖项mix.exs
defp deps do
[
{:erlport, "~> 0.9"} ,
]
end
然后安装项目依赖项。
$ cd elixir_python
$ mix deps.get
创建priv/python
目录,您将在其中保留我们的python模块
$ mkdir -p priv/python
为Erlport相关功能创建一个elixir模块包装器。
#lib/python.ex
defmodule ElixirPython.Python do
@doc """
Start python instance with custom modules dir priv/python
"""
def start() do
path = [
:code.priv_dir(:elixir_python),
"python"
]|> Path.join()
{:ok, pid} = :python.start([
{:python_path, to_charlist(path)}
])
pid
end
def call(pid, m, f, a \\ []) do
:python.call(pid, m, f, a)
end
def cast(pid, message) do
:python.cast(pid, message)
end
def stop(pid) do
:python.stop(pid)
end
end
为了使Elixir进程异步接收消息,我们将创建一个简单的GenServer
模块
#lib/python_server.ex
defmodule ElixirPython.PythonServer do
use GenServer
alias ElixirPython.Python
def start_link() do
GenServer.start_link(__MODULE__, [])
end
def init(args) do
#start the python session and keep pid in state
python_session = Python.start()
#register this process as the message handler
Python.call(python_session, :test, :register_handler, [self()])
{:ok, python_session}
end
def cast_count(count) do
{:ok, pid} = start_link()
GenServer.cast(pid, {:count, count})
end
def call_count(count) do
{:ok, pid} = start_link()
# :infinity timeout only for demo purposes
GenServer.call(pid, {:count, count}, :infinity)
end
def handle_call({:count, count}, from, session) do
result = Python.call(session, :test, :long_counter, [count])
{:reply, result, session}
end
def handle_cast({:count, count}, session) do
Python.cast(session, count)
{:noreply, session}
end
def handle_info({:python, message}, session) do
IO.puts("Received message from python: #{inspect message}")
#stop elixir process
{:stop, :normal, session}
end
def terminate(_reason, session) do
Python.stop(session)
:ok
end
end
现在让我们创建python模块priv/python/test.py
#priv/python/test.py
import time
import sys
#import erlport modules and functions
from erlport.erlang import set_message_handler, cast
from erlport.erlterms import Atom
message_handler = None #reference to the elixir process to send result to
def cast_message(pid, message):
cast(pid, message)
def register_handler(pid):
#save message handler pid
global message_handler
message_handler = pid
def handle_message(count):
try :
print "Received message from Elixir"
print count
result = long_counter(count)
if message_handler:
#build a tuple to atom {:python, result}
cast_message(message_handler, (Atom('python'), result))
except Exception, e:
# you can send error to elixir process here too
# print e
pass
def long_counter(count=100):
#simluate a time consuming python function
i = 0
data = []
while i < count:
time.sleep(1) #sleep for 1 sec
data.append(i+1)
i = i + 1
return data
set_message_handler(handle_message) #set handle_message to receive all messages sent to this python instance
编译!
$ mix compile
跑!
$ iex -S mix
iex(1)> ElixirPython.PythonServer.call_count(4)
[1, 2, 3, 4] #printed after waiting 4sec
iex(2)> ElixirPython.PythonServer.call_count(10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] #printed after waiting 10sec!
还要注意, call_count
会导致iex
挂起,直到我们从python获得返回结果为止
现在让我们异步调用相同的python函数!
iex(3)> ElixirPython.PythonServer.cast_count(4)
:ok
Received message from Elixir
4
Received message from python: [1, 2, 3, 4] # printed after 4sec, no wait
iex(4)> ElixirPython.PythonServer.cast_count(10)
:ok
Received message from Elixir
10
iex(5)>
nil
Received message from python: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] #printed after 10sec without blocking
请注意,我们的python函数完成后,我们如何继续在iex中工作并获得结果。 尝试使用大小不同的数字调用cast_count
。
这就对了! 现在,您可以在Elixir和Python之间异步通信。
编码愉快!
From: https://hackernoon.com/mixing-python-with-elixir-ii-async-e8586f9b2d53