代码中的软件工程

代码中的软件工程

本篇博客是基于孟宁老师的高级软件工程课程内容完成的,感谢孟宁老师的教学指导。

参考资料:代码中的软件工程

C/C++编译调试环境配置

GCC安装配置

为了编译调试课程项目案例,需要一个 C/C++ 编译器和调试器,这里采用Mingw-w64/GCC。

下载并安装完成后,将安装目录下的/bin目录加入环境变量,添加环境变量后,打开CMD,执行gcc -v查看是否安装成功,如下图出现各种信息则安装成功。

VSCode配置

本次项目采用Visual Studio Code(简称vscode)作为代码编辑器,为了让vscode支持C/C++,需要为其安装c/c++扩展:

在扩展中搜索c++并安装。

使用简单的cpp文件配置c++环境:

  • 新建空文件夹test;

  • 打开VScode --> 打开文件夹 --> 选择刚刚创建的文件夹Code;

  • 新建HelloWorld.cpp文件;

  • 点击运行,启动调试,选择 C++(GDB/LLDB),再选择 g++.exe,之后会自动生成launch.json配置文件,编辑此文件;

    {
        "configurations": [
            {
                "name": "g++.exe - 生成和调试活动文件",
                "type": "cppdbg",
                "request": "launch",
                "program": "${fileDirname}\\${fileBasenameNoExtension}.exe",
                "args": [],
                "stopAtEntry": false,
                "cwd": "${workspaceFolder}",
                "environment": [],
                "externalConsole": true,
                "MIMode": "gdb",
                "miDebuggerPath": "C:\\UserPrograms\\mingw64\\bin\\gdb.exe",
                "setupCommands": [
                    {
                        "description": "为 gdb 启用整齐打印",
                        "text": "-enable-pretty-printing",
                        "ignoreFailures": true
                    }
                ],
                "preLaunchTask": "task g++"
            }
        ]
    }
    
  • 返回.cpp文件,按F5进行调试,会弹出找不到任务”task g++“,选择“配置任务”,会自动生成tasks.json文件,编辑此文件 ;

    {
        "tasks": [
            {
                "type": "cppbuild",
                "label": "task g++",
                "command": "C:\\UserPrograms\\mingw64\\bin\\g++.exe",
                "args": [
                    "-g",
                    "${file}",
                    "-o",
                    "${fileDirname}\\${fileBasenameNoExtension}.exe"
                ],
                "options": {
                    "cwd": "C:\\UserPrograms\\mingw64\\bin"
                },
                "problemMatcher": [
                    "$gcc"
                ],
                "group": {
                    "kind": "build",
                    "isDefault": true
                },
                "detail": "Generated task by Debugger"
            }
        ],
        "version": "2.0.0"
    }
    

    launch.json文件中“preLaunchTask”的值必须与tasks.json文件中“label”的值一致。

  • 再次返回.cpp文件,按F5进行调试,可见运行成功,弹出窗口。

代码分析

使用git clone课程项目menu后,在vscode中打开,按上述方法进行配置,项目结构如下。

该项目实现一个命令行的菜单小程序,最终目标是完成一个通用的命令行的菜单子系统便于在不同项目中重用。

模块化设计

模块化的基本原理

模块化是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离,关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法。

模块化的优点

模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。从而整个软件系统也更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。而且整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。

模块化程度的衡量

一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。

耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。 一般在软件设计中我们追求松散耦合。

内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。

项目中的模块化设计

在本项目中,我们把将数据结构和它的操作与菜单业务之间进行分离处理就是一个典型的模块化代码,我们把与 菜单业务处理相关的代码放在menu.c文件中,把与数据结构及其操作相关的代码放在linklist.h文件中在,并在linklist.c文件中实现所需函数。

在linklist.h文件中, 定义了链表结点的结构与操作函数

在linklist.c文件中, 定义了链表的结构并实现了之前定义的操作函数

在这一部分代码中没有设计任何项目的业务逻辑,只是定义了链表这种数据结构和操作。如果要用到链表操作,只需要导入linktable.h,就可以将数据结构应用到业务逻辑中,无需关注数据结构的细节。

可重用接口

消费者重用和生产者重用

消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。

