Taskbus - 基于Qt的跨平台多进程合作框架(一)基本原理


上一篇文章中,我探讨了一种通过进程交换的合作框架,随后突如其来的一场重病拖慢了实现过程。尽管如此,经过同学们半年多的讨论、实验,目前实现了这个想法。感谢在我病休近6个月期间,徐老师、杜同学的坚持,其中的UI基础性编码工作由他们完成。在本篇,我首先介绍基本原理,后续将详细讲解开发过程。由于身体仍旧没有完全恢复,博客、论坛的回答可能滞后,见谅!
本文的GitHub链接为 这里
一个 发布包见这里
演示视频 在这里

1. 什么是Taskbus

Taskbus 是一种面向非专业开发者的跨平台多进程合作框架,具有进程切割、语言无关、编译器无关、架构无关四个特点。

非专业开发者是一个泛泛的概念,可以理解为没有受过专业化的软件工程化训练的开发者。诸如需要频繁自行开发小工具进行算法验证的高校教研团队,以及深入某一领域(化工、机械、通信、电子等)进行数据分析,需要长期从事非消费类工具软件开发的工程师团队。

Taskbus 从感官上提供一种类似Simulink或GNU-Radio的模块化拖拽界面,可用于在通用计算机上实现准实时的处理逻辑。但是,从结构上,其与二者完全不同。Taskbus 对编译器、运行平台、开发语言不做要求。它通过定义一种功能发布与数据交换标准,提供一套进程管理平台,以便把不同语言开发的进程组合起来。在例子中,您可以看到Python2/3,NodeJS,C#,Matlab、Qt、C++、MFC等工具链生成的模块,它们在平台统一调度下完成功能。

MainUI

2. 关键特性

taskBus 的核心理念是 “定IO标准不定具体工具,定连接结构不定架构算法”

  1. taskBus 仅定义数据交换的方法与格式,对具体实现语言、运行环境没有要求。这带来了非常高的灵活性。
  2. taskBus 仅定义进程间的逻辑连接结构,并不定义用户搭建的具体功能所采用的架构、所需要的方法。
    它的关键特性:
  • 简单: 进程通过标准输入输出(stdin,stdout,stderr)吞吐数据、命令行参数接受初始化参数。没有对数据库、COM、CORBA、动态链接、SOAP或网络协议的知识要求。基于平台提供的调试功能,经过录制、回放操作,可以脱离平台独立调试各个模块的逻辑。
  • 灵活: 通过专题(Subject)、通路(Path),通过标准输入输出管道可以分时处理多个逻辑流。它们之间的关系完全由模块设计者确定。基于网络模块、数据库模块提供的功能,可以在不同的操作系统上构建分布式的处理系统。您甚至可以用封装器把Matlab、Octave、Python程序包装进来。
  • 稳定: 错误被控制在一个模块进程内部,易于发现问题。 可为各个模块设置独立的优先级(nice)。
  • 高效: 多进程并行、分布式计算与优先级控制,使得通用PC计算环境也可达到实时处理/准实时处理的能力。当您的模块加入GPU加速等特性后,可以使整个系统性能得到大幅度提升。
  • 推送而非请求:与GNU-Radio不同,taskBus的前序模块主动向后端推送数据,而负荷控制由模块通过简单的方法实现(参考下文“负荷控制”部分)。
    在这里插入图片描述

3. 基本原理

很多经典的编程语言课本都是从控制台开始的. 控制台程序通过键盘接收用户输入,向屏幕打印运算结果。事实上,无论在Linux还是Windows中,进程启动时便有三个特殊的文件句柄可用了。他们是标准输出(stdout),标准输入(stdin),标准错误(stderr)。默认情况下,stdin与键盘关联,stdout、stderr与屏幕关联。

大多数现代语言支持创建子进程, 并且可以通过 “管道重定向” 技术接管子进程的标准管道。Taskbus 技术即基于此特性,从各个子进程的stdout读取数据,并转发给需要的子进程(stdin)。

3.1 输入输出

一个实现XOR操作的教科书C程序,一般类似这样:

#include <stdio.h>
int main(int argc, char *argv[])
{
	unsigned int a[4];
	scanf("%u,%u,%u,%u",a,a+1,a+2,a+3);
	for(int i = 0; i < 4; ++i)
		a[i] = 0xFFFFFFFF ^ a[i];
	printf("%u,%u,%u,%u\n",a[0],a[1],a[2],a[3]);
	return 0;
}

上面的程序里,从键盘输入四个数字,取反后输出。由于键盘与stdin关联,屏幕与stdout关联,上述程序实质上与下面的程序等效:

	fscanf(stdin,"%u,%u,%u,%u",a,a+1,a+2,a+3);
	fprintf(stdout,"%u,%u,%u,%u\n",a[0],a[1],a[2],a[3]);

Taskbus 模块的输入输出与上述程序非常类似,唯一的区别是使用了二进制读写函数:

	unsigned int a[4];
	fread(a,sizeof(int),4,stdin);
	for(int i = 0; i < 4; ++i)
		a[i] = 0xFFFFFFFF ^ a[i];
	fwrite(a,sizeof(int),4,stdout);

