65.9K
CodeProject 正在变化。 阅读更多。
Home

带管道并发内核的自动化系统

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.46/5 (4投票s)

2007年1月15日

CPOL

17分钟阅读

viewsIcon

26050

downloadIcon

264

提供了一个多线程解决方案,作为自动化调酒机并发流水线内核。该应用程序引擎可以为系统流水线处理三种或四种不同类型的鸡尾酒配方。

引言

本节中的大多数文章都侧重于改进同步对象上的线程同步技术,或改进线程管理。当试图解决涉及多个共享对象和顺序操作的自动化问题时,您可能会发现这些技术在构建此类系统方面帮助不大。

在本文中,描述了一个简单的并发系统,并基于并发软件设计平台实现了一个流水线并发解决方案。该设计具有以下优点:

  1. 软件架构清晰地描述了流水线任务,只需调整任务之间的同步资源分配策略,即可轻松修改内核并发。
  2. 通过使用软件设计平台SDK,无需使用(几乎不使用)操作系统同步对象或线程,即可实现期望的系统流水线并发。这个简单的解决方案使得那些对编写多线程自动化软件不太感兴趣或没时间的人能够快速实现并发应用程序框架,并将精力集中在系统中的其他重要问题上。

问题描述

设计了一台自动鸡尾酒机,通过按下机器前面板上的菜单按钮即可自动制作鸡尾酒。

目标是设计一个应用程序,在客户同时发出多个请求时,最大限度地提高系统并发性。使用GUI模拟机器前面板,以便客户可以通过菜单点播鸡尾酒并领取成品饮品。

机器可以处理四种鸡尾酒配方:

  1. 迈泰。
  2. 蝎子碗。
  3. 僵尸。
  4. 马提尼。

机器由以下五个主要硬件组件组成:

图 1
  1. 组件R。它是一个带有手臂的机器人,负责在P1、P2、P3和P4之间抓取杯子。
  2. 组件P1。它盛放各种鸡尾酒的杯子。不同的配方可能需要不同的杯子。
  3. 组件P2。它根据配方混合鸡尾酒,然后将混合好的饮品倒入杯中。
  4. 组件P3。它为杯中的鸡尾酒进行冷却。
  5. 组件P4。它将一杯成品饮品呈现给顾客。饮品被取走后,传感器将检测到该动作。

基本上,制作饮品的流程是使用R将鸡尾酒杯从P1移到P4,并根据配方要求在适当的时间启动组件Pi。

迈泰、蝎子碗和僵尸的详细操作顺序如下:

  1. 当通过按下机器前面板上的按钮请求制作鸡尾酒时,R从P1取一个杯子并将其放到P2。
  2. R从P2移到中立位置,以便能更快地再次移动到P1、P2和P3中的任何一个。
  3. P2混合鸡尾酒并将其倒入杯中。
  4. R将杯子从P2移到P3。
  5. R从P3移到中立位置,以便能更快地再次移动到P1、P2和P3中的任何一个。
  6. P3冷却饮品。
  7. 鸡尾酒冷却后,R将杯子从P3移到P4。
  8. R从P3移到中立位置,以便能更快地再次移动到P1、P2和P3中的任何一个。
  9. 顾客从P4取走饮品。传感器检测到P4已空,因此上述过程可以重复。

鸡尾酒(马提尼)的操作与上述相同,只是跳过了冷却过程。换句话说,混合好的饮品直接从P2移到P4,无需经过P3。

  1. 当通过按下机器前面板上的按钮请求制作鸡尾酒时,R从P1取一个杯子并将其放到P2。
  2. R从P2移到中立位置,以便能更快地再次移动到P1、P2和P3中的任何一个。
  3. P2混合鸡尾酒并将其倒入杯中。
  4. R将杯子从P2移到P4。
  5. R从P4移到中立位置,以便能更快地再次移动到P1、P2和P3中的任何一个。
  6. 顾客从P4取走饮品。传感器检测到P4已空,因此上述过程可以重复。

