https://github.com/xieguigang/sciBASIC

应用程序管线模式就是我们将执行时间比较长,计算任务比较重量级的代码放到一个新的子进程之中执行。通过子进程进行任务执行的应用程序管线模式在各个操作系统上的大型应用程序中都会涉及到。

例如,下面的截图显示的就是微软的Visual Studio的MsBuild应用程序管线进程:

下面的截图显示的就是在Linux平台上的应用程序管线进程:

应用程序管线模式的优点

尽管采用应用程序管线模式开发一套系统会相比较于原来的采用线程的方式处理计算任务,会让代码复杂一些。但是总的来说采用应用程序管线模式得到的好处还是要多很多的,下面列举除了一些主要的优势之处:

  • 内存隔离:因为我们的计算任务代码是运行于一个新的应用程序进程之中,所以任务代码的内存是与主进程的代码内存相互隔离开的。当我们的计算任务执行结束后,所使用的内存会随着任务子程序的结束而被释放掉。所以采用本模式,你不需要过度担心处理数据计算任务的代码的内存泄漏的问题。
  • 更好的计算性能:因为我们的计算任务代码是运行于一个新的应用程序进程之中的,内存空间是很干净的。所以不会出现由于计算任务代码是运行于主进程的某一条线程之中,因为访问其他对象出现的内存加锁而导致性能问题。并且,由于线程计算会受限制于线程的调度优先度等级而有些时候会降低计算性能;采用应用程序管线模式,将计算代码转移到新的任务子进程中执行可以避免出现此类线程计算导致的性能问题。
  • 更加流畅的UI操作体验:前面我们提到,计算任务代码的内存是和主进程之间隔离的,所以这样子主进程和计算任务子进程之间都可以获得内存占用降低到最小的好处。内存占用低意味着用户在主进程上操作UI的流畅度会更好,这是因为:越多的内存占用意味着CPU进行数据查找的时间会变长,这会导致UI出现卡顿;其次,在Windows系统上,当应用程序内存占用率较高的时候,系统会自动将程序的一部分内存数据转移到虚拟内存中,这会导致用户通过UI操作主进程重新访问某些处于虚拟内存中的数据的时候产生卡顿或者感到迟缓(尽管你的UI操作代码已经采用了异步处理)。

应用程序管线模式开发

应用程序管线模式一般是通过应用程序之间的命令行数据交互来完成的。子进程向主进程的消息数据的发送可以直接通过标准输出设备,也就是System.Console模块来完成,例如:

Public Shared Sub SendMessage(message As String)
    Call Console.WriteLine($"[SET_MESSAGE] {message}")
End Sub

Public Shared Sub SendProgress(percentage As Double, message As String)
    Call Console.WriteLine($"[SET_PROGRESS] {percentage} {message}")
End Sub

采用上面的两个函数就可以向父进程返回诸如消息字符串或者任务百分比进度等信息了。

命令行交互

应用程序管线模式的任务计算的子进程一般为一个命令行程序,我一般是使用下面的一个进程调用帮助函数来作为本管线模式的核心调用函数来使用的:

Public Function ExecSub(app$, args$, onReadLine As Action(Of String), Optional in$ = "") As Integer
    Dim p As Process = CreatePipeline(app, args)
    Dim reader As StreamReader = p.StandardOutput

    If Not String.IsNullOrEmpty([in]) Then
        Dim writer As StreamWriter = p.StandardInput

        Call writer.WriteLine([in])
        Call writer.Flush()
    End If

    While Not reader.EndOfStream
        Call onReadLine(reader.ReadLine)
    End While

    Call p.WaitForExit()

    Return p.ExitCode
End Function

Private Function CreatePipeline(app As String, args As String) As Process
    Dim p As New Process
    p.StartInfo = New ProcessStartInfo
    p.StartInfo.FileName = app
    p.StartInfo.Arguments = args
    p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden
    p.StartInfo.RedirectStandardOutput = True
    p.StartInfo.RedirectStandardInput = True
    p.StartInfo.UseShellExecute = False
    p.StartInfo.CreateNoWindow = True
    p.Start()

    Return p
End Function

我们从上面示例的两个函数可以看出来,我们创建了子进程之后,将其标准输出进行了重定向。这样子经过标准输出重定向之后,我们就可以捕获计算任务子进程的Console输出了。在ExecSub函数之中,我们可以通过onReadLine参数把对子进程标准输出的交互函数传递进来,从而可以使我们能够将子进程的状态消息更新到主进程的UI界面上。

消息捕获

我们从前面的示例代码中可以看得到,在我设计的在计算任务子进程中向主进程传递消息的函数,字符串消息是按照一定的格式输出的。所以我们只要按照一定的格式解析捕获得到的标准输出字符串,就可以将其解释为对应的UI更新操作了:

Private Sub ProcessMessage(line As String)
    If line.StringEmpty Then
        Return
    End If

    If line.StartsWith("[SET_MESSAGE]") Then
        ' [SET_MESSAGE] message text
        RaiseEvent SetMessage(line.GetTagValue(" ", trim:=True).Value)
    ElseIf line.StartsWith("[SET_PROGRESS]") Then
        ' [SET_PROGRESS] percentage message text
        Dim data = line.GetTagValue(" ", trim:=True).Value.GetTagValue(" ", trim:=True)
        Dim percentage As Double = Val(data.Name)
        Dim message As String = data.Value

        RaiseEvent SetProgress(percentage, message)
    End If
