C语言实现 操作系统 经典的进程同步问题(2)
哲学家进餐问题
哲学家进餐问题是一个经典的同步问题,涉及多个哲学家试图同时用餐,但每个哲学家左右两边只有一把叉子。为了避免死锁和饥饿,可以使用记录型信号量(也称为计数信号量)来管理叉子的使用。
1、利用记录型信号量解决哲学家进餐问题
(1)信号量初始化:使用 sem_init 初始化每把叉子的信号量,初始值为1,表示可用。
哲学家线程:每个哲学家线程尝试拿起左边的叉子(sem_trywait),如果失败则继续思考。如果成功拿起左边的叉子,再尝试拿起右边的叉子。如果失败,则放下左边的叉子并继续思考。如果成功拿起两把叉子,则哲学家开始吃饭,吃完后放下两把叉子。
输出控制:使用互斥锁 print_mutex 控制输出顺序,避免输出混乱。
线程创建和等待:创建哲学家线程,并使用 pthread_join 等待所有线程完成(实际上这是一个无限循环程序,所以不会真正退出)。
资源清理:销毁信号量和互斥锁。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h> #define NUM_PHILOSOPHERS 5
#define THINK_TIME 1
#define EAT_TIME 2 sem_t forks[NUM_PHILOSOPHERS]; // 每把叉子一个信号量
pthread_mutex_t print_mutex; // 控制输出顺序的互斥锁 void *philosopher(void *arg) { int id = *((int *)arg); int left_fork = id; int right_fork = (id + 1) % NUM_PHILOSOPHERS; while (1) { // 思考 printf("Philosopher %d is thinking.\n", id); sleep(THINK_TIME); // 尝试拿起左边的叉子 if (sem_trywait(&forks[left_fork]) == -1) { // 拿起左边的叉子失败,继续思考 continue; } // 尝试拿起右边的叉子 if (sem_trywait(&forks[right_fork]) == -1) { // 拿起右边的叉子失败,放下左边的叉子并继续思考 sem_post(&forks[left_fork]); continue; } // 拿起两把叉子,开始吃饭 printf("Philosopher %d is eating.\n", id); sleep(EAT_TIME); printf("Philosopher %d has finished eating.\n", id); // 放下叉子 sem_post(&forks[left_fork]); sem_post(&forks[right_fork]); } pthread_exit(NULL);
} int main() { pthread_t threads[NUM_PHILOSOPHERS]; int ids[NUM_PHILOSOPHERS]; // 初始化信号量,每把叉子的初始值为1,表示可用 for (int i = 0; i < NUM_PHILOSOPHERS; i++) { sem_init(&forks[i], 0, 1); } // 初始化互斥锁,用于控制输出顺序 pthread_mutex_init(&print_mutex, NULL); // 创建哲学家线程 for (int i = 0; i < NUM_PHILOSOPHERS; i++) { ids[i] = i; pthread_create(&threads[i], NULL, philosopher, &ids[i]); } // 等待所有线程完成(实际上,这是一个无限循环程序,所以这里不会退出) for (int i = 0; i < NUM_PHILOSOPHERS; i++) { pthread_join(threads[i], NULL); } // 销毁信号量和互斥锁 for (int i = 0; i < NUM_PHILOSOPHERS; i++) { sem_destroy(&forks[i]); } pthread_mutex_destroy(&print_mutex); return 0;
}
注意:
sem_trywait是非阻塞的,如果信号量的值大于0,则减一并返回,否则立即返回-1并设置errno为EAGAIN。- 使用
pthread_join等待线程实际上在这个例子中是不必要的,因为哲学家线程是无限循环的。如果要终止程序,可以添加额外的控制逻辑。
(2)init_random():初始化随机数生成器。
think(int philosopherNumber):模拟哲学家思考的过程,打印消息并随机睡眠一段时间。
eat(int philosopherNumber):模拟哲学家吃饭的过程,打印消息并随机睡眠一段时间。
*philosopher(void *arg):哲学家的线程函数,包含无限循环,不断执行思考和吃饭的过程。在拿起叉子前使用互斥锁保护,成功后释放互斥锁,吃饭后放下叉子。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h> // 包含sleep函数所需的头文件
#include <time.h> #define PHILOSOPHERS_COUNT 5 sem_t forks[PHILOSOPHERS_COUNT];
sem_t mutex; // 初始化随机数生成器
void init_random() { srand(time(NULL));
} void think(int philosopherNumber) { printf("Philosopher %d is thinking.\n", philosopherNumber); // 模拟思考时间 int sleepTime = rand() % 5 + 1; sleep(sleepTime);
} void eat(int philosopherNumber) { printf("Philosopher %d is eating.\n", philosopherNumber); // 模拟吃饭时间 int sleepTime = rand() % 5 + 1; sleep(sleepTime);
} void *philosopher(void *arg) { int philosopherNumber = *((int *)arg); while (1) { // 注意:这里的无限循环可能导致程序永远不退出 think(philosopherNumber); // 使用互斥锁保护对叉子的访问 sem_wait(&mutex); // 尝试拿起左右两边的叉子 int leftFork = philosopherNumber; int rightFork = (philosopherNumber + 1) % PHILOSOPHERS_COUNT; sem_wait(&forks[leftFork]); sem_wait(&forks[rightFork]); // 释放互斥锁,因为已经成功拿起了两把叉子 sem_post(&mutex); eat(philosopherNumber); // 放下叉子 sem_post(&forks[leftFork]); sem_post(&forks[rightFork]); } return NULL;
} int main() { pthread_t philosophers[PHILOSOPHERS_COUNT]; int philosopherNumbers[PHILOSOPHERS_COUNT]; // 初始化随机数生成器 init_random(); // 初始化信号量 sem_init(&mutex, 0, 1); for (int i = 0; i < PHILOSOPHERS_COUNT; i++) { sem_init(&forks[i], 0, 1); } // 创建哲学家线程 for (int i = 0; i < PHILOSOPHERS_COUNT; i++) { philosopherNumbers[i] = i; pthread_create(&philosophers[i], NULL, philosopher, &philosopherNumbers[i]); } // 注意:这里使用pthread_join会导致程序永远等待,因为哲学家线程是无限循环的 // 如果需要程序退出,应该实现某种退出机制,比如使用全局变量和条件变量 for (int i = 0; i < PHILOSOPHERS_COUNT; i++) { pthread_join(philosophers[i], NULL); } // 清理资源(在实际应用中,应该在确保所有线程都正确退出后再进行) for (int i = 0; i < PHILOSOPHERS_COUNT; i++) { sem_destroy(&forks[i]); } sem_destroy(&mutex); // 注意:由于使用了无限循环,下面的代码将不会被执行 // 这里只是为了展示如何清理资源 // 在实际应用中,应在程序退出前确保所有线程都正确终止 return 0;
}
注意:由于while (1)循环的存在,pthread_join和信号量销毁的代码在实际应用中不会被执行。在实际应用中,您需要实现一种机制来安全地终止线程并清理资源。这通常涉及到使用全局变量、条件变量或其他线程间通信机制来协调线程的终止。
2、利用AND信号量机制解决哲学家进餐问题
(1)AND信号量要求线程(在本例中为哲学家)同时获取多个资源(叉子)才能继续执行。然而,标准的POSIX信号量API并不直接支持AND信号量,但我们可以通过使用互斥锁(mutex)和计数信号量(counting semaphore)的组合来模拟这种机制。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <stdbool.h>
#include <time.h>#define PHILOSOPHERS_COUNT 5
#define THINK_TIME 1 // 思考时间(秒)
#define EAT_TIME 1 // 吃饭时间(秒)sem_t forks[PHILOSOPHERS_COUNT]; // 每只叉子一个信号量
pthread_mutex_t mutex; // 互斥锁,用于保护对退出标志的访问
bool should_exit = false; // 退出标志// 初始化随机数生成器(虽然在这个示例中未直接使用 rand,但保留以备将来需要)
void init_random() {srand(time(NULL));
}// 哲学家线程函数
void *philosopher(void *arg) {int philosopher_number = *((int *)arg);while (!should_exit || /* 另一个条件,用于确保在退出前完成当前循环 */(should_exit && (philosopher_number % 2 == 0 || /* 可选的:让偶数编号的哲学家先完成 */(sem_trywait(&forks[philosopher_number]) == 0 && sem_trywait(&forks[(philosopher_number + 1) % PHILOSOPHERS_COUNT]) == 0)))) {// 思考printf("Philosopher %d is thinking.\n", philosopher_number);sleep(THINK_TIME);// 尝试拿起左右两边的叉子(模拟 AND 信号量)pthread_mutex_lock(&mutex); // 保护对叉子信号量的操作if (sem_trywait(&forks[philosopher_number]) == 0 &&sem_trywait(&forks[(philosopher_number + 1) % PHILOSOPHERS_COUNT]) == 0) {// 成功拿起两把叉子pthread_mutex_unlock(&mutex); // 释放互斥锁,因为已经成功拿起了叉子// 吃饭printf("Philosopher %d is eating.\n", philosopher_number);sleep(EAT_TIME);// 放下叉子sem_post(&forks[philosopher_number]);sem_post(&forks[(philosopher_number + 1) % PHILOSOPHERS_COUNT]);} else {// 未能同时拿起两把叉子,释放互斥锁并继续思考pthread_mutex_unlock(&mutex);}// 检查退出标志pthread_mutex_lock(&mutex);if (should_exit) {pthread_mutex_unlock(&mutex);break; // 直接跳出循环}pthread_mutex_unlock(&mutex);}return NULL;
}int main() {pthread_t philosophers[PHILOSOPHERS_COUNT];int philosopher_numbers[PHILOSOPHERS_COUNT];// 初始化随机数生成器(虽然在这个示例中未直接使用)init_random();// 初始化信号量和互斥锁pthread_mutex_init(&mutex, NULL);for (int i = 0; i < PHILOSOPHERS_COUNT; i++) {sem_init(&forks[i], 0, 1); // 初始值为 1,表示叉子可用}// 创建哲学家线程for (int i = 0; i < PHILOSOPHERS_COUNT; i++) {philosopher_numbers[i] = i;pthread_create(&philosophers[i], NULL, philosopher, &philosopher_numbers[i]);}// 让哲学家们运行一段时间,然后设置退出标志sleep(10); // 这里只是为了演示,实际中可能使用其他条件来触发退出pthread_mutex_lock(&mutex);should_exit = true;pthread_mutex_unlock(&mutex);// 等待哲学家线程退出for (int i = 0; i < PHILOSOPHERS_COUNT; i++) {pthread_join(philosophers[i], NULL);}// 清理资源for (int i = 0; i < PHILOSOPHERS_COUNT; i++) {sem_destroy(&forks[i]);}pthread_mutex_destroy(&mutex);return 0;
}
(2)sem_t mutex;:用于保护对共享资源(如state数组和meals_eaten数组)的访问。
sem_t s[N];:表示左右叉子的可用性。如果s[i]为0,则第i位哲学家的右叉子(或第(i-1)%N位哲学家的左叉子)不可用。
sem_t s_try[N];:用于在哲学家尝试获取叉子时同步。
int state[N];:表示每位哲学家的当前状态(思考、饥饿、就餐)。
int meals_eaten[N];:记录每位哲学家吃过的饭的数量。
pthread_cond_t cond[N];:条件变量,用于在哲学家无法获取叉子时等待
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <stdbool.h>#define N 5
#define THINKING 0
#define HUNGRY 1
#define EATING 2
#define MAX_MEALS 10 sem_t mutex;
sem_t s[N];
sem_t s_try[N];
int state[N];
int meals_eaten[N];pthread_cond_t cond[N];void *philosopher(void *num);
void take_forks(int);
void put_forks(int);
void test(int);void debug_print(int philosopher_num, const char *message) {printf("Philosopher %d: %s\n", philosopher_num, message);
}int main() {pthread_t thread_id[N];sem_init(&mutex, 0, 1);for (int i = 0; i < N; i++) {sem_init(&s[i], 0, 0);sem_init(&s_try[i], 0, 0);meals_eaten[i] = 0;pthread_cond_init(&cond[i], NULL);}for (int i = 0; i < N; i++) {pthread_create(&thread_id[i], NULL, philosopher, (void *)(long long)i);}for (int i = 0; i < N; i++) {pthread_join(thread_id[i], NULL);}return 0;
}void *philosopher(void *num) {long long int i = (long long int)(num);for (int meal = 0; meal < MAX_MEALS; meal++) {// 思考{sem_wait(&mutex);state[i] = THINKING;sem_post(&mutex);debug_print(i, "Thinking");}// 尝试获取叉子take_forks((int)i);// 吃饭{sem_wait(&mutex);state[i] = EATING;sem_post(&mutex);debug_print(i, "Eating");}// 放下叉子put_forks((int)i);}return NULL;
}void take_forks(int i) {sem_wait(&mutex);state[i] = HUNGRY;debug_print(i, "Hungry");test(i);if (state[i]!= EATING) {pthread_cond_wait(&cond[i], &mutex);}sem_post(&mutex);sem_wait(&s_try[i]);sem_wait(&s[i]);
}void put_forks(int i) {sem_wait(&mutex);state[i] = THINKING;debug_print(i, "Put down forks");meals_eaten[i]++;test((i + 4) % N);test((i + 1) % N);sem_post(&mutex);sem_post(&s[(i + 4) % N]);sem_post(&s[(i + 1) % N]);pthread_cond_signal(&cond[(i + 4) % N]);pthread_cond_signal(&cond[(i + 1) % N]);
}void test(int i) {sem_wait(&mutex);if (state[i] == HUNGRY && state[(i + 4) % N]!= EATING && state[(i + 1) % N]!= EATING) {state[i] = EATING;sem_post(&s_try[i]);sem_post(&s[i]);pthread_cond_signal(&cond[i]);}sem_post(&mutex);
}
注意:
- 代码中使用了信号量和条件变量来同步对共享资源的访问,以避免竞态条件和死锁。
- 通过
mutex信号量保护对state数组的访问,确保在检查和修改哲学家状态时不会发生数据竞争。 - 通过
s[i]和s_try[i]信号量以及条件变量cond[i]来协调哲学家获取和释放叉子的过程。 - 代码中的
% N操作确保了索引在有效范围内循环。
相关文章:
C语言实现 操作系统 经典的进程同步问题(2)
哲学家进餐问题 哲学家进餐问题是一个经典的同步问题,涉及多个哲学家试图同时用餐,但每个哲学家左右两边只有一把叉子。为了避免死锁和饥饿,可以使用记录型信号量(也称为计数信号量)来管理叉子的使用。 1、利用记录型…...
有效的字母异位词【字符串哈希】
题目 题解: 1.排序: #include<algorithm>class Solution{public:bool isAnagram(string s,string t){sort(s.begin(),s.end());sort(t.begin(),t.end());return st;} } 时间复杂度O(nlogn) 2.哈希表 #include<algorithm>int hash1[100]; …...
如何选择与运用工具提升工作效率的秘密指南
一、引言 ---- 在当今这个信息爆炸的时代,编程工具的选择对于开发者的工作效率至关重要。从智能的代码编辑器到强大的版本控制工具,再到那些能让我们事半功倍的自动化脚本,每一款工具都有其独特的优势和价值。那么,哪款编程工具…...
Spring系列 AOP实现过程
文章目录 实现原理EnableAspectJAutoProxyAnnotationAwareAspectJAutoProxyCreator 代理创建过程wrapIfNecessarygetAdvicesAndAdvisorsForBeanfindCandidateAdvisorsfindAdvisorsThatCanApply createProxy AspectJ注解处理代理调用过程 实现原理 本文源码基于spring-aop-5.3.…...
C语言 getchar 函数完全解析:掌握字符输入的关键
前言 在C语言中,getchar 是一个非常实用的函数,用于从标准输入流(通常是键盘)读取单个字符。这对于处理文本输入非常有用,尤其是在需要逐个字符处理的情况下。本文将深入探讨 getchar 函数的用法和特点,并…...
Docker安装mysql8并配置主从复制
1. 安装mysql8 1.1 新增挂载文件 # 新增mysql挂载文件夹 mkdir -p /root/docker/mysql/m01/log mkdir -p /root/docker/mysql/m01/data mkdir -p /root/docker/mysql/m01/conf1.2 新增mysql配置文件 # 新增mysql配置文件 cd /root/docker/mysql/m01/conf vim my.cnf # 下面是…...
快手:数据库升级实践,实现PB级数据的高效管理|OceanBase案例
本文作者:胡玉龙,快手技术专家 快手在较初期采用了OceanBase 3.1版本成功替换了多个核心业务、数百套的MySQL集群。至2023年,快手的数据量已突破800TB大关,其中最大集群的数据量更是达到了数百TB级别。为此,快手将数据…...
基于Node.js+Express+MySQL+VUE实现的计算机毕业设计共享单车管理网站
单车信息选择骑行 骑行状态留言公告/springboot/javaWEB/J2EE/MYSQL数据库/vue前后分离小程序 目录 功能图 界面展示 开发目标 开发背景意义 开发意义 开发目的 项目概述 技术选型与理由 系统设计与功能实现 项目可执行性分析 系统架构需求 性能需…...
人工智能辅助的神经康复
人工智能辅助的神经康复是通过应用人工智能(AI)技术来改善神经系统损伤患者的康复过程。此领域结合了深度学习、数据分析和机器人技术,旨在提升康复效果、个性化治疗方案和监测进展。以下是该领域的关键组成部分和应用: 1. 康复评…...
KKT实际运用 -MATLAB
FMINCON函数可以很方便的求出:fun:目标函数,即需要最小化的函数,输入参数为向量x,输出为标量f(x)。x0:初始点,即求解过程的起始点,可以是标量、向量或矩阵。A和b:线性不等…...
php在线相册
1、将静态页面效果完成 解压到www里 整个数据 暂时是错误的 建立连接密码为root 运行sql文件 右键根目录刷新 刷新后成功 开始 测试 如果需要上传照片,点击创建相册,选择上传文件,选择文件后退出 导入alumbenew2 2.提交表单方式 3.利用ph…...
Xcode手动安装SDK模拟器
1.下载SDK模拟器&Xcode SDK和Xcode官方下载地址 2.下载好后使用命令将SDK导入到Xcode中如下命令 注:我是在/Applications 目录下执行的命令,模拟其地址直接拖拽过来 sudo xcode-select -s Xcode.app xcodebuild -runFirstLaunch xcodebuild -imp…...
Docker安装consul + go使用consul + consul知识
1. 什么是服务注册和发现 假如这个产品已经在线上运行,有一天运营想搞一场促销活动,那么我们相对应的【用户服务】可能就要新开启三个微服务实例来支撑这场促销活动。而与此同时,作为苦逼程序员的你就只有手动去 API gateway 中添加新增的这…...
JWT 漏洞 - 学习手册
0x01:JWT 前导知识 0x0101:JWT 详解 0x02:JWT 漏洞介绍 0x0201:JWT 漏洞介绍 0x03:JWT 挖掘思路 JWT 漏洞挖掘思路 - JWT Payload 敏感信息泄露 备注:通过泄露的 JWT Payload 获取用户的敏感信息&#…...
HTML【知识改变命运】03font 字体标签
题目:在页面上显示"北京"两个字,字体为微软雅黑,颜色为红色,大小为40xp; font标签可以修饰字体的大小,颜色,和字体 属性:color颜色,face字体,size大…...
集师专属知识付费小程序搭建 心理咨询小程序搭建
一、产品简介 集师SaaS知识付费软件,为知识创业者或商家提供一站式内容交付解决方案,助力商家搭建集品牌传播、商业变现和用户运营于一体的线上知识服务系统,覆盖全渠道经营场景,占据每个流量入口,使流量变现快速高效…...
https://www.aitoolpath.com/ 一个工具数据库,目前储存了有2000+各种工具。每日更新
AI 工具爆炸?别怕,这个网站帮你整理好了! 哇塞,兄弟们!AI 时代真的来了!现在各种 AI 工具跟雨后春笋似的,噌噌噌地往外冒。AI 写作、AI 绘画、AI 代码生成……简直是要逆天啊! 可是…...
科技的成就(六十三)
583、八小时工作制 最先提出这种理念的人竟然也是一名企业家,而且还是一名空想社会主义者。这名叫做罗伯特欧文的英国人,也凭借先进的人本管理理念成为了现代人事管理之父。 584、SDN(软件定义网络) "SDN(软件定…...
浅谈抗量子密码学:保护未来的数字安全
一、引言 随着量子计算机技术的发展,传统的加密算法面临前所未有的挑战。量子计算机利用量子位(qubits)的特性,能够在理论上比经典计算机更快地破解现有的加密系统。为了应对这一威胁,研究者们正在开发所谓的“抗量子…...
10款物联网开源嵌入式操作系统对比分析
摘要 本文对目前市场上广受欢迎的10款物联网开源嵌入式操作系统进行了深度对比分析,包括Huawei LiteOS、RT-Thread、AliOS Things等。通过探讨这些操作系统的实时性、可扩展性、特点、运行要求、开发社区活跃度和应用领域等方面,帮助开发者更好地理解它…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...
【生成模型】视频生成论文调研
工作清单 上游应用方向:控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...
基于Java+MySQL实现(GUI)客户管理系统
客户资料管理系统的设计与实现 第一章 需求分析 1.1 需求总体介绍 本项目为了方便维护客户信息为了方便维护客户信息,对客户进行统一管理,可以把所有客户信息录入系统,进行维护和统计功能。可通过文件的方式保存相关录入数据,对…...
【无标题】路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论
路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论 一、传统路径模型的根本缺陷 在经典正方形路径问题中(图1): mermaid graph LR A((A)) --- B((B)) B --- C((C)) C --- D((D)) D --- A A -.- C[无直接路径] B -…...
FFmpeg:Windows系统小白安装及其使用
一、安装 1.访问官网 Download FFmpeg 2.点击版本目录 3.选择版本点击安装 注意这里选择的是【release buids】,注意左上角标题 例如我安装在目录 F:\FFmpeg 4.解压 5.添加环境变量 把你解压后的bin目录(即exe所在文件夹)加入系统变量…...
【FTP】ftp文件传输会丢包吗?批量几百个文件传输,有一些文件没有传输完整,如何解决?
FTP(File Transfer Protocol)本身是一个基于 TCP 的协议,理论上不会丢包。但 FTP 文件传输过程中仍可能出现文件不完整、丢失或损坏的情况,主要原因包括: ✅ 一、FTP传输可能“丢包”或文件不完整的原因 原因描述网络…...
Python训练营-Day26-函数专题1:函数定义与参数
题目1:计算圆的面积 任务: 编写一个名为 calculate_circle_area 的函数,该函数接收圆的半径 radius 作为参数,并返回圆的面积。圆的面积 π * radius (可以使用 math.pi 作为 π 的值)要求:函数接收一个位置参数 radi…...
stm32进入Infinite_Loop原因(因为有系统中断函数未自定义实现)
这是系统中断服务程序的默认处理汇编函数,如果我们没有定义实现某个中断函数,那么当stm32产生了该中断时,就会默认跑这里来了,所以我们打开了什么中断,一定要记得实现对应的系统中断函数,否则会进来一直循环…...
MLP实战二:MLP 实现图像数字多分类
任务 实战(二):MLP 实现图像多分类 基于 mnist 数据集,建立 mlp 模型,实现 0-9 数字的十分类 task: 1、实现 mnist 数据载入,可视化图形数字; 2、完成数据预处理:图像数据维度转换与…...
篇章一 论坛系统——前置知识
目录 1.软件开发 1.1 软件的生命周期 1.2 面向对象 1.3 CS、BS架构 1.CS架构编辑 2.BS架构 1.4 软件需求 1.需求分类 2.需求获取 1.5 需求分析 1. 工作内容 1.6 面向对象分析 1.OOA的任务 2.统一建模语言UML 3. 用例模型 3.1 用例图的元素 3.2 建立用例模型 …...