之所以让机器处理两种不同的配方顺序,是为了故意使配方执行稍微复杂一些,因为不同配方执行下的系统资源需求不同。不同配方对同一任务所需的时间差异可能导致不同的系统资源操作顺序。例如,“马提尼”这款酒即使比执行时间长的酒晚点单,也可能先出来,因为“马提尼”配方不需要冷却过程。如果没有良好的并发架构,您可能会想象软件需要处理这些特殊情况。在本示例中,没有专门的代码来处理这种情况,因为路由器的架构和分配给任务的同步资源会自动处理资源同步。

组件Pi如何工作的细节在这里不重要。并发实现是本文的重点。

系统分析

以下分析基于JEK Platform的并发对象模型。建议在阅读以下分析之前,先阅读JEK Platform中的一些示例。

JEK对象模型是应用程序引擎由路由和任务矩阵组成。行代表路由,列代表任务。JEK内核在每个路由中执行任务。

对于这个简单的示例,即使您没有阅读过JEK Platform的任何示例,分析也是直接而明显的。需要识别任务或步骤,以便设计潜在的并发。

1.类型1配方

配方:迈泰、蝎子碗、僵尸

Tasks(任务)

函数

CT1

R从P1取一个杯子并将其放到P2。

CTr1

然后,R移到中立位置。

CT2

P2混合请求的鸡尾酒,并将其倒入杯中。(P2具体如何工作不重要。)

CT3

鸡尾酒混合后,R将杯子从P2移到P3。

CTr3

然后,R移到中立位置。

CT4

P3开始冷却鸡尾酒。(P3具体如何工作不重要。)

CT5

R将杯子从P3移到P4。

CTr5

然后,R移到中立位置。

CT6

等待顾客从P4取走。顾客从P4取走杯子后,该任务执行必要的操作。我们假设T6轮询硬件以检查杯子是否已被取走。

图 2

由于大多数配方的执行顺序相同,因此为不同路由创建了相同的任务。

尽管Trᵢ(i = 3, 5)任务是让机器人手臂返回中立位置,但由于起始位置不同,执行上下文可能因硬件而异。

从硬件配置和上述流程分析中观察到的流水线如下:

  1. CT3完成后,P2空闲,可以混合下一杯鸡尾酒。
  2. CT5完成后,P3空闲,可以冷却下一杯鸡尾酒。
  3. 很明显,如果实现了流水线,调酒机最多可以同时容纳三杯饮品。这三杯鸡尾酒分别位于P2、P3和P4。

并行任务分析

  1. 路由内部的微并发。
    • CTr1和CT2。
    • CTr3和CT4。
  2. 从硬件架构可以看出系统流水线并发操作是显而易见的,因为它可以在内部容纳三杯饮品。因此,我们需要识别可以流水线处理的任务边界。
    • 从CT1到CTr3。
    • CT4。
    • 从CT5到CT6。

一个路由只能实现微并发。流水线并发必须通过多个路由来实现。在本例中,每个路由的目的和顺序都完全相同(“马提尼”除外)。

这些路由共享同一个称为同步资源的系统资源。

如果机器内实现了配方流水线,则意味着至少有一个任务正在执行。因此,如果请求四杯鸡尾酒,只有三杯被流水线处理。根据以上分析,机器可以同时流水线处理三杯鸡尾酒。定义了三个相同的路由。

定义了以下同步资源:

  • SR_R代表带有玻璃杯搬运臂的机器人。
  • SR_P2代表P2。
  • SR_P3代表P3。
  • SR_P4代表P4。

由于可能存在多个路由同时活动,因此通过同步资源实现了路由之间的任务同步,分析如下:

  1. 对于CT1,R被四个路由共享。它被定义为ProducerConsumerSR,名称为SR_R。此同步资源可以防止四个路由中的CT1任务同时执行,因为物理限制不允许这样做。
    • 之所以不能定义为MutexSR,是因为CTr1需要在CT1之后立即占用它。互斥发生在多个任务之间。
    • 不将CTr1定义为CT1的一部分的原因是CTr1可能花费大量时间。如果将其定义为CT1的一部分;在CT2期间的并发性会降低。现在,CTr1可以与CT2并行。
  2. 不同路由中的CT2共享P2,P2被定义为名为SR_P2的ProducerConsumerSR。
  3. CT3和CTr3占用已定义的SR_R。
  4. 与CT2一样,不同路由中的CT4共享相同的硬件资源P3。我们定义SR_P3来表示此同步资源。
  5. CT5和CTr5占用已定义的SR_R。
  6. 不同路由中的CT6共享P4。我们定义SR_P4来表示此同步资源。

