pyepics使用Python通道访问的高级话题

24 篇文章 24 订阅

本章包含各种"使用注释"和实现细节,它们可能在获取来自pyepics模块的最佳性能中得到帮助。

用于get(),ca.get_complete()的wait和timeout选选项

get函数epics.caget(), pv.get()和epics.ca.get()都请求从网络传递数据。对于大型数组或者慢速网络,这会花费显著的时间。对于已经断开的PVs,get调用将完全不返回一个值。出于这些原因,这些函数都接收一个timeout关键字选项。最底层epics.ca.get()也有一个wait选项,以及一个相伴的函数epics.ca.get_complete()。这部分描述这些的细节。

如果你正在使用epics.caget()或pv.get(),你可以提供一个timeout值。如果返回的值是None,则要么PV真地断开了或者在接收到数据前已过了超时时间。如果get没有结束,在那种PV连接了但数据还未被接收到,一个后续的epics.caget()或pv.get()将最终结束并且接收到这个值。即是,如果对应大型waveform记录的PV报告它连接了,但一个pv.get()返回None,只要在之后再次尝试将有作用:

>>> p = epics.PV('LargeWaveform')
>>> val = p.get()
>>> val
>>> time.sleep(10)
>>> val = p.get()

在最底层(由pv.get()和epics.caget()使用它),epics.ca.get()发出一个带有内部回调函数的get-request。即是,它用一个预定义的回调函数调用CA库函数libca.ca_array_get_callback()。用wait=True(默认),epics.ca.get()接着等待到超时或者直到CA库调用指定的回调函数。如果回调函数被调用了,接着可以转化和返回这个值。

如果没有及时调用这个回调或者如果使用了wait=False,但PV连接了,最终将调用这个回调,并且仅等待(或者如果epics.ca.PREEMPTIVE_CALLBACK为False,使用epics.ca.pend_event())可能对于数据到达足够了。在这种情况下,你可以调用epics.ca.get_complete(),这将不发出对应发送数据的新请求,但为先前的get请求完成等待(最长timeout时间)。

如果超出了timeout或者如果没有它能够等待的"未完成get"结束,epics.ca.get_complete将返回None。因而,你应该小心地使用来自epics.ca.get_complete()的返回值。

注意:pv.get()(以及epics.caget())通常将依赖由监视回调程序自动填入PV值。如果监视回调被禁用(如对大型数组所作的以及可以被关闭)或者如果监视还未被调用,pv.get()将检查它是否应该调用epics.ca.get()或者epics.ca.get_complete()。

如果没有指定,对应epics.ca.get_complete的timeout(和所有其它get函数)将被设置成:

timeout = 0.5 + log10(count)

那是将等待的最长时间,并且如果接收数据快于那个,get将尽快返回。

对应连接很多PVs的策略

import epics

pvnamelist = read_list_pvs()
pv_vals = {}
for name in pvnamelist:
    pv = epics.PV(name)
    pv_vals[name] = pv.get()

或者甚至只要:

values = [epics.caget(name) for name in pvnamelist]

这引起了某些性能损失。要最小化代价,我们需要了解其原因。

创建一个PV对象(使用pv.PV或者pv.get()或者epics.caget()中之一)将自动使用连接和事件回调,尝试在这个会话中保持PV存活和最新。通常,这是一个优势,由于你不需要显式地处理通道访问地很多方面。但创建一个PV确实请求一些网络传输,并且在建立所有连接和事件回调时,这个PV才"完全连接"并且准备进行一个PV.get()。实际上,PV.get()将在那些连接都建立时才运行。对于每个PV,这花费非常接近30毫秒。即是,对于1000个PVs,以上方法将花费大约30秒。

最简单地修改是通过首先创建所有PVs并且接着获取它们地值,允许所有那些连接并行和在后天发生。那将看起来像这样:

# improve time to get multiple PVs:  Method 1
import epics

