ZUCC_操作系统_Lab4线程的创建与管理
Lab 4线程的创建与管理
代码在页底
一.“hello world"单线程
#include<stdio.h>
int main(void){p_msg("hello ");p_msg("world\n"); return 0;
}void p_msg(char *s){int i;for(i=0;i<5;i++){printf("%s",s);fflush(stdout);sleep(1);}
}
1)此例程使用函数调用,理解函数调用是顺序执行的;
1.表现形式:先打印 hello,再打印world
2)理解fflush的作用
fflush():
1.单词释义:flush:冲洗 fflush:刷新缓冲区
2.作用:清洗读写缓冲区,立即物理写入输出缓冲区的数据
3.类型:fflush(stdin):刷新输入缓冲区,将输入缓冲区的数据丢弃fflush(stdout):刷新输出缓冲区,将输出缓冲区数据打印到输出设备 4.fflush(out)在源程序中作用:sleep(1)使得进程休眠,但是输出的字符串已经保留到缓冲区fflush(out)表示,尽管在休眠状况下,直接将缓冲区的数据打印到输出设备(显示屏)
5.实例:去掉fflush(out),实验结果:等五秒后电脑一次打印完”hello hello hello hello hello world“但是改写printf("hello")->printf("hello\n"),去掉fflush(out)仍就是每秒打印一次"hello"
二."hello world"多线程
易错提醒:
1.代码:pthread_create(&t1,NULL,p_msg,(void*)“hello”);
–>pthread_create(&t1,NULL,(void*)p_msg,(void*)“hello”);
2.编译: gcc -o t t.c -lpthread
-lpthread 链接线程库
#include<stdio.h>
#include<pthread.h>
void *p_msg(char *m){char *cp=(char*) m;int i;for(i=0;i<5;i++){printf("%s",m);fflush(stdout);sleep(1); }return NULL;
}
int main(void){pthread_t t1,t2;void *p_msg(char*);pthread_create(&t1,NULL,(void*)p_msg,(void*)"hello"); //注意:p_msg->(void *)p_msgpthread_create(&t2,NULL,(void*)p_msg,(void*)"world\n"); //pthread_join(t1,NULL);pthread_join(t2,NULL);return 0;
}
1)从输出内容,观察线程的并发执行
1. 源程序先书写调用“hello"的函数,再书写调用"world"函数但图中两次不同的输出显示,hello与world输出先后顺序不定,从而表示了两个线程是并发执行的
2. 并发:一定时间段内,多个程序交替使用CPU,但在任意时间点上只有一个程序在CPU上运行
2)运行例程,用ps -aL ,ps -efl,pstree -p观察线程信息
1.ps -aL
ps -aL
1.命令提示符:ps -a:选择除两个会话头之外的所有进程以及与终端无关的进程ps -L:显示线程ps -aL:显示除两个会话头之外的所有进程以及与终端无关的进程的线程
注意:注意大小写,不能用ps -alps -l表示长格式,不会显示线程相关信息(LWP值)2.线程相关信息:PID: process identification 进程标识符LWP: light weight process 轻量级进程(线程)TTY: teletypes 虚拟控制台,串口以及伪终端设备组成的终端设备CMD: command prompt 命令提示符
2.ps -efL
ps -efL
1.命令提示符:ps -e: 显示所有进程 与ps -A一致ps -f: 做清单列表ps -L: 显示线程ps -efL:显示所有进程与线程的清单列表2.线程相关信息:UID: User ID 用户IDPPID:Parent Process ID 父进程IDNLWP: The number of LWP 一个进程中线程的数量STIME Start Time 进程开始的时间
3.pstree -p
pstree -p
1.命令提示符:pstree -p: 显示进程树,在进程后括号内十进制表示PID
4.线程信息
线程信息:
1.命令提示符:gcc -lpthread: 链接线程函数库grep p1: 检索与p1相同的字符串的内容 ./p1 > p1.txt: 将p1执行的结果保存在p1.txt文件中./p1 |ps -L: 表示只统计执行p1时,相关的线程信息,"|"表示同时
5.遇到的小小问题:
gcc编译书上代码时会有提示出错,但仍可生成可执行文件?
解决方式:主函数:pthread_create(&t1,NULL,p_msg,(void*)"hello");->pthread_create(&t1,NULL,(void*)p_msg,(void*)"hello");
3)理解pthread_create()函数的作用
1.头文件:#include<pthread.h>
2.函数声明:int pthread_create(pthread_t *thread,pthread_attr_t *attr,void *(*start_routine)(void*),void *arg)
3.参数说明:pthread_t *thread: 指向线程的标识符,返回线程的IDpthread_attr_t *attr: 设置线程的属性,NULL表示默认属性void *(*start_routine)(void*): 线程运行函数的起始地址void *arg: 传给线程启动函数的参数
4.返回值:成功创建返回0,失败返回错误码
5.pthread_create()函数说明:是类UNIX操作系统(Unix,Linux,Mac OSX等)创建线程的函数创建线程,本质是确定调用该线程函数的入口点线程创建后,就开始运行相关线程函数
4)理解pthread_join()函数的作用;删除pthread_join函数后,观察例程运行
1.头文件:#include<pthread.h>
2.函数声明:int pthread_join(pthread_t thread,void **retval)
3.参数说明:pthread_t thread: 线程标识符,即线程ID thread:线 pthread:线程 void **retval: 用户定义的指针,用来储存被等待线程的返回值 retval:return value
4.返回值:成功等待返回0,失败返回错误值
5.pthread_join()函数说明:A线程调用B线程并执行pthread_join()后,A线程处于阻塞状态当B线程结束,pthread_join()函数返回,回收B线程的资源,A线程才继续执行
1.结果:1.不使用pthread_join()函数,主线程可以输出,但子线程不会有输出2.仅有一个pthread_join()函数,也会有一样的输出
2.删除一个pthread_join:
3.删除两个pthread_join:
4.相关链接:
详情链接
5)改写代码,用并发的父子进程输出同样的内容,体会进程和线程的差别
#include <stdio.h>
#include <stdlib.h>int main(void) {int i;int pid[5];for(i=0;i<5;i++)if((pid[i]=fork())==0){printf("hello");exit(0); } else{wait(0);printf("world\n");}return 0;
}
//注意:及时对子进程执行退出,防止其运行后面的进程
//注意:printf("world\n")中"\n"不能去掉,否则打印的次数不详
0 | 进程 | 线程 |
---|---|---|
1.创建 | fork() | pthread_create() |
2.等待终止 | wait() | pthread_join() |
3.包括 | 程序,数据,TCB | 程序,数据,堆栈,PCB |
4. | 调度,执行 | 调度,执行;拥有资源所有权 |
三.线程的并发执行和ID状态
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<sys/syscall.h>
#define num 3
void *PrintThread(void*);int main(void){int i,ret;pthread_t a_pthread;int thread[num];for(i=0;i<num;i++)thread[i]=i;for(i=0;i<num;i++){ret=pthread_create(&a_pthread,NULL,PrintThread,(void*)(&thread[i]));if(ret==0)printf("Pthread launch successfully!\n");}i=getchar();return 0;
}
void *PrintThread(void *n){int i;for(i=0;i<3;i++){printf("num is %d,pid is %d,lwp id is %d,tid is %d\n",*((int*)n),getpid(),syscall(SYS_gettid),pthread_self());sleep(1); }return NULL;
}
1)分析输出内容不连续的原因
由图可知,pid均为10537,lwp有所不同,表明运行的所有线程均在同一进程下
多线程共用同一进程的资源
各线程在调用PrintThread()时每次输出后sleep(1)休眠1s,该线程进入阻塞态
在其休眠期间,其他线程由就绪态->运行态,执行程序并输出
2)删除sleep(1),观察变化;分析原因
现象:每个线程内容连续输出
原因:每个线程在创建后进入就绪态,按照系统调用依次执行各线程,无sleep()和pthread_join()使用,线程正常执行,不会中断,直到线程正常结束,由运行态->死亡态 待一个线程结束后,按系统调度将就绪态中一个线程执行,变为运行态
3)分析输出内容,理解线程各种ID的含义
1.相关信息:num : 创建的第几个线程pid : 进程IDlwp : 线程 (light weight process:轻量级进程(线程))tid : 线程ID (thread ID)(与lwp同表示线程,值不同)
2.内容分析:1.3个线程均成功创建 Pthread launch successfully!且仅有10538,10539,10540三个线程号2.共用一个进程 pid均为10537
四.POSIX互斥锁的使用
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<pthread.h>
#include<sys/syscall.h>
#include<sys/types.h>int gnum=0;
pthread_mutex_t mutex;
static void pthread_add2(void);
static void pthread_add3(void);int main(void){pthread_t p1=0;pthread_t p2=0;int ret=0;printf("main program is start,gnum is %d,pid is %d\n",gnum,getpid());pthread_mutex_init(&mutex,NULL);ret=pthread_create(&p1,NULL,(void*)pthread_add2,NULL);//ret=ret=pthread_create(&p2,NULL,(void*)pthread_add3,NULL);pthread_join(p1,NULL);pthread_join(p2,NULL);printf("main program is exited\n");return 0;
}
static void pthread_add2(void){int i;printf("This is pthread1,pid is %d,tid is %d,lwp id is %d \n",getpid(),pthread_self(),syscall(SYS_gettid));for(i=0;i<3;i++){
// pthread_mutex_lock(&mutex);gnum++;sleep(1);gnum++;printf("pthread_add 2 gnum is %d \n",gnum);
// pthread_mutex_unlock(&mutex); }pthread_exit(NULL); //**
}
static void pthread_add3(void){ //**int i;printf("This is pthread2,pid is %d,tid is %d,lwp id is %d \n",getpid(),pthread_self(),syscall(SYS_gettid));for(i=0;i<4;i++){
// pthread_mutex_lock(&mutex);gnum++;sleep(1);gnum++;sleep(1);gnum++;printf("pthread_add 3 gnum is %d \n",gnum); //gum
// pthread_mutex_unlock(&mutex); //unlock}pthread_exit(0);
}
1)保留注释,运行程序,输出混乱,分析原因
1. 由图可知,进程ID为12615的进程创建了两个线程:12616,12617.两个线程并发执行调用进程的资源当一个线程调用pthread_add2并执行sleep(1)休眠时,该线程进入阻塞期另一个线程由就绪态,被调用执行成运行态,执行pthread_add3并输出依次两个线程不断运行->阻塞,就绪->运行交替,所以输出内容交替
2)删除sleep()函数,运行程序,混乱消失,思考是否从根本上解决问题
1. 没有图1所示,同一进程产生的两个线程由系统调度,依次顺序执行全部程序和输出,并结束但是,如图2所示,由于线程是并发执行的,线程的调度由操作系统执行,所以同一优先级线程调度非顺序,导致最后输出结果gnum不一致
3)恢复sleep()函数,删掉注释,使用互斥锁,观察结果,理解互斥锁的作用
结果:1.优先输出两行"This is pthread*,pid is *,tid is *,lwp id is *"2.完整的执行完一个线程后,再执行另一个线程
互斥锁的作用:1.使得同一优先级线程可以按照顺序执行各线程2.当一个线程调用互斥锁时,其他请求锁的线程会进入等待队列,待锁释放后,按照优先级获得锁
疑问:1.为啥优先输出两行"This is pthread*,pid is *,tid is *,lwp id is *"?
4)掌握互斥锁的基本操作:初始化,加锁(P操作),解锁(V操作)
1.全局变量: pthread_mutex_t mutex
2.动态互斥锁初始化: pthread_mutex_init(pthread_mutex_t *restrict mutex,pthread_mutexattr_t *restrict attr)
3.加锁: pthread_mutex_lock(pthread_mutex_t *mutex)
4.解锁: pthread_mutex_unlock(pthread_mutex_t *mutex)
5)输出内容不在混乱,分析互斥锁与删除sleep()不同的原因
1.相同:不用sleep()和用互斥锁都可以使得输出内容有序
2.不同:不用sleep(): 线程并发的一次执行完成.A线程,B线程输出先后顺序不一定用互斥锁: 线程A先创建,先开启互斥锁,线程B创建时申请锁的使用进入等待队列只有线程A释放锁后,线程B才能开始执行.A.B线程输出顺序一定
6)使用互斥锁修改例程3,使得输出结果不交错
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<sys/syscall.h>
#define num 3
void *PrintThread(void*);
pthread_mutex_t mutex;int main(void){int i,ret;pthread_t a_pthread;int thread[num];pthread_mutex_init(&mutex,NULL);for(i=0;i<num;i++)thread[i]=i;for(i=0;i<num;i++){ret=pthread_create(&a_pthread,NULL,PrintThread,(void*)(&thread[i]));if(ret==0)printf("Pthread launch successfully!\n");}i=getchar();return 0;
}
void *PrintThread(void *n){int i;for(i=0;i<3;i++){pthread_mutex_lock(&mutex);printf("num is %d,pid is %d,lwp id is %d,tid is %d\n",*((int*)n),getpid(),syscall(SYS_gettid),pthread_self());sleep(1); pthread_mutex_unlock(&mutex);}return NULL;
}
五.POSIX互斥锁
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<errno.h>
#include<unistd.h>int myglobal=10;
pthread_mutex_t mutex;
void *thread_function(void *arg){int i,j; //i,j不能混用for(i=0;i<20;i++){
// pthread_mutex_lock(&mutex);j=myglobal;j++;printf(".");fflush(stdout);usleep(1000);myglobal=j;
// pthread_mutex_unlock(&mutex);}return NULL;
}
int main(void){int i;pthread_t mythread=0;pthread_mutex_init(&mutex,NULL);if(pthread_create(&mythread,NULL,thread_function,NULL)){printf("error creating mythread\n");abort(); //**abort(0)}for(i=0;i<20;i++){
// pthread_mutex_lock(&mutex);myglobal++;printf("O");fflush(stdout);usleep(1000);
// pthread_mutex_unlock(&mutex);}if(pthread_join(mythread,NULL)){printf("error joining mythread\n");abort();}printf("\n myglobal equald is %d\n",myglobal); //**pthread_mutex_destroy(&mutex);exit(0);
}
1)观察程序加锁前后的变化,理解互斥锁的使用
1.abort()释义: abort:终止头文件:#include<stdlib.h>作用: 异常终止当前进程
2.互斥锁的使用:效果:使得线程依次顺序执行,避免并发导致的的输出混乱,单一输出的不完整
3.疑问:1.为啥先输出‘0’,再输出'.'?(为啥主函数的线程先拿到了锁)
六.代码区
1.“hello world"单线程
#include<stdio.h>
int main(void){p_msg("hello ");p_msg("world\n"); return 0;
}void p_msg(char *s){int i;for(i=0;i<5;i++){printf("%s",s);fflush(stdout);sleep(1);}
}
2."hello world"多线程
#include<stdio.h>
#include<pthread.h>
void *p_msg(char *m){char *cp=(char*) m;int i;for(i=0;i<5;i++){printf("%s",m);fflush(stdout);sleep(1); }return NULL;
}
int main(void){pthread_t t1,t2;void *p_msg(char*);pthread_create(&t1,NULL,(void*)p_msg,(void*)"hello"); //注意:p_msg->(void *)p_msgpthread_create(&t2,NULL,(void*)p_msg,(void*)"world\n"); //pthread_join(t1,NULL);pthread_join(t2,NULL);return 0;
}
2-1.用并发的父子进程输出同样的内容
#include <stdio.h>
#include <stdlib.h>int main(void) {int i;int pid[5];for(i=0;i<5;i++)if((pid[i]=fork())==0){printf("hello");exit(0); } else{wait(0);printf("world\n");}return 0;
}
//注意:及时对子进程执行退出,防止其运行后面的进程
//注意:printf("world\n")中"\n"不能去掉,否则打印的次数不详
3.线程的并发执行和ID状态
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<sys/syscall.h>
#define num 3
void *PrintThread(void*);int main(void){int i,ret;pthread_t a_pthread;int thread[num];for(i=0;i<num;i++)thread[i]=i;for(i=0;i<num;i++){ret=pthread_create(&a_pthread,NULL,PrintThread,(void*)(&thread[i]));if(ret==0)printf("Pthread launch successfully!\n");}i=getchar();return 0;
}
void *PrintThread(void *n){int i;for(i=0;i<3;i++){printf("num is %d,pid is %d,lwp id is %d,tid is %d\n",*((int*)n),getpid(),syscall(SYS_gettid),pthread_self());sleep(1); }return NULL;
}
4.POSIX互斥锁的使用
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<pthread.h>
#include<sys/syscall.h>
#include<sys/types.h>int gnum=0;
pthread_mutex_t mutex;
static void pthread_add2(void);
static void pthread_add3(void);int main(void){pthread_t p1=0;pthread_t p2=0;int ret=0;printf("main program is start,gnum is %d,pid is %d\n",gnum,getpid());pthread_mutex_init(&mutex,NULL);ret=pthread_create(&p1,NULL,(void*)pthread_add2,NULL);//ret=ret=pthread_create(&p2,NULL,(void*)pthread_add3,NULL);pthread_join(p1,NULL);pthread_join(p2,NULL);printf("main program is exited\n");return 0;
}
static void pthread_add2(void){int i;printf("This is pthread1,pid is %d,tid is %d,lwp id is %d \n",getpid(),pthread_self(),syscall(SYS_gettid));for(i=0;i<3;i++){
// pthread_mutex_lock(&mutex);gnum++;sleep(1);gnum++;printf("pthread_add 2 gnum is %d \n",gnum);
// pthread_mutex_unlock(&mutex); }pthread_exit(NULL); //**
}
static void pthread_add3(void){ //**int i;printf("This is pthread2,pid is %d,tid is %d,lwp id is %d \n",getpid(),pthread_self(),syscall(SYS_gettid));for(i=0;i<4;i++){
// pthread_mutex_lock(&mutex);gnum++;sleep(1);gnum++;sleep(1);gnum++;printf("pthread_add 3 gnum is %d \n",gnum); //gum
// pthread_mutex_unlock(&mutex); //unlock}pthread_exit(0);
}
4-1.使用互斥锁修改例程3,使得输出结果不交错
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<sys/syscall.h>
#define num 3
void *PrintThread(void*);
pthread_mutex_t mutex;int main(void){int i,ret;pthread_t a_pthread;int thread[num];pthread_mutex_init(&mutex,NULL);for(i=0;i<num;i++)thread[i]=i;for(i=0;i<num;i++){ret=pthread_create(&a_pthread,NULL,PrintThread,(void*)(&thread[i]));if(ret==0)printf("Pthread launch successfully!\n");}i=getchar();return 0;
}
void *PrintThread(void *n){int i;for(i=0;i<3;i++){pthread_mutex_lock(&mutex);printf("num is %d,pid is %d,lwp id is %d,tid is %d\n",*((int*)n),getpid(),syscall(SYS_gettid),pthread_self());sleep(1); pthread_mutex_unlock(&mutex);}return NULL;
}
5.POSIX互斥锁
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<errno.h>
#include<unistd.h>int myglobal=10;
pthread_mutex_t mutex;
void *thread_function(void *arg){int i,j; //i,j不能混用for(i=0;i<20;i++){
// pthread_mutex_lock(&mutex);j=myglobal;j++;printf(".");fflush(stdout);usleep(1000);myglobal=j;
// pthread_mutex_unlock(&mutex);}return NULL;
}
int main(void){int i;pthread_t mythread=0;pthread_mutex_init(&mutex,NULL);if(pthread_create(&mythread,NULL,thread_function,NULL)){printf("error creating mythread\n");abort(); //**abort(0)}for(i=0;i<20;i++){
// pthread_mutex_lock(&mutex);myglobal++;printf("O");fflush(stdout);usleep(1000);
// pthread_mutex_unlock(&mutex);}if(pthread_join(mythread,NULL)){printf("error joining mythread\n");abort();}printf("\n myglobal equald is %d\n",myglobal); //**pthread_mutex_destroy(&mutex);exit(0);
}
ZUCC_操作系统_Lab4线程的创建与管理相关推荐
- Java:使用Executors创建和管理线程
http://zhangjunhd.blog.51cto.com/113473/70068/ 1. 类 Executors 此类中提供的一些方法有: 1.1 public static Executo ...
- C++多线程并发(一)--- 线程创建与管理
文章目录 前言 一.何为并发 1.1 并发与并行 1.2 硬件并发与任务切换 1.3 多线程并发与多进程并发 二.如何使用并发 2.1 为什么使用并发 2.2 在C++中使用并发和多线程 三.C++线 ...
- java 线程的创建和执行_线程管理(一)线程的创建和运行
声明:本文是< Java 7 Concurrency Cookbook>的第一章, 作者: Javier Fernández González 译者:郑玉婷 校对:欧振聪 线程的创建和运行 ...
- 线程管理(七)守护线程的创建和运行
声明:本文是< Java 7 Concurrency Cookbook >的第一章, 作者: Javier Fernández González 译者:郑玉婷 校对:方腾飞 守护线程的创建 ...
- Android官方开发文档Training系列课程中文版:线程执行操作之创建多线程管理器
原文地址:http://android.xsoftlab.net/training/multiple-threads/create-threadpool.html 上节课我们学习了如何定义一个任务.如 ...
- C11头文件threads.h声明了创建和管理线程,信号,条件变量的函数
作者Danny Kalev 是通过以色列系统分析师协会认证的系统分析师, 并且是专攻C++的软件工程师. Kalev 写了多本C++的书籍,同时给不同的软件开发者站点投搞C++文章. 他是C++标准委 ...
- 操作系统实验一:线程的创建与撤销
实验一:线程的创建与撤销 2.1.1 实验目的 (1)熟悉Windows系统提供的线程创建与撤销系统调用. (2)掌握Windows系统环境下线程的创建与撤销方法. 2.1.2 实验准备知识 1.线程 ...
- 操作系统4小时速成:进程管理占考试40%,进程状态,组织,通信,线程拥有调度,进程拥有资源,进程和线程的区别
操作系统4小时速成:进程管理占考试40%,进程状态,组织,通信,线程拥有调度,进程拥有资源,进程和线程的区别 2022找工作是学历.能力和运气的超强结合体,遇到寒冬,大厂不招人,可能很多算法学生都得去 ...
- 操作系统实验一、线程的创建与撤销
实验一:线程的创建与撤销 一.实验目的 (1)熟悉windows系统提供的线程创建与撤销系统调用. (2)掌握windows系统环境下线程的创建与撤销方法. 二.实验准备 线程的概念 (1)线程(th ...
最新文章
- OpenCV+python:像素运算
- 欧盟发布《人工智能道德准则》:「可信赖 AI」才是 AI 的指路明灯
- 赤兔四足机器人的作用_跑得快,打不死!清华大学开发“小强”机器人,壮汉狂踩也挡不住前进步伐...
- PAT甲级1155 Heap Paths (30 分):[C++题解]堆、堆的遍历、树的遍历、dfs输出路径、完全二叉树建树
- python对excel数据更改_利用python对excel中一列的时间数据更改格式代码示例
- java 简单阻塞队列,制作一个简单的任务队列(使用阻塞队列)
- Sql Server的艺术(二) SQL复杂条件搜索
- iOS越狱开发theOS搭建
- TRF7970A 天线
- errors collectiions
- 3D材质管理软件Adobe Substance 3D Sampler中文版
- ubuntu 设置静态路由_ubuntu 配置静态路由
- 数字孪生城市可视化运营管理系统 智慧城市解决方案
- LeetCode 105. 从前序与中序遍历序列构造二叉树(dfsdfs、边界判定情况、做一题送一题)
- 【冰爪游戏】MC教程 —— 自定义皮肤
- 使用POI 删除批注
- 快速制作一个chrome插件
- 润物无声因挚爱,育人无痕待花开
- nginx实现路由转发
- 记一次IIS发布网站导致系统时常跳入登录页面的问题解决
热门文章
- 云原生社区 meetup 第四期广州站报名中
- 视频教程-Linux运维高薪课程-Linux
- 液晶面板里面有些什么配件_一张图看懂液晶面板内部结构,竟如此复杂
- 移动网络的切换、重选和重定向
- wifi信号衰减与距离关系_wifi无线信号传输衰减和距离的关系公式[室内定位]
- tomcat报错405
- MCR和MRC汇编指令
- 如何相对高效解决代码测评、训练过程中遇到的 Bug
- 第十六届智能车竞赛全国总决赛究竟该怎么举办讨论中的“混沌”现象
- 3小时快速入门html5+css(2022)