同步资源分配

同步资源

占用任务

释放任务

SR_R

CT1

CTr1

SR_R

CT3

CTr3

SR_R

CT5

CTr5

SR_P2

CT1

CT3

SR_P3

CT3

CT5

SR_P4

CT5

CT6

图 3

2. 类型2配方

此配方稍短,因为它不需要冷却过程。混合完饮品后,杯子直接从P2移到P4,而不是移到P3。

配方:马提尼

Tasks(任务)

函数

CT1

R从P1取一个杯子并将其放到P2。

CTr1

然后,R移到中立位置。

CT2

P2混合请求的鸡尾酒,并将其倒入杯中。

CT3_Martini

鸡尾酒混合后,R从P2取走杯子并将其移至P4

CTr3_Martini

R移到中立位置。

CT6

等待顾客从P4取走。顾客从P4取走杯子后,该任务执行必要的操作。我们假设T6轮询硬件以检查杯子是否已被取走。

图 4

实现

为应用程序引擎创建了四个路由。路由1、2、3的任務結構相同,因為配方1、2、3的順序相同。唯一的區別是它們的執行上下文。路由4與路由1、2、3的任務結構不同。

路由类

函数

CRoute1

处理迈泰配方。

CRoute2

处理蝎子碗配方。

CRoute3

处理僵尸配方。

CRoute4_Short

处理马提尼配方。

图 5

路由类定义

class CCustomRouteContext : public knRouteContext 
{
public:
    CCustomRouteContext(CMachine *pEngine);
         ~CCustomRouteContext();
 
         CMachine*        m_pEngineRef;
         CAutoBartender1Dlg* GetDialog();
         void          AllocateRecipe(CCocktailRecipeBase *pRecipe);
         void          DeAllocateRecipe();
         CCocktailRecipeBase *m_pCocktailRecipe;
};
 
class CRoute1 : public knRoute
{
public:
    CRoute1(knEngine *pEngine, CCustomRouteContext *pCustomRouteContext);
 
     virtual void    Construct_CreateRoute_AddTask();
            virtual void    BeginRoute(knRouteContext *pRouteContext);         
            virtual void         EndRoute(knRouteContext *pRouteContext);
};
 
class CRoute2 : public knRoute
{
public:
    CRoute2(knEngine *pEngine, CCustomRouteContext *pCustomRouteContext);
 
         virtual void    Construct_CreateRoute_AddTask();
         virtual void    BeginRoute(knRouteContext *pRouteContext);         
         virtual void    EndRoute(knRouteContext *pRouteContext);
};
 
class CRoute3 : public knRoute
{
public:
    CRoute3(knEngine *pEngine, CCustomRouteContext *pCustomRouteContext);
 
         virtual void    Construct_CreateRoute_AddTask();
         virtual void    BeginRoute(knRouteContext *pRouteContext);         
         virtual void    EndRoute(knRouteContext *pRouteContext);
};
 
class CRoute4_Short : public knRoute
{
public:
    CRoute4_Short(knEngine *pEngine, CCustomRouteContext *pCustomRouteContext);
 
         virtual void    Construct_CreateRoute_AddTask();
         virtual void    BeginRoute(knRouteContext *pRouteContext);         
         virtual void    EndRoute(knRouteContext *pRouteContext);
};

引擎类定义

class CMachine : public knEngine
{
public:
    CMachine(CAutoBartender1Dlg *pDlg);      
         ~CMachine();

         virtual void    Construct_Debug_EnableDesignStudio();
         virtual void    Construct_Create_NamedSystemResource();
         virtual bool    Construct_CreateJob_AddRoutes();

     // Engine events

           virtual void       EngineNotify__Start_Job();
           virtual void         EngineNotify__Stop_Job(enumJobStopReason eStopReason);

          // Recipe handling

          void             InitCocktailRecipes();
          CObArray            m_arrayRecipe;
          CAutoBartender1Dlg  *m_pDlg;
};

模拟的配方定义在四个数组中。每行定义一个任务的执行内容。每个路由读取此数组,并使用函数CCustomRouteContext::AllocateRecipe(CCocktailRecipeBase *pRecipe)将执行项分配给任务。以下列出了迈泰的示例配方。