3.2 专题与通路

一个子进程只有一对输入输出管道。通过设置专题与通路,可以仅用一路管道分享多路内容。

3.2.1 专题

专题(Subject)指示一类数据。如声卡采集的波形,以及从文件中读取的字节流。
在图形界面上,专题显示为管脚。每个专题有一个便于记忆的名字;在运行时,taskBus平台会根据连线关系,为每个专题设置一个整数ID。ID相同的输入、输出管脚会被连接起来。
在这里插入图片描述
一个专题的生产者生成数据,交给平台。平台把数据交给所有连接了此专题的消费者。

注意: 不同的管脚可以生产同一ID的专题,也可以监听一个相同的专题。对生产者,这样做的行为是一致的。对消费者,如何处理专题ID相同的情况,取决于模块实现者。

3.2.2 通路

通路(PATH)在一类专题中,区分一条独立的自然时序。下面的例子中,两路声卡采集的数据汇入同一个FFT变换器。对于变换器来说,需要区分出两条自然时序,才能不导致混淆。
在这里插入图片描述
上图中的声卡模块采用自身的进程ID(2、6)作为通路号,这样非常便捷地标定了数据的来源。

3.3 带有专题和通路的IO

鉴于上面的介绍,我们在前文代码中稍加修改,即可实现基于stdio的通信。

void record( char a[], int data_len, int path_id)
{
	int out_subject_id, out_path_id;
	int out_data_len;
	char b[MAX_LEN];
	deal(a, datalen,path_id,
		 &out_subject_id,
		 &out_path_id,
		 &out_data_len,
		 b
		 );
	fwrite (&out_subject_id,sizeof(int),1,stdin);
	fwrite (&out_path_id,sizeof(int),1,stdin);
	fwrite (&out_data_len,sizeof(int),1,stdin);
	fwrite (b,sizeof(char),out_data_len,stdin);
}
int main(int argc, char *argv[])
{
	int subject_id, path_id;
	int data_len;
	char a[MAX_LEN];
	while (!finished())
	{
		fread (&subject_id,sizeof(int),1,stdin);
		fread (&path_id,sizeof(int),1,stdin);
		fread (&data_len,sizeof(int),1,stdin);
		fread (a,sizeof(char),data_len,stdin);
		switch (subject_id)
		{
		case ID_WAV:
			record(a,data_len,path_id);
			break;
		case ID_DAT:
			deal(a,data_len,path_id);
			break;
		default:
			break;
		}
	}
	return 0;
}

上述代码缺少上下文,但清晰的示意了taskBus最基本的通信原理。不同模块之间,正是通过这样的方法进行沟通的。

3.4 模块功能发布

由开发者独立开发的模块,需要使用JSON文件发布自己的功能。这样,平台就知道模块支持的专题类型、参数选项。
一个典型的功能描述文件必须包括三部分,分别是:

  1. 参数表
  2. 输入专题表
  3. 输出专题表
    其他用户自定义部分依旧可以读入并显示在平台,但没有实际的意义。 平台提供的helloworld例子包含两个功能,一个是比特抑或,一个是顺序取反。JSON文件的结构如下:
{
    "example_bitxor":{
        "name":"bitxor",
        "parameters":{
             "mask":{
                "type":"unsigned char",
                "tooltip":"bytemask",
                "default":255,
                "range":{
                    "min":0,
                    "max":255
                }
            }
        },
        "input_subjects":
        {
            "data_in":{
                "type":"byte",
                "tooltip":"input"
            }
        },
        "output_subjects":{
            "data_out":{
                "type":"byte",
                "tooltip":"output"
            }
        },
        "info":{
            "auther":"kelly",
            "version":[1,0,0],
            "mail":"[email protected]"
        }
    },
    "example_reverse":{
        "name":"reverse",
        "parameters":{
         },
        "input_subjects":
        {
            "data_in":{
                "type":"byte",
                "tooltip":"input"
            }
        },
        "output_subjects":{
            "data_out":{
                "type":"byte",
                "tooltip":"output"
            }
        },
        "info":{
            "auther":"kelly",
            "version":[1,1,0],
            "mail":"[email protected]"
        }
    }
}

可以看到,文件由两大块组成。第一块为 example_bitxor部分,第二块为example_reverse,对应了两个功能。

在各个功能内部,又分name、parameters、input_subject、output_subject四个子项目,分别对应友好名称、静态属性、输入专题、输出专题。

**注意:**尽管各个属性含有“type”类型指示以及range取值范围指示,但平台把所有输入输出看做字节流,这些取值仅仅为了提醒用户。一个成熟的模块应该有详细的接口文档描述输入输出类型、字节序、大小端。对文本类型,要明确或者可以设施字符集。上面的模块,在平台上显示为:
ExampleModules

3.5 命令行参数

taskBus 平台根据JSON文件启动进程。在启动各个功能模块时,taskBus 通过命令行参数送入所有的信息。命令行参数有如下几类。