pvnamelist = read_list_pvs()
pvs = [epics.PV(name) for name in pvnamelist]
values = [p.get() for p in pvs]

可以进一步改进它吗?回调是可以地,但要以代价。为了这里讨论,我们可以称原来版本"方法0",而创建所有PVs接着获取它们的值的方法称为"方法1“。用这两个方法,这个脚本有了对应所有指定名称的PVs的完全连接的PV对象,因而这些PVs的后续使用将非常高效。

通过关闭任何连接或者事件回调,避免PV对象一起以及使用epics.ca接口,这可以变得更快。这已经被封装到了epics.caget_many(),其可以用作:

# get multiple PVs as fast as possible:  Method 2
import epics
pvnamelist = read_list_pvs()
values = epics.caget_many(pvlist)

在使用全都已经连接的1000个PVs的测试中,方法2将花费大于0.25秒,相较于方法1的0.4秒和方法0的30秒。要理解epics.caget_many()做了什么,一个这个的更加完整版本看起来像这样:

# epics.caget_many made explicit:  Method 3
from epics import ca

pvnamelist = read_list_pvs()

pvdata = {}
pvchids = []
# create, don't connect or create callbacks
for name in pvnamelist:
    chid = ca.create_channel(name, connect=False, auto_cb=False) # note 1
    pvchids.append(chid)

# connect
for chid in pvchids:
    ca.connect_channel(chid)

# request get, but do not wait for result
ca.poll()
for chid in pvchids:
    ca.get(chid, wait=False)  # note 2

# now wait for get() to complete
ca.poll()
for chid in pvchids:
    val = ca.get_complete(data[0])
    pvdata[ca.name(chid)] = val

这里的代码可能需要详细的解释。如上提到的,它使用ca层,非PV对象。第二,对epics.ca.create_channel()的调用使用connect=False和auto_cb=False,这表示在返回前不等待连接,并且不自动分配一个连接回调。

通常,这些不是你所想要的,由于你想要一个连接的通道并且如果连接状态变化,得到通知,但我们这里目标未最快速度。我们接着使用epics.ca.connect_channel()来连接所有通道。接着,我们告诉CA库请求这个通道的数据而不等待接收它。不使epics.ca.get()等待随我们进行的每个通道的数据的主要目的是每个数据传输花费时间。我们而是为所有通道在一个独立线程中发送请求的数据,而不等待。接着我们通过仅调用一次epics.ca.poll()等待,(不是len(pvnamelist)次)。最终,我们使用epics.ca.get_complete()方法转化现在已经被伴随线程接收到的数据到一个python值。

方法2和方法3有相同的运行时,其某种程度上快于方法1,并且比方法0快得多。使用哪种方法取决于使用情况。实际上,这里显示的测试只获取PV值一次。如果你正在写一个获取1000个PVs,写它们到磁盘并且退出的脚本,则方法2(epics.caget_many())可能确切是你所想要的。但如果你的脚本将获取1000个PVs并且保持做着其它事情,或者即使它每分钟运行一个获取1000个PVs的循环并且写它们到磁盘,则方法1将实际更快。那是在一个循环中进行epics.caget_many():

 

# caget_many() 10 times
import epics
import time
pvnamelist = read_list_pvs()
for i in range(10):
    values = epics.caget_many(pvlist)
    time.sleep(0.01)

将比创建一次PV并且在循环中获取它们的值花费更长时间:

# caget_many() 10 times
import epics
import time
pvnamelist = read_list_pvs()
for i in range(10):
    values = epics.caget_many(pvlist)
    time.sleep(0.01)

在使用1000个PVs的测试中,用epics.caget_many()的循环花费大约1.5秒,而通过PV.get()的版本循环花费0.5天。

要清楚,连接到EPICS PVs是开销大的,不从连接的PVs获取数据。通过不保留连接或者在PVs上创建监视器,可以降低连接开销,但如果你要重复使用PVs,那节省将快速丢失。简言之,除非你检测了你的使用情况并且演示了epics.caget_many()对于你的需求更好,才使用方法1而不是epics.caget_many()。