struct strucRecipeStep
{
    long                 lIndex;
        long                 lTimeInMilliSeconds;   
           
        char                 sTaskClassName[128];                                  
        char                 sOperation[128];
        char                 sOperationContext[128];                                 
        char                 sPlatformRobotState[128];
};
 
const strucRecipeStep RecipeDef_Maitai[TOTAL_RECIPE_EXE_STEPS1] =  
{
    {0, 10000,        "CT1",  "R moves glass from P1 to P2.",     "Mai Tai glass",     "Mai Tai "},
    {1, 1000,         "CTr1", "R retrieve from P2 to neutral.",   "N/A",            ""},
    {2, 10000,        "CT2",  "P2 mixes cocktail.",               "Mai Tai mix",       ""},
    {3, 10000,        "CT3",  "R moves glass from P2 to P3.",     "N/A",            "Mai Tai"},
    {4, 1000,         "CTr3", "R retrieve from P3 to neutral.",   "N/A",            ""},
    {5, 10000,        "CT4",  "P3 cools cocktail.",               "Mai Tai cooling", ""},
    {6, 10300,        "CT5",  "R moves glass from P3 to P4.",     "N/A",            "Mai Tai "},
    {7, 1000,         "CTr5", "R retrieve from P3 to neutral.",   "N/A",            ""},
    {8, 0,            "CT6",  "Wait for customer to pick up.",    "N/A",               ""},       
};

结果分析和设计验证

创建了一个对话框窗口来模拟调酒机。与真实机器一样,您必须按下“启动机器”来启动调酒师并发应用程序引擎。

当通过按下“启动机器”按钮启动机器时,将启用四个菜单项。在此过程中,调酒师应用程序引擎以暂停模式启动四个路由。

五个组件的活动显示在五个框中。执行活动,例如哪个任务占用了组件以及正在处理哪个配方,显示在框内。

验证简单案例

您可以通过按下四个按钮中的一个来订购鸡尾酒。在配方执行过程中,杯子从P1移动到P4,这由下面图中的浅蓝色箭头标记。深蓝色箭头显示目前P2上有什么样的饮品。

图 6

处理配方时,执行时序很容易验证。在下面的路由0(执行迈泰配方)的时序图中,任务按顺序执行,并且有两对并行任务。任务会合也看起来是正确的。

Sample screenshot

图 7

验证简单流水线案例

如果订购不止一杯饮品,机器将在内部流水线处理订单。在下面的示例中,迈泰和蝎子碗被机器同时处理,但每杯饮品处于不同的处理步骤。在下面的机器前面板图中,迈泰(路由0)和蝎子碗(路由1)几乎同时被订购。P4上的迈泰已完成。P3上的蝎子碗已排队并完成冷却步骤。

Sample screenshot

图 8

当饮品准备好时,它将停留在P4,直到顾客通过点击复选框“点击取货:(饮品)”将其取走。然后,P3上的排队饮品将移至P4。

下面的时序图中的三个时间线(由蓝色气泡标记)解释如下:

  1. R在将混合好的迈泰移至P3进行冷却后,完成移动到中立位置的操作。路由1(蝎子碗配方)开始将杯子从P1移至P2。
  2. R在将冷却好的迈泰移至P4后,完成移动到中立位置的操作。此时,迈泰杯已在P4上。然后,R开始将蝎子碗杯从P2移至P3,这是任务CT3。
  3. 路由1(蝎子碗配方)的CT4已完成。这意味着蝎子碗饮品已冷却,可以移至P4。

由于顾客尚未取走迈泰,系统内已排队两杯饮品。

Sample screenshot

图 9

验证特殊流水线案例

研究了一个特殊案例来测试应用程序引擎的编排能力。测试案例是先订购僵尸,然后立即订购马提尼。

在本例中,我们故意让马提尼配方的执行时间比僵尸配方短(CT4冷却时间长,以便马提尼可能赶上僵尸配方执行)。由于马提尼跳过了由P3处理的冷却过程,因此即使马提尼订购得更早,也可能先出来。