类别 参数 意义 解释
进程 ----instance= 向模块送入进程ID值 整形。用于区分独立的进程,这个措施避免了模块自己来生成唯一ID
进程 ----function= 向模块指定当前实例开启的功能。 一个模块可以支持很多功能。
进程 ----information 平台请求模块输出JSON描述并退出。 模块既可以附带JSON文件,也可以在本参数中printf JSON。
专题 ----<sub_name>= 向模块指定专题名对应的ID 专题名由各个模块确定,可以出现多条。
用户属性 ----= 用户自定的初始化属性 可在平台“属性”栏设置。

以上述模块JSON为例子,下图中的模块,在启动时,命令行如下:

user@local$ example_helloworld.exe --instance=6 --function=example_bitxor --mask=255 --data_in=6 --data_out=1

Comandline args

3.6 第一个Hello-world 模块的代码

我们把HelloWold模块的代码粘贴到这里,使用C++,可以非常方便的实现上述功能。

  • 该代码没有使用任何标准C++之外的特性
  • 事实上,在第四章您可以看到,我们已经为接入平台提前做好了不少简化工作。但简化工作会掩盖细节,下面的代码仍旧是最体现模块运行原理的好例子。
#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib>
#ifdef WINNT
#include <io.h>
#include <fcntl.h>
#endif
using namespace std;
#define MAX_DTALEN 65536
int main(int argc, char * argv[])
{
	//In windows, stdio must be set to BINARY mode, to
	//prevent linebreak \\n\\r replace.
#ifdef WINNT
	setmode(fileno(stdout),O_BINARY);
	setmode(fileno(stdin),O_BINARY);
#endif
	bool bInfo = false, finished = false;
	int instance = 0;
	int sub_input = 0, sub_output = 0;
	char mask = 0;
	string function;
	//1. parse cmdline
	for (int i=1;i<argc;++i)
	{
		string arg_key = argv[i], arg_value = argv[i];
		int idx = arg_key.find('=');
		if (idx>=0 && idx<arg_key.size())
		{
			arg_key = arg_key.substr(0,idx);
			arg_value = arg_value.substr(idx+1);
		}
		if (arg_key=="--function")
			function = arg_value;
		else if (arg_key=="--information")
			bInfo = true;
		else if (arg_key=="--instance")
			instance = atoi(arg_value.c_str());
		else if (arg_key=="--data_in")
			sub_input = atoi(arg_value.c_str());
		else if (arg_key=="--data_out")
			sub_output = atoi(arg_value.c_str());
		else if (arg_key=="--mask")
			mask = atoi(arg_value.c_str());
		fprintf(stderr,"%s:%s\n",arg_key.c_str(),arg_value.c_str());
		fflush(stderr);
	}
	//2. function case
	if (bInfo)
	{
		//In this example, json file will be published with exe file.
		//We will return directly.  Or, you can output json here to stdout,
		//If you do not want to publish your json file.
		return 0;
	}
	else if (instance<=0 || function.length()==0)
		return -1;
	else
	{
		char header[4], data[MAX_DTALEN+1];
		memset(data,0,MAX_DTALEN+1);
		int n_sub = 0, n_path = 0, n_len = 0;
		while(false==finished)
		{
			fread(header,1,4,stdin);	//2.1 read header
			if (header[0]!=0x3C || header[1]!=0x5A || header[2]!=0x7E || header[3]!=0x69)
			{
				fprintf(stderr,"Bad header\n");
				break;
			}
			fread(&n_sub,sizeof(int),1,stdin);
			fread(&n_path,sizeof(int),1,stdin);
			fread(&n_len,sizeof(int),1,stdin);
			if (n_len<0 || n_len >MAX_DTALEN)
			{
				fprintf(stderr,"Bad length %d\n",n_len);
				break;
			}
			fread(data,sizeof(char),n_len,stdin);

			if (n_sub<=0)
			{
				if (strstr(data, "quit")!=nullptr)
				{
					finished = true;
					continue;
				}
			}
			else if (n_sub != sub_input)
				continue;

			if (function=="example_bitxor")
			{
				for (int i=0;i<n_len;++i)
					data[i] ^= mask;
			}
			else if (function=="example_reverse")
			{
				for (int i=0;i<n_len/2;++i)
				{
					char t = data[i];
					data[i] = data[n_len-1-i];
					data[n_len-1-i] = t;
				}
			}
			else
			{
				fprintf(stderr,"Unknown function %s\n",function.c_str());
				break;
			}
			fwrite(header,1,4,stdout);
			fwrite(&sub_output,sizeof(int),1,stdout);
			fwrite(&n_path,sizeof(int),1,stdout);
			fwrite(&n_len,sizeof(int),1,stdout);
			fwrite(data,sizeof(char),n_len,stdout);
			fflush(stdout);
		}
	}
	//3.exit
	return 0;
}

下一篇文章,我们通过一个示例来叙述客户端模块的开发过程。

发布了127 篇原创文章 · 获赞 330 · 访问量 48万+

猜你喜欢

转载自blog.csdn.net/goldenhawking/article/details/84191631
今日推荐