电子说
我们提到过使用局部变量、全局变量时,必须注意处理好并发访问。
举个例子:有A、B、C三个线程在并发执行。A线程修改了变量V的值,期望线程C能够读取到最新的值。B线程却在C线程读取变量V的值之前修改了V的值。这种情况我们说变量V被污染了、数据脏了。 要处理好变量的并发访问、多线程对变量的访问,我们需要使用同步机制。
在多线程软件设计中,不仅仅对变量的访问,对任何竞争性资源的使用/访问都必须使用合理的同步机制进行管理。比如,有两个线程都需要使用某个模拟输出通道,但是模拟输出通道只有1个、同一时刻只能由1个线程使用,这种情况下我们把这个模拟输出通道称作竞争性资源。
如果不对这个模拟输出通道的访问进行合理的管理,可能导致输出错误的模拟量、线程死锁、软件假死等问题。
LabVIEW可用的同步机制
在Programming->Synchronization分类下,我们可以看到LabVIEW里可用的同步机制。
名称 | 作用 | 相关VI |
通知器操作 Notifier Operations |
挂起某个线程直到收到某个通知 | Wait on Notification、Send Notification等 |
队列操作 Queue Operations |
使用队列在线程内或线程间传递数据 | Enqueue Element、Dequeue Element等 |
信号量 Semphore |
通过信号量限制对竞争性资源的访问 | Acquire Semphore、Release Semphore等 |
集合点 Rendezvous |
通过集合点同步多个线程。每个到达集合点的任务将等待,直到集合点处等待的任务达到指定的数量后,所有任务才继续执行。 | Wait at Rendezvous、Resize Rendezvous等 |
事件发生 Occurrence |
通过事件发生控制和同步线程内或线程间的活动。 | Generate Occurrence、Wait on Occurrence等 |
首次调用 First Call? |
判断某段代码或某个子VI是否首次执行 | / |
同步数据流 Synchronize Data Flow |
同步数据流,可使用多个线程的数据传送在该VI处得到同步,以确保数据传送顺序。 | / |
同步机制的应用
下面我们用具体的例子来详细讲解一下不同的同步机制。
1)Notifer Operations
上面这个示例,Loop1和Loop2是两个并行的线程。这是实现多线程的最基本的方法。Loop1负责产生数据和停止这两个线程的运行;Loop2负责读取Loop1产生的数据,并在需要的时候及时终止线程(退出循环)。它是怎么实现的呢?Loop2等待数据通知器发出通知、等待终止通知器发出通知,收到通知后把数据读出来或者退出循环。Loop1则是把数据或者退出控件值通过发送通知传递给Loop2。
第一步:Obtain Notifer,获取通知器,如果不存在则创建。创建时按照初始值进行初始化。
第二步:Send Notification,发送通知。把数据或者控件值通过发送通知发送给等待对应通知器的线程。
第三步:Wait on Notification,等待通知。Loop2里调用Wait on Notification挂起线程,直到收到Loop1发出的通知后继续执行。
第四步:Release Notifer,释放通知器。释放资源,避免内存泄露。 这个示例里Loop1加了100ms的等待,可以确保通过Send Notification发送出去的数据可以被Loop2获取到。
如果没有这个等待100ms,Loop1发送的数据是可能丢失的,通知器不会缓冲已经发送的消息,新的消息会覆盖旧的消息。如果需要缓冲消息(连带数据),可以使用队列。
关于通知器,我们再看一个VI:Wait on Notification from Multiple,它方便我们等待多个通知,实现多对一的同步。
2)Queue Operations
下面这个代码,实现的功能和上面Notifier Operations里的例子是一样的。不同的是这里用的是队列(Queue)。队列的特点是先进先出(FIFO)、缓存数据。
前面提到过,如果Loop1没有100ms等待,使用Notifer Operations是可能会丢失数据的,但是队列这里是不会的。哪怕我们去除Loop1里的100ms等待、在Loop2里加上100ms等待,让数据产生的速度大于数据被读取的速度,也是不会导致数据丢失的,来不及被读取的数据都会被存储在队列里。
队列不能实现类似Wait on Notification from Multiple的功能。
3)Semaphore
Semaphore,信号量,是一个常见且重要的概念。可以简单理解为类似红绿灯(信号灯)的功能。四个方向的车都要过十字路口,十字路口又不能同时过的,它是一个竞争性资源,不同方向的车(线程)对它的使用存在竞争。实际生活中是怎么办的呢?谁获得了绿灯(信号量)谁就可以通过。 多个线程访问竞争性资源前,先尝试获取信号量,能够获得信号量的线程有权使用竞争性资源,使用完竞争性资源后释放信号量(单方向绿灯不能一直亮着啊)。
此外,对关键代码段(Critical Section)也需要使用信号量进行保护。关键代码段是指涉及变量或竞争性资源访问的代码,采用信号量进行管理,以避免多个线程同时修改变量或试图同时访问竞争性资源。
4)Rendezvous
Rendezvous,集合点,比较好理解,大家都到集合点后再出发。
例如下面这个代码:
第一步,创建集合点。集合点大小为2,表示需要等待两个任务到达集合点代码才能往下执行。
第二步、第三步,在集合点等待,Wait at Rendezvous。当Loop1和Loop2的数据流都到达集合点后,Loop1和Loop2才能够往下继续执行。
第四步,销毁集合点,释放内存。
5)Occurrences
事件发生(Occurrences),用于在不同代码位置或不同线程之间同步活动。事件发生不需要轮询。 例如以下代码: 第一步,产生一个事件发生(Occurrence)。 第二步,Loop2等待第一步产生的Occurrence。这个时候Loop2线程被挂起,只有等待的事件发生(Occurrence)被Set后这个线程才会继续执行。 第三步,Loop1在完成其它可能的工作后,Set第一步中产生的事件发生(Occurrence),以使得所有在等待该Occurrence的线程可以继续运行。
6)First Call?、Synchronize Data Flow
First Call?检查某段代码或者某个子VI是否是第一次运行。 Synchronize Data Flow用于同步数据访问,它有四个输入端,并有四个输出端与输入端一一对应。只有四个输入端的数据都达到该VI节点了,代码才会从该节点继续往下执行。 例如下面代码,当First Call?检查到该VI是第一次运行时,先等待四个模拟信号数据(可以是实际项目中的文件读取、数据采集、系统初始化等工作)都到齐后,再往下执行代码。
所以下面这个测试代码(ONCE就是上面的VI)里,只会有一个Graph控件会有波形数据显示。另一个Graph控件的ONCE子VI被判断为非首次执行,没有波形数据产生。
使用好这些同步机制可以让我们设计出可靠的应用软件。涉及代码内并发访问、多线程、竞争性资源使用的,必须采用一定的同步机制,否则软件一定会有出错的时候——可能暂时没发现,但是隐患一直在。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !