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寄存器值等保存在此任务的任务堆栈中

抢占式调度

轮询系统中的任务也不再是排着队来了,而是有优先级的执行。
也就是更重要的任务到来时,调度器能够把原有任务暂停转而执行重要任务。

任务调度器(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