孟宁老师在高级软件工程课上讲解了一个通用的菜单小程序,通过这个程序的逐步演化让我们学习软件工程的基本思想。

一. 编译和调试环境配置

1. 编译项目

项目源代码在https://github.com/mengning/menu

在项目目录下输入make,make程序会自动找到makefile并执行编译工作。

2. 调试环境配置

GDB在命令行下调试程序效率低下,而且不够直观,所以我们采用孟宁老师推荐的Visual Studio Code编辑器配合GDB调试程序。

在vscode中打开项目目录menu,在编辑器左侧选择Run选项,点击“Run and Debug”按钮,选择默认的“C++(GDB/LLDB)”,

接下来vscode会在项目目录下生成一个.vscode文件夹,里面有tasks.json和launch.json两个配置文件。launch.json中的preLaunchTask是调试之前执行的构建任务,在tasks.json中可以配置该任务。我们把task的名称改为menu,并且加入menu.c和linktable.c两个文件:

设置断点,切换到文件test.c,按下F5,就可以开始调试了。

二、项目分析

老师提供了menu项目的实验代码,这些代码展示了菜单程序如何从简单到复杂发展,源代码在https://gitee.com/mengning997/se/tree/master/src

1. 模块化的思想

模块化思想就是把一个复杂的系统分解成多个相对独立的模块,每个模块完成特定的功能。这么做有几个优点:

  • 单个模块较为简单,更容易理解
  • 修改方便,一个模块的变更只影响很少的几个软件模块
  • 减少冗余代码,一个模块可以多次使用
  • 更容易定位bug
  • 减少命名冲突

项目lab2到lab4的演进就是一个软件从简单到复杂、从紧耦合到松散耦合的过程。

lab2接受一个输入,然后用if-else语句查找对应的指令。

lab3.1使用链表结构代替if-else分支语句,使查找过程变得更加简洁,同时把指令对应的操作分离出来,写成单独的函数。

在lab3.2中,可以看到只有menu.c一个文件,这个文件包含了对链表的定义、赋值以及查找等操作,

lab3.3把链表的一部分实现放到了单独的源文件中,但是链表的创建还是硬编码在menu.c文件中。

lab4真正实现了一个可以重复使用的链表,使得menu.c可以专注于处理输入的指令。

2. 可重用接口

lab4中链表节点定义如下:

/*
 * LinkTable Node Type
 */
typedef struct LinkTableNode
{
    struct LinkTableNode * pNext;
} tLinkTableNode;

和一般的链表节点不同,这里的节点中只有一个指向下一个节点的指针,而没有用来存放数据的变量。这是因为linktable是一个通用的链表接口,它适用于所有类型的数据,包括自定义的数据类型。为了使用它,我们需要在menu.c中定义包含数据部分的节点,实际使用时通过强制类型转换传入参数:

/* data struct and its operations */

typedef struct DataNode
{
    tLinkTableNode * pNext;
    char*   cmd;
    char*   desc;
    int     (*handler)();
} tDataNode;

/* show all cmd in listlist */
int ShowAllCmd(tLinkTable * head)
{
    tDataNode * pNode = (tDataNode*)GetLinkTableHead(head); 
    while(pNode != NULL)
    {
        printf("%s - %s\n", pNode->cmd, pNode->desc);
        /* 强制类型转换,把 (tDataNode *) 转换成 (tLinkTableNode *) */
        pNode = (tDataNode*)GetNextLinkTableNode(head,(tLinkTableNode *)pNode);
    }
    return 0;
}

lab5.1增加的callback方式的函数,使用者只需要提供查找的方式,无需知道接口的内部结构,就能让链表搜索

3. 线程安全

线程类似于独立的进程,但是它们共享地址空间,从而能访问到相同的数据。在多线程的程序中,由于一段代码可能被中断,导致代码每次执行的结果都有可能不一样。可重入函数可以由多个任务并发使用,而不必担心数据错误。

在多线程情况下,链表的插入、删除操作存在读写操作,如果不加锁就可能出现运行异常,在实验中使用互斥量pthrad_mutex让linktable的插入、删除操作变成原子操作。

以链表的插入操作为例,pthread_mutex_lock和pthread_mutex_unlock包围了链表插入的核心操作:

/*
 * Add a LinkTableNode to LinkTable
 */
int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
    if(pLinkTable == NULL || pNode == NULL)
    {
        return FAILURE;
    }
    pNode->pNext = NULL;
    pthread_mutex_lock(&(pLinkTable->mutex));
    if(pLinkTable->pHead == NULL)
    {
        pLinkTable->pHead = pNode;
    }
    if(pLinkTable->pTail == NULL)
    {
        pLinkTable->pTail = pNode;
    }
    else
    {
        pLinkTable->pTail->pNext = pNode; /* (1) */
        pLinkTable->pTail = pNode;        /* (2) */
    }
    pLinkTable->SumOfNode += 1 ;
    pthread_mutex_unlock(&(pLinkTable->mutex));
    return SUCCESS;		
}

在不加锁的情况下,如果两个线程同时对 同一个链表进行插入,线程1执行到(1)处中断,等到线程2插入完成后再执行(2),就会造成pTail指针指向混乱。

posted on 2020-11-10 21:01  xiop  阅读(285)  评论(0)    收藏  举报