事件背景
算法使用
c++
封装为sdk
,外层使用go
搭建服务,使用cgo
加载so
进行跨语言调用。在
sdk
实现过程中,使用proto
传输数据之后,需要转为CVMat
,然后才进行推理。追踪之后,发现在
proto
字节数组进行CVMat
的解码消耗巨大,大约占据核心推理的 1 3 \frac{1}{3} 31时间。导致封装服务
QPS
大约下降 25 % 25\% 25%。
基本限制
-
sdk
的方法,是串行的,不能并发调用 -
跨语言调用必定需要数据传输,数据转换必然存在
-
全套使用
c++
开发,得不偿失,方案废弃
# 解决思路
- 启动多个实例
每个实例仍然存在消耗,废弃
- 多线程解码
使用c++
进行多线程编程比较麻烦,不好管理,增加代码复杂度。
同时,外部的入口唯一,多次请求才可能进入多个数据,并发条件并不成立。
最终方案
在go
直接进行sdk
的并发调用,然后对sdk
中的核心推理部分进行加锁。
由于是并发调用,多个请求都会进行直接进入到
sdk
中进行操作,CVMat
编码部分都会进行操作。但是由于锁的限制,每次都只会存在一个请求进行核心推理操作。
这样一来,c++
部分只添加了三行代码
- 头文件
- 锁声明
- 上锁
至于go
的并发,就当是个笑话。
新的视角
一般来说,并发是这样的
共享的资源,或者说是有限(单一)的生产者,然后对应多个的消费者。
这种情况下,关键在于共享资源状态的一致性。每个消费者触发消费的时候,全体消费者对于资源的一致性认可。
一般的的卖票的例子就是这种情况,比较特殊的是上厕所,这种表现的是状态的长时间保持,也就是资源的独占。
但是更显著的特征是,方向性
。这是消费的过程。
同时,并发的根源性问题,引申出一个关键的补充信息----
效率同步
。为了提升效率,我们才使用的并发,如果任务当中存在快慢的差异,针对慢的部分,我们仍然会考虑并发。
使用更多的消费者去加速这一段慢的操作,去平衡两边的速率,去逼近最优的解。
这样,就出现了任务的
阶段化
的拆解。比如
netty
的reactor
模型,接收的group
不多,甚至唯一,但是处理线程是多个。最常见的就是服务集群了,高可用是一方面,但是对于处理较慢的部分,多个实例消费,也是原因之一。
如果说消费是太阳一样的放射的话,从相反的角度来看,就会是像漏斗一样的汇聚流程。
好比银行柜台填表,亦或是升学。下面的多个实例,进行层级的过滤,然后产出(进行)主要的成果(操作)。
更贴切一些,我们写数据库的时候,势必要保证数据的唯一性,也就是说,每次都是串行的。
但是服务层级上,我们或许都是并发操作。
前一种,是串行到并行,后一种,是并行到串行。
重中之重
两者之间的区别在哪里呢,或者说在于资质(资格)的评价。
- 串行到并行
卖票什么的我们先放过,使用面试来进行对比,进行两种差异化的并发的统一。
当你通过面试,或者说录用之后,或许让你做前端、后端、前台…
数据处理的时候也是如此,根据内容然后进行不同阶段的操作。
在此,同样的角色,可以对应的是多个(种)的途径。
- 并行到串行
同样的,入职之前,你必须通过多轮面试,多轮的资质校验。
HR
,技术面,经理面…
然后你才能最终成为一个新员工。
也就是说,两者的统一对立面在于,我们是想得到,还是想处理这么一个东西。
大部分情况下,或者说服务开发过程中,我们都是去处理这么一个东西,为了更好更方便的处理,才使用的并发。
但是如果是获取的话,我们需要的是使用并发来进行基础操作,然后串行的收获果实。
统一的例子
使用池塘打水来举例。
一家人都去提水,每个人都动员。
对于池塘
这个共享资源,每个人都在消费,这是并发。串行到并发。(当然,这个状态并不严谨,或者水井比较合适
)
但是,一家人,汲水之后,放在哪里呢,肯定是倒在家里的水缸里。
此时,就是并发多线程汇聚单线程的操作了,肯定是要一个个来的。
因此,对于目标(共享消费的对象,或者汇聚的结果
),我们必须保证串行,必须上锁。
而对于过程的不关注,我们就可以使用并发去进行快速的解决。
感想
这次的操作,就结果来说,的确是一个简单的并发案例,但是隐蔽的视角,让我感触良多。
主要的是,它的入口和操作唯一对应,也就是天然的似串行化
。
同时,我们更多的经验在于让一个任务并发处理,而不是让部分任务并发处理。
我们的任务经常都是经过拆解之后的独立任务,都是纯并发的处理方式。
同时,这是一种比较隐匿的锁的细粒度化,最外层的锁在于我们思维中的只能串行调用
。
但是真正的串行是推理,推理之前,我们并行无不可。
正是并发,跨语言调用,在真正的核心推理之前,由于串行而阻塞,但是资格审核都已通过CVMat
解码。