在重用已有的软件模块代码时一般会重点考虑如下四个关键因素:

  • 该软件模块是否能满足项目所要求的功能;
  • 采用该软件模块代码是否比从头构建一个需要更少的工作量,包括构建软件模块和集成软件模块等相关的工作;
  • 该软件模块是否有完善的文档说明;
  • 该软件模块是否有完整的测试及修订记录。

我们清楚了消费者重用时考虑的因素,那么生产者在进行可重用软件设计时需要重点考虑的因素也就清楚了。

接口的相关概念

接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式

在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放的一组属性和方法的集合。

接口包含五个基本要素:

  • 接口的目的;
  • 接口使用所需满足的前置条件或假定条件;
  • 使用接口的双方遵守的协议规范;
  • 接口使用之后的效果,一般称为后置条件;
  • 接口所隐含的质量属性。
项目中的可重用接口设计

在本项目中,我们用到了链表这种数据结构,而在一些比较大的项目中,我们可能会多次用到链表。因此,我们可以编写一个有关链表的通用模块,方便以后的重用。于是我们编写了前面的linktable.c和linktable.h文件。

linktable.h文件中定义了一系列的接口,比如GetNextLinkTableNode函数:

/*
 * get next LinkTableNode
 */
tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);

该函数的功能是获取下一个链表节点,调用该函数时需要传入链表和当前节点,函数返回下一个结点。

调用者使用这个接口,只需要关注传入的参数和结果,无需关心细节。

menu.c中对该接口的调用:

/* find a cmd in the linklist and return the datanode pointer */
tDataNode* FindCmd(tLinkTable * head, char * cmd)
{
    tDataNode * pNode = (tDataNode*)GetLinkTableHead(head);
    while(pNode != NULL)
    {
        if(!strcmp(pNode->cmd, cmd))
        {
            return  pNode;  
        }
        pNode = (tDataNode*)GetNextLinkTableNode(head,(tLinkTableNode *)pNode);
    }
    return NULL;
}

线程安全

线程

线程是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一般默认一个进程中只包含一个线程。

操作系统中的线程概念也被延伸到CPU硬件上,多线程CPU就是在一个CPU上支持同时运行多个指令流,而多核CPU就是在一块芯片上集成了多个CPU核,比如4核8线程CPU芯片就是在集成了4个CPU核,每个CPU核上支持2个线程。

可重入函数

可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。

线程安全

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

函数的可重入性与线程安全之间的关系

  • 可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题;
  • 不可重入的函数一定不是线程安全的。
项目中的线程安全设计

项目中的线程安全设计体现在链表的定义中,如果多个线程对同一个链表执行操作则存在线程安全问题,所以项目在定义链表结构时,添加了用于互斥操作的线程锁。

/*
 * LinkTable Type
 */
struct LinkTable
{
    tLinkTableNode *pHead;
    tLinkTableNode *pTail;
    int			SumOfNode;
    pthread_mutex_t mutex;
};

我们将链表作为一种临界资源,对其进行互斥访问,操作链表前要为其上锁,操作结束后要为其解锁。

/*
 * Delete a LinkTable
 */
int DeleteLinkTable(tLinkTable *pLinkTable)
{
    if(pLinkTable == NULL)
    {
        return FAILURE;
    }
    while(pLinkTable->pHead != NULL)
    {
        tLinkTableNode * p = pLinkTable->pHead;
        pthread_mutex_lock(&(pLinkTable->mutex));
        pLinkTable->pHead = pLinkTable->pHead->pNext;
        pLinkTable->SumOfNode -= 1 ;
        pthread_mutex_unlock(&(pLinkTable->mutex));
        free(p);
    }
    pLinkTable->pHead = NULL;
    pLinkTable->pTail = NULL;
    pLinkTable->SumOfNode = 0;
    pthread_mutex_destroy(&(pLinkTable->mutex));
    free(pLinkTable);
    return SUCCESS;		
}

代码的pthread_mutex_lock操作和pthread_mutex_unlock操作,就时对链表进行上锁解锁操作,实现了临界资源的互斥访问,实现线程安全。

posted @ 2020-11-06 16:06  freedem  阅读(196)  评论(0)    收藏  举报