Verilog、Simulink、Labview、PLC等数据流编程语言,并没有明显的执行顺序所在。(组合逻辑、网表、跳转表对应的是数据驱动编程)
物理世界,是天然的多线程。代码世界,是天然的单线程。
节点/元件
在计算机中叫进程,在ROS中叫节点,在电路叫元件,在Verilog中叫模块,在人工智能叫智能体……
一个对象,就是一个元件。
而它有输入输出的端口,我们把它分为两类
管道负责数据——传入参数与返回值、变量的赋值与调用
导线负责控制——事件驱动、中断触发、扫描刷新、普朗克时间
数据传递各种数据类型。输入接口、输出接口。源头有固定数据源、用户输入
导线只传递布尔尖脉冲。启动信号、完成信号。源头有固定时钟源、用户触发
可以由分频器来产生分支源头,分频器分出特定条件下的时钟信号。
数据检测器,把管道数据生成为导线信号
计数执行器,把导线型号应用于管道数据
按周期的分频器
按逻辑的分频器
或:线或。
与:具有单稳态触发宽度的与。毕竟在严谨的认知下,信号达到绝对同时传入可以认为是不可能的。
按数据的分频器
条件判断
数据触发
各种概念在节点的体现
对象
类是元件的原型,是对元件的概念描述:“555定时器的设计规范如下”
对象是对元件的创建,是对元件的应用实现:“去给我买个XX牌的555定时器”
对于硬件语言,对象的创建和销毁,可以理解为元件的启用和关闭,触发端的接入与拔出
类的多个方法,对应模块的多个触发端
即使在循环或递归里产生了大量临时对象,物理上也是有大容量的内存为其提供支持。
变量与寄存器
在C语言中叫变量,在数字电路中叫触发器或寄存器……
它触发后只有两种操作:赋值、读取
赋值需要一个参数端口,读取不需要参数端口。返回值端口都是本身的值(对于可反射语言,本身的值可以看作本身;对于大多数静态语言,本身和值是不一样的)
组合逻辑
可以认为是频率比原始时钟源还要高的触发频率,让运算就像自然进行的一样。
考虑Verilog:assign语句不占用步骤,使用一次之后就不再重定义,一个信号只能有一个 assign 语句,多次赋值会导致冲突或意外行为。
assign c = a + b;
考虑C:define编译前就生效,不宜出现重定义
#define c (a+b)
组合逻辑是一种宏定义,一种元编程。这是在编译时期就融入的运算逻辑,不随外界触发而影响
(物理层面上其实也做不到,逻辑门有延迟、有竞争冒险)
时序逻辑
组合逻辑的函数结构非常简单,可以直接组合逻辑表示,但如果有for循环甚至while判定呢
def randbig():
a = 0
while a < 100:
a = rand()
return a
要有执行流程
要考虑“何时执行”(触发)
在一般的编程语言中,这是“提到函数的名字”的过程。在节点中实际上是导线信号送入触发。
func()和do_func = True的区别。
对计算机或者语言解释器来说,函数的结果是未来的事情都不重要;
但对于电路,节点是实实在在的,肯定会考虑未完成的状态;
不能立即给出答案;返回值存在默认值。这就是竞争与冒险存在的依据。
流程控制:goto、循环结构
goto语句很直白。把信号导线接驳到label传回就是了。
for和while的问题遂迎刃而解了。(为什么当时设计不出乘法器,估计还是因为,一,数码需要排线传递太复杂,二,加法器并没有我模型中必须要有的触发和选通)
函数递归
函数递归,和循环有本质区别,在于它内部创建了新的对象,无非就是启用了一个新元件罢了。
若函数内再递归调用自己,那临时对象又会增加(在计算机中也是,每调用一次自身,就会占用一块内存存储这个函数块),开销较大,不建议使用(实际上Verilog也不支持)
网上搜索了一下硬件递归,大家的理解停留在“循环/反馈”上,我认为循环/反馈并不算是递归,递归是在函数内原封不动地调用一个一模一样的函数。
{
1
{
2
{
3
{
4
}
}
}
}
和
{
{
2
{
3
{
4
d
}
c
}
b
}
a
}
是不一样的,而循环语句只能做到前者(在没有临时变量需要保存的情况下)。
而传统的循环,可以是在【函数的实例】里,再调用这个实例本身(内部导线又接驳到了输入触发,本质上是循环,只是与goto与for与while写法不同罢了。
资源共享之门控
在RoboCup3D的机器人学习中,一个behavior就把我所有的电机资源全占用了,往往不能腾出其他电机/传感器做其他判断和动作。对于资源的占用,我的程序应该“心里有数”。对于各种元件(进程、内存)占用的考虑都是如此。
(48 封私信 / 70 条消息) 如何理解互斥锁、条件锁、读写锁以及自旋锁? - 知乎
多线程的同步与互斥(互斥锁、条件变量、读写锁、自旋锁、信号量)-CSDN博客
(48 封私信 / 66 条消息) 一文搞懂六大进程通信机制原理(全网最详细) - 知乎
实际上,进程的同步与互斥本质上也是一种进程通信(这也就是待会我们会在进程通信机制中看见信号量和 PV 操作的原因了),只不过它传输的仅仅是信号量,通过修改信号量,使得进程之间建立联系,相互协调和协同工作,但是它缺乏传递数据的能力。
互斥锁
对于单个元件,“厕所有没有人”。抢不到就离开这个状态洗洗睡了。
while (抢锁(lock) == 没抢到) {
本线程先去睡了请在这把锁的状态发生改变时再唤醒(lock);
}
自旋锁
在没抢到锁之前阻塞,不停地轮询(在厕所门口等着)直到抢到为止,也就是不需要外部触发(中断)了。
while (抢锁(lock) == 没抢到) {
// 什么也不做();
}
互斥锁和自旋锁的特殊实现
读写锁
在“读取”方面进行放开。“一个坑只能有一个人拉,但是可以有很多人吃”
条件变量(门控信号)
对于一个元件,在ENable端(条件判断)为真的时候才执行。另一个元件有机会使条件成立(给出条件成立信号)(或者自旋锁不断轮询)。可以视为是互斥锁变量有了明确的判断逻辑。
/* lock = True; */ lock = true_or_false_question();
这类似于硬件中断(或者SysTick,或者MainLoop,或者自旋锁while(1)轮询)触发了回调函数,回调函数里有对相关条件的判断。
这也是我时序编程思想的起源。
信号量
信号量(类比停车场)用信号量解决停车场问题-CSDN博客
对于多个元件表示资源的可用量。“停车场的可用车位数”,“当前有几个人在拉屎几个人在排队”,可以理解为多个bool类型的lock合并在一起变成int等其他类型数据。
单线程
数据有数据的流向,程序执行有程序执行的顺序。
单线程的语言,导线顺序一般可以视为从上到下。
但是已经声明的对象往往重复出现,直接从上往下写出一个copy总是不太合适。这个时候,就不只是从上往下了
单线程的语言,导线没有分叉和合并。但是可以多次经过同一个元件。
再回到这段代码
int a = 3;
bool b = 4;
a = 5;
《对象的复用》提到导线穿过同一个元件,也就是有回环。但是之前a赋值3,现在a赋值5,如何体现回环后的不一样?
引入选通输出。在常量池里有1, 2, 3, 4, 5...等常量,但不是谁都能传入a的。第二次,导线穿过5而没穿过3,于是常量5传入a。
a.input(5.output())
管道的两端,选通输入触发和选通输出触发都被触发(选通),数据才正确流入。
于是可以认为所有端口之间都有潜在的联通关系,只是大部分关系自始至终都不会被触发。
多线程与异步编程概述
还是拿python举例吧
async def concurrent_gather():
results = await asyncio.gather(
async_task1(param1),
async_task2(param2),
async_task3(param2)
)
return results
在数据流语言中就简单很多了:
管道分叉,灌入对应的数据;
导线分叉,同时触发多个元件;
当然,设计时候要开始注意计算延迟,避免竞争冒险了。最简单的方法是用同步的时钟信号,就像sysTick一样统一调度任务。
假如数据流程序clock的频率是10Hz,那对应的顺序的程序的调度器的频率是100Hz,才能最多并行执行十个任务吧。
物理世界是并行计算的,但CPU是串行计算的,计算机语言并没有像硬件描述语言一样对触发的描述那么全面。下面是用单线程的CPU通过轮询调度,来模拟多线程的过程。
计划,赶不上变化。机器人程序大多数时候都不该是顺序执行的。所以需要多线程。
一文彻底搞懂多线程、高并发原理和知识点 - XiaoLin_Java - 博客园
“任务”其实就是线程
RT-Thread快速入门-线程管理(上) - 知乎
RTOS中的任务是线程、进程、还是协程?-CSDN博客
我们Action之前各种各样的程序,本质上都是轮询+中断+标志位判断+函数封装。
裸机调度
协程释放
一个协程,主动放弃自己,转而调用另一个协程。缺点是,在裸机中会有很难看的一大堆嵌套,同时还要特意避免while套while;优点是,省去了复杂的调度器(MainLoop)中的入口条件判断。
跳转
有点像单向链表。
把main里的while拆分到了Task里面的小while,跳转就是没有形成统一的循环。
void Task1()
{
while(1)
{
Action();
if(something) break;
}
Task2();
}
void main()
{
Task1();
}
由于C语言(及大多数编程语言的特性),这样写直接堵死了多线程的可能。
轮询响应,轮询执行
跳转+中断
void Task1()
{
while(1)
{
Action();
if(something) break;
}
Task2();
}
void IRQHandler1()
{
something = 1;
}
实时响应,轮询执行
中断内跳转
void Task1()
{
while(1)
{
Action();
}
}
void IRQHandler1()
{
Task2();
}
利用了中断的抢断特性来跳出任务。直接改变函数入口可以避免while套while,但同样难以实现多线程。不是什么东西都有中断。
实时响应,实时执行。
线程调度
我的MainLoop可以视为一个调度器,只是没有任何规则设计,各个任务排着队调用,不在乎时间与优先级。
轮询
有点像哈希表。把循环集中了起来(MainLoop)
标志位写法
void Task1()
{
Action();
if(something) { // 协助式标志位修改:在任务中判断
STATE = 2; // 任务的切换
STATE_3 = 1; // 任务的启用
}
}
void MainLoop()
{
if(STATE == 1) Task1();
if(STATE == 2) Task2();
if(STATE_3) Task3();
if(something) {
modify(&STATE); // 调度器中判断该执行哪个任务
}
}
函数指针写法
void Task1_1()
{
Action();
if(something) { // 协助式标志位修改:在任务中判断
NOW_TASK = &Task1_2; // 任务的切换
P_TASK = &Task3; // 任务的启用
}
}
void MainLoop()
{
NOW_TASK(); // 一个函数指针
// 函数指针写法不用写那么多
P_TASK(); // 一个函数指针
if(something) {
modify(&STATE); // 调度器中判断该执行哪个任务
}
}
我们只要把任务拆分得无穷小,就几乎随时可以跳出任务,就可以媲美实时系统了……但是每个轮询周期前会有复杂的判断系统或者重定向系统。浪费系统资源。
轮询响应,轮询执行。
轮询+中断
void IRQHandler1()
{
TaskIRQ();
modify(&STATE);
}
无非就是另一个main入口。中断要短,主场是留给轮询系统的,否则中断里的局部任务会阻碍剩下在轮询系统中的全局任务的进行
实时响应,轮询执行。
中断内轮询
void IRQHandler1()
{
TaskIRQ();
modify(&STATE);
while(1) MainLoop();
}
无非就是另一个main入口,那么就像main一样定向到MainLoop中。旧的MainLoop还没执行完就进入新的MainLoop了。
相比跳转+中断的写法,把轮询系统统一进了MainLoop而不是分开在Task中。
实时响应,实时执行。
任务封装
一个函数作为一个任务,存在缺陷,因为只能包含下面的其中之一:
- 任务启动一次
- 任务执行循环
- 任务退出一次
一个函数作为任务的一个阶段,则阶段之间变量的共享(作用域)存在问题。每个周期之间的变量也不互通。不愿意用全局变量
面向过程解决方案:
传统的跳转写法,循环写在Task里面。无法多线程,否则用多线程库,暂存函数,直接跳到函数中间某个部分(线程又是对象的范畴了)。
void Task1()
{
Initialize();
while(1)
{
Action();
if(something)
{
Quit();
break;
}
}
Task2(); // 外层有轮询MainLoop则可以是 STATE = 2;
}
面向对象解决方案:
实际上,任务应该是一个对象。构造方法,成员方法,析构方法。
或者像python的上下文管理器一样。什么是Python中的上下文管理器(context manager)?如何使用上下文管理器?-腾讯云开发者社区-腾讯云
class Task1_type()
{
// 多线程保证了暂停之后还有事干,面向对象保证了可以暂存变量值,于是可以有暂停操作
bool pause = 0;
Task1_type() {
Initialize();
}
void run() {
if(pause) return; // 暂停
Action();
if(something){
NOW_TASK = Task2_type();
~Task1_type();
}
}
~Task1_type() {
Quit();
}
};
void MainLoop()
{
NOW_TASK.run();
}
RTOS任务实时调度
FreeRTOS的任务详解_freertos 任务分析-CSDN博客
RTOS 中的任务调度与三种任务模型_rtos任务调度-CSDN博客
任务调度器,其实就是MainLoop进阶版。
RTOS的重要本领就是暂存和恢复任务的能力,这样任务可以自由切换。这个能力来自任务堆栈。CPU寄存器值等保存在此任务的任务堆栈中
- 比我的类变量高明,把什么东西都存进去了;
- 直接对寄存器底层操作,所以我函数可以大胆写while,想怎么写就怎么写;当然还是需要结构体登记任务的信息的,只是不需要我一大堆变量存在对象里面了。
抢占式调度
轮询系统中的任务也不再是排着队来了,而是有优先级的执行。
也就是更重要的任务到来时,调度器能够把原有任务暂停转而执行重要任务。

任务调度器(MainLoop)若在中断中触发,就是实时的。不是什么东西都有中断。
实时响应,实时执行。
分时调度
当两个任务的优先级一样时,若它们的优先级最高,则它们执行时间片调度。
任务调度器(MainLoop)在SysTick中触发,分时调度均匀分配了Task1和Task2的执行时间,轮流执行。
轮询响应,轮询执行。
抢占式调度其实也经过了SysTick同步。(这其实就是数字电路的时钟信号触发)
协助式调度
只要一个任务不主动 yield 交出 CPU 使用权,它就会一直执行下去。类似协程的思想,但是还是由调度器裁定执行权。
GPOS进程非实时调度
进程,无非就是资源隔离的线程,变量不共享。(浏览网页突然浏览器奔溃了这不会影响到我的音乐播放器)
【原创】为什么Linux不是实时操作系统 - 沐多 - 博客园
为何Linux 不原生支持硬实时性?-CSDN博客
一个最生动的例子吧,你在Konsole里面输入了sudo apt install krita,此时另一个软件kdenlive还在安装,于是消息显示:
无法获得锁 /var/lib/dpkg/lock - open (11 Resource temporarily unavailable)
也就是说,Linux在设计规则时,就不像是RTOS一样,任务想抢就能抢到的(自旋锁、完全公平调度器……)
ASYNC协程:简易的异步编程
异步Rust 操作系统 Embassy 与 FreeRTOS 性能对比_ITPUB博客
多线程 - Python3异步编程详解:从原理到实践 - vistart的个人空间 - SegmentFault 思否
(47 封私信) 一文读懂Python async/await异步编程 - 知乎
事件循环机制(Event Loop)的基本认知一、什么是事件循环机制? 为什么是上面的顺序呢? 原因是JS引擎指向代码是 - 掘金
05. 事件循环与非阻塞I/O | CppGuide社区
谈谈事件循环 / 轮询(Event-Loop) | Hi! Tmiracle
虽然轮询可以实现并发,但我还是觉得先有并行,后有串行。Verilog和硬件设计证明此,物理世界的规律证明此。
事件循环


代码从上往下执行;
先执行序号1;
再执行setTimeout,eventLoop登场;
再执行序号3;
eventLoop还在不断循环的访问callbackqueue;
2s之后Web API会将要执行的打印序号2这句话放入callbackqueue,
eventLoop将callbackQueue中的内容放入调用栈,开始执行序号2。
这样看来,“事件循环”其实就是在单线程里面,在应用层面实现的一个小小的轮询系统。
核心概念
以python为例。
协程(Coroutine):async def定义的函数。我估计是由于没有像RTOS一样对任务的强控制性,所以叫协程而非线程。
任务(Task):任务是对协程的封装,使其可以并发执行
async声明了函数为异步进行的,在async函数内对async函数使用await表示用普通方法调用这个函数,也就是像普通函数一样等待执行完成后再进行下一步动作。
示例:
# 定义异步函数
async def async_function():
# await只能在async函数中使用
result = await some_async_operation()
return result
# 运行异步函数
asyncio.run(async_function())
并发执行的三种方法:
# 1. 使用gather并发执行多个协程
async def concurrent_gather():
results = await asyncio.gather(
async_task1(),
async_task2(),
async_task3()
)
return results
# 2. 使用TaskGroup (Python 3.11+)
async def concurrent_taskgroup():
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(async_task1())
task2 = tg.create_task(async_task2())
# 退出时自动等待所有任务
# 3. 使用create_task立即调度
async def concurrent_create_task():
task1 = asyncio.create_task(async_task1())
task2 = asyncio.create_task(async_task2())
result1 = await task1
result2 = await task2