End Sub

上面的消息处理函数的代码中,我们可以看到,程序触发了两个事件。如果我们将事件绑定到UI控件的Text属性值更新之中,就可以完成主进程的UI更新了。将上面的消息处理函数与前面所提到的命令行调用的ExecSub函数联用,我们只需要将ProcessMessage函数当作为参数值传递进去就好了,例如:

 Public Function Run() As Integer
     Dim code As Integer = PipelineProcess.ExecSub(app, arguments, AddressOf ProcessMessage)
     RaiseEvent Finish()
     Return code
End Function

上面的帮助类源代码,大家可以在Github上阅读RunSlavePipeline.vb这个源代码文件。

mzkit软件开发中的应用程序管线实例

因为在mzkit软件之中,处理的数据一般为质谱原始数据。对于诸如raw/cdf文件或者mzkit软件自己的mzpack这类二进制文件还好,处理的效率一般非常高;但是对于mzXML/mzML这类纯文本文件的数据而言,文本解析将会占用计算任务代码很大的一部分处理时间。所以在最近的mzkit软件开发的时候,我已经逐步将线程中的计算代码转移到了R#脚本程序包之中,通过应用程序管线模式进行调用,以此来提升mzkit程序的数据处理效率。

下面我就以最近编写好的一个在mzkit软件中处理质谱成像原始数据的计算任务,来向大家讲解我是如何在mzkit软件中进行应用程序管线模式的开发的。

计算任务代码

计算任务代码我是放在R#脚本端来完成的,这样子mzkit主程序就可通过调用Rscript的执行来构成应用程序管线模式了。计算代码我放在了TaskScript模块之中,所以我们可以通过一下的R#脚本代码进行调用:

imports "task" from "Mzkit_win32.Task";

对于具体的任务计算代码,我们可以忽略掉具体的业务相关代码,将注意力放在数据交互上,可以得到下面的R#执行函数代码:

<ExportAPI("cache.MSI")>
Public Sub CreateMSIIndex(imzML As String, cacheFile As String)
    RunSlavePipeline.SendProgress(0, "Initialize reader...")
    ' ...
    RunSlavePipeline.SendProgress(0, "Create workspace cache file, wait for a while...")

    For Each pixel As ScanData In allPixels
        Call cache.WritePixels(New ibdPixel(ibd, pixel))

        i += 1

        If ++j = d Then
            j = 0
            RunSlavePipeline.SendProgress(i / allPixels.Length * 100, $"Create workspace cache file, wait for a while... [{i}/{allPixels.Length}]")
        End If
    Next

    ' ...
    Call RunSlavePipeline.SendProgress(100, "build pixels index...")
    ' ...
    Call RunSlavePipeline.SendProgress(100, "Job done!")
End Sub

我们能看到,我们在上面的计算任务代码之中,通过了RunSlavePipeline.SendProgress函数向mzkit主进程返回了任务的执行进度信息。

调用上面的计算任务函数的完整R#脚本代码如下:

imports "task" from "Mzkit_win32.Task";

# title: Build indexed MSI cache
# author: xieguigang <xie.guigang@gcmodeller.org>

[@info "the file path of the imzML raw data file to create cache index."]
[@type "*.imzML"]
const imzML as string = ?"--imzML" || stop("no raw data file provided!");
[@info "the file path of the MSI indexed cache file."]
[@type "filepath"]
const cache as string = ?"--cache" || stop("a cache file path must be provided!");

sleep(1);
task::cache.MSI(imzML, cache);

我们通过使用R#命令,可以得到上面的脚本代码的命令行调用方法:

主进程中调用计算代码

在调用我们的计算代码,创建任务管线之前,我们首先会需要按照脚本的命令行格式拼接出命令行参数;随后创建进程任务就可以了,例如:

Dim cli As String = $"""{Rscript}"" --imzML ""{imzML}"" --cache ""{cachefile}"""
Dim pipeline As New RunSlavePipeline(RscriptPipelineTask.Rscript.Path, cli)

之后呢,我们就可以创建出对应的Windows窗体对象;绑定好消息事件,就可以将任务进度消息显示给我们的用户了,例如:

Dim progress As New frmTaskProgress

progress.ShowProgressTitle("Open imzML...", directAccess:=True)
progress.ShowProgressDetails("Loading MSI raw data file into viewer workspace...", directAccess:=True)
progress.SetProgressMode()

AddHandler pipeline.SetProgress, AddressOf progress.SetProgress
AddHandler pipeline.Finish, Sub() progress.Invoke(Sub() progress.Close())

Call New Thread(AddressOf pipeline.Run).Start()
Call progress.ShowDialog()

我们从上面的示例代码可以看到,任务的进度消息通过管线进程对象的SetProgress事件反馈显示到UI界面之上。下面是我们的应用程序管线模式代码运行的实际效果:

可以看得到,mzkit程序已经可以通过上面的代码,创建出了一个Rscript任务进程,正常的执行我们的质谱成像数据处理相关的计算任务代码了。

Latest posts by xie guigang (see all)

Attachments

No responses yet

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注