您可以想象,如果冷却时间太短,马提尼就永远无法赶上僵尸。但是,阻止这种情况发生的冷却过程的临界时间是多少?最快的方法是模拟。

在本实验中,先点击了僵尸按钮,然后是马提尼按钮。结果是马提尼先出来。

Sample screenshot

图 10

上面案例的时序图中的四个时间线(由蓝色气泡标记)解释如下:

  1. 路由2(僵尸配方)通过启动CT1开始执行。
  2. 路由3(马提尼配方)启动CT3_Martini,该任务占用P4(SR_P4)。这是阻止路由2将冷却好的僵尸杯放到P4上的最关键事件。这就是为什么马提尼订单赶上了僵尸订单,尽管它订购得更早。路由2(僵尸配方)之所以不能先占用P4,是因为它仍在冷却过程中CT4。任务CT5(CT4之后)占用了P4(SR_P4),但它来得太晚了。
  3. 僵尸饮品已在P3上完成。路由2(僵尸配方)的CT4已完成。僵尸饮品已冷却,但CT5被阻塞,因为P4(SR_P4)被路由3的CT3_Martini占用。
  4. 马提尼已完成并在P4上。

如果仔细观察,如果CT4比蓝色气泡2标记的垂直线短,马提尼就永远无法赶上僵尸。

Sample screenshot

图 11

展示的JEK Platform特性

这个简单的示例演示了几个重要的JEK Platform特性:

  1. 路由是一个逻辑工作执行路径。机器允许最多三个鸡尾酒进行流水线处理。引擎中创建了四个路由来执行四种配方。这个概念使用户更容易在相同的硬件配置下处理不同类型的配方。可以创建具有不同任务配置的不同路由来处理不同类型的配方。用户可以同时(几乎同时)点击四个鸡尾酒按钮,机器将根据同步资源的定义及其在不同路由中任务的分配来排队请求。由于JEK内核负责处理排队,因此无需编写代码。
  2. 应用程序引擎可以在实际硬件上运行之前进行模拟。这减少了协调硬件操作时损坏硬件的可能性。
  3. 时序图无需应用程序层中的特殊代码即可生成。
  4. 在本例中,可以通过简单地更改配方定义来验证不同的时序,从而允许在不同的时序条件下测试应用程序引擎,以确保产品质量。

比较流水线内核与其他可能解决方案

如果不使用JEK SDK,最明显的实现方法是为每个硬件组件创建一个线程,因为这是实现最大并发性的唯一方法。需要实现线程管理器或监视器来监视正在执行的配方,从而确定不同配方所需的线程执行上下文。在分析过程中不太可能使用系统化方法,架构可能难以理解,并且线程同步代码可能非常难以维护。更糟糕的是,轻微的硬件修改可能导致大量的代码更改,这使得未来的增强更加困难。维护这样的代码成本很高。

本文提供的解决方案具有高可维护性和低增强成本,因为它具有易于理解的架构。软件开发工具还提供集成的模拟和验证环境,大大简化了设计和验证。

局限性和未来改进

当前实现中最明显的局限性是,即使机器可以在内部排队最多三杯饮品,也无法排队相同类型的鸡尾酒请求。例如,客户不能在短时间内按三次迈泰按钮来同时订购三杯迈泰。

可能的解决方案

目前,四个路由预加载了四种不同的配方。JEK SDK要求在引擎启动时创建路由对象,并且路由对象不能动态创建或删除。一种可能的解决方案是为一种配方创建三个路由,这样机器就有可能同时处理三杯相同的鸡尾酒。因此,可以创建十二个路由来处理这种情况。

创建的路由数量与机器的物理架构无关。

结论

本文提出的实现的优点是:

  1. 系统分析方法易于理解,从而可以使用JEK SDK轻松实现应用程序引擎。
  2. 只要工程师了解如何使用JEK对象模型分析并发系统,应用程序引擎框架就可以由不同的工程师轻松修改和增强。
  3. JEK Platform支持应用程序引擎的验证和模拟,这将节省开发成本。

使用操作系统同步原语和线程的替代实现不能保证软件工程师团队拥有系统的分析方法并实现灵活的并发内核架构。几乎所有开发团队都不允许开发模拟和验证支持环境,这可能导致产品开发成本大大提高。

© . All rights reserved.