time.sleep()或epics.poll()

 为了一个程序与EPICS设备通信,它需要一些时间用于通信发生。epics.ca.PREEMPTIVE_CALLBACK设置为True,将在独立于主Python线程的一个线程中处理这个通信。这意味着CA事件可以在任何时候出现,并且不需要调用epics.ca.pend_event()来显式地运行事件处理。

为了事件被处理,必须偶尔地从主Python线程释放一些时间。做这件事地最简单方式是用time.sleep(),使得一个事件循环可以就是:

>>> while True:
>>>     time.sleep(0.001)

不幸地,time.sleep()方法不是一个非常高精度的时钟,取决于系统具有1到10ms的代表性分辨率。因而,即使事件将是将是异步产生的并且带有抢占式回调的EPICS不需要运行epics.ca.pend_event()或epics.ca.poll(),用一个事件循环,可能达到更好的性能:

>>> while True:
>>>     epics.poll(evt=1.e-5, iot=0.1)

由于这种循环将比使用time.sleep()更多的运行。

使用Python线程

PyEPICS包的一个重要特性是它可以与Python线程一起使用,EPICS 3.14支持用于客户代码的线程。即使在最好情况中,使用线程会有些麻烦并且导致无法预计的行为,并且通道访问库对于使用Python线程增加了一点负责性。这部分讨论了对PyEpics使用线程的策略。

首先,要对通道访问使用线程,你必须设置epics.ca.PREEMPTIVE_CALLBACK=True。这是一个默认值,如果epics.ca.PREEMPTIVE_CALLBACK被设置成了False,线程将不工作。

其次,如果你正在使用PV对象并且没有大量使用epics.ca模块(即是,没有生成和传递chids),则以下基本对你隐藏了复杂性。如果你正在使用线程化代码,最好阅读这个来理解问题是什么。

通道访问上下文

通道访问库为它的线程模型使用了contexts的概念,用contexts保存线程集合以及通道和过程变量。对非线程化工作,一个进程将在做任何实际CA工作前使用单个被初始化的context(在epics.ca.initialize_libca()中进行)。在一个线程化程序中,每个新线程从一个必须被初始化或被替换的新的,未初始化的context开始。因而,每个与CA交互的新python线程必须用epics.ca.create_context()显式地创建它自己的context(并且接着,在线程结束时用epics.ca.destroy_context()销毁这个context)或者连接到一个已有的context。

一般推荐的方法是贯穿一整个进程使用单个CA上下文并且连接每个线程到创建的第一个上下文(可能从主线程)。这避免了很多潜在的陷阱(和崩溃),并且这个做非常简单。它是使用PV对象时的默认模式。

contexts的最显式使用是把epics.ca.create_context()放在每个函数调用的开始作为线程目标,以及在每个线程结束放置epics.ca.destroy_context()。这将使得在那个线程中的所有活动在它自己的上下文中被做。这有效,但意味着需要更多注意,并且因而不推荐。

连接到初始被创建的上下文的最好方式是在每个将被Thread.run()调用的函数中任何其它CA调用前调用epics.ca.use_initial_context()。相当地,你可以添加一个withIntialContext()装饰器给这个函数。创建一个PV对象将隐式地为你做这件事,只要它是你地在这个函数中第一个CA动作。每次你做一次PV.get()或PV.put()(或者一些其它方法),它将也检查初始的上下文正在被使用。

当然,这种方法需要CA已经被初始化。强烈推荐在主线程中做那件事。如果它出现在一个子线程中,那个线程必须对于所有CA工作都存在,因而要么进程的生命或者非常小心只进行某些CA调用的进程。如果你编写一个线程化程序,在其中第一个真实CA调用是在一个子线程内部,推荐你在主线程中初始化CA。

为了方便,在epics.ca模块中的CAThread是一个对标准threading.Thread的非常浅的包装,其就在你的线程化函数被运行前添加了epics.ca.use_initial_context()的调用。这使得你的目标函数不显式地设置上下文,但仍然确保在所有函数中使用初始的上下文。

如何使用CA和Threads

概况以上讨论的,要使用线程,你必须在PREEMPTIVE_CALLBACK模式中使用run。更进一步,推荐你使用单个上下文,并且在主程序线程中初始化CA,因而你的单个CA上下文属于主线程。唯一地使用PV对象使得这变得容易,但使用更底层接口完成它也是相当简单。对应使用线程地选项(大致按可靠性顺序)是:

  1. 对线程工作使用PV对象。这确保你在单个CA上下文中工作。
  2. 对将使用CA调用的线程使用CAThread而不是Thread。
  3. 在会是一个线程目标函数的所有函数顶部放置epics.ca.use_initial_context(),或者用withInitialContext()装饰器装饰,@withIntialContext
  4. 在位于一个新线程内部的所有函数顶部使用epics.ca.create_context(),并且确保在那个函数末尾放置epics.ca.destroy_context()
  5. 最好忽略建议和希望。如果你没有创建新PVs并且只在一个子线程内读取在主线程中创建的PVs的值,你可能见不到这个问题。

线程示例

这是使用Python线程的测试代码的简化版本。它基于原先来自Friedrich Schotte, NIH的代码,并且以thread_test.py被包含在源发行版的tests目录中。

在这个示例中,我们定义了一个run_test过程,它将从一个提供的列表创建PVs,并且监视这些PVs,在它们变化时打印出这些值。创建并且并发运行两个线程,用重叠的PV列表,虽然一个线程比别一个运行更短的时间。

import epics
import threading
import pvnames


def test_basic_thread():
    result = []
    def thread():
        epics.ca.use_initial_context()
        pv = epics.get_pv(pvnames.double_pv)
        result.append(pv.get())

    epics.ca.use_initial_context()
    t = threading.Thread(target=thread)
    t.start()
    t.join()

    assert len(result) and result[0] is not None


def test_basic_cathread():
    result = []
    def thread():
        pv = epics.get_pv(pvnames.double_pv)
        result.append(pv.get())

    epics.ca.use_initial_context()
    t = epics.ca.CAThread(target=thread)
    t.start()
    t.join()

    assert len(result) and result[0] is not None


def test_attach_context():
    result = []
    def thread():
        epics.ca.create_context()
        pv = epics.get_pv(pvnames.double_pv2)
        assert pv.wait_for_connection()
        result.append(pv.get())
        epics.ca.detach_context()

        epics.ca.attach_context(ctx)
        pv = epics.get_pv(pvnames.double_pv)
        assert pv.wait_for_connection()
        result.append(pv.get())

    epics.ca.use_initial_context()
    ctx = epics.ca.current_context()
    t = threading.Thread(target=thread)
    t.start()
    t.join()

    assert len(result) == 2 and result[0] is not None
    print(result)


def test_pv_from_main():
    result = []
    def thread():
        result.append(pv.get())

    epics.ca.use_initial_context()
    pv = epics.get_pv(pvnames.double_pv2)

    t = epics.ca.CAThread(target=thread)
    t.start()
    t.join()

    assert len(result) and result[0] is not None

经过以上长时间讨论,按顺序说几句化:此代码使用标准线程库并在在目标函数中任何CA调用前显式地调用epics.ca.use_initial_context()。也注意:从主线程首先调用run_test(),使得初始的CA上下文属于主线程。最终,在run_test()函数中epics.ca.use_initial_context()调用会被epics.ca.create_context()替换,并且正常运行。

来自这个的输出看起来:

First, create a PV in the main thread:
Run 2 Background Threads simultaneously:
-> thread "A" will run for 3.000 sec, monitoring ['Py:ao1', 'Py:ai1', 'Py:long1']
-> thread "B" will run for 6.000 sec, monitoring ['Py:ai1', 'Py:long1', 'Py:ao2']
   Py:ao1 = 8.3948 (A)
   Py:ai1 = 3.14 (B)
   Py:ai1 = 3.14 (A)
   Py:ao1 = 0.7404 (A)
   Py:ai1 = 4.07 (B)
   Py:ai1 = 4.07 (A)
   Py:long1 = 3 (B)
   Py:long1 = 3 (A)
   Py:ao1 = 13.0861 (A)
   Py:ai1 = 8.49 (B)
   Py:ai1 = 8.49 (A)
   Py:ao2 = 30 (B)
Completed Thread  A
   Py:ai1 = 9.42 (B)
   Py:ao2 = 30 (B)
   Py:long1 = 4 (B)
   Py:ai1 = 3.35 (B)
   Py:ao2 = 31 (B)
   Py:ai1 = 4.27 (B)
   Py:ao2 = 31 (B)
   Py:long1 = 5 (B)
   Py:ai1 = 8.20 (B)
   Py:ao2 = 31 (B)
Completed Thread  B
Done

注意:当线程A和B运行时,在每个线程中为PV:ai1产生一个回调。

注意:用以下显式地清除在每个线程中创建PVs的回调:

[p.clear_callbacks() for p in pvs]

没有这个,在这个线程结束后,对应线程A的回调将继续存在。

对PyEpics使用多进程

对Python线程的替代方案是使用多进程,用标准的Python multiprocssing模块。虽然使用多进程有超过线程的一些优势,对于与PyEpics一起使用它也有重要的隐含。基本问题是多进程需要完全分开,并且不共享全局状态。对于EPICS通道访问,这意味着像建立通信通道,回调和通道访问上下文的所有那些事情不能容易地在进程之间共享。

解决方法是使用CAProcess,它作用就像multiprocessing.Process,但直到如何分开在进程之间地上下文。这意味着你将必须为每个过程创建PV对象(即使它们指向相同的PV)。

class CAProcess(group=None, target=None, name=None, args=0, kwargs={})

multiprocessing.Process的子类,在它自己的进程中运行目标函数前,它清除全局通道访问上下文。

class CAPool(process=None, initializer=None, name=None, initargs=0, maxtaskperchild=None)

multiprocessing.Process的子类,创建CAProcess实例的池。

给出了成功使用多进程的简单示例:

from __future__ import print_function
import epics
import time
import multiprocessing as mp
import threading

import pvnames
PVN1 = pvnames.double_pv # 'Py:ao2'
PVN2 = pvnames.double_pv2 # 'Py:ao3'

def subprocess(*args):
    print('==subprocess==', args)
    mypvs = [epics.get_pv(pvname) for pvname in args]

    for i in range(10):
        time.sleep(0.750)
        out = [(p.pvname, p.get(as_string=True)) for p in mypvs]
        out = ', '.join(["%s=%s" % o for o in out])
        print('==sub (%d): %s' % (i, out))

def test_mpprocess():
    def monitor(pvname=None, char_value=None, **kwargs):
        print('--main:monitor %s=%s' % (pvname, char_value))

    print('--main:')
    pv1 = epics.get_pv(PVN1)
    print('--main:init %s=%s' % (PVN1, pv1.get()))
    pv1.add_callback(callback=monitor)

    try:
        proc1 = epics.CAProcess(target=subprocess,
                                args=(PVN1, PVN2))
        proc1.start()
        proc1.join()
    except KeyboardInterrupt:
        print('--main: killing subprocess')
        proc1.terminate()

    print('--main: subprocess complete')
    time.sleep(0.5)
    print('--main:final %s=%s' % (PVN1, pv1.get()))

这里,主进程和子进程都可以与相同PV交互,虽然它们需要在每个进程中创建一个独立的连接(在这里,使用PV)。

注意:不同的CAProcess实例通过mmultiprocessing.Queue能够通信。在本次编写时,在使用多进程管理器上没有进行测试。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值