首页
关于
留言
接口
搜索
首页
登录
登录
搜索
KAKA 梦很美
累计撰写
47
篇文章
累计收到
0
条评论
首页
栏目
首页
登录
页面
首页
关于
留言
接口
基础原理
置顶
数据结构与算法之稀疏数组
概念 当一个数组中存储了大量为0或者大量相同的数时, 为了缩小程序的规模, 我们可以使用稀疏数组来保存该数组。 处理方式是 通过记录数组一共有几行几列, 有多少个不同的值, 把具有不同值的元素的 行列值 记录在一个小规模数组(稀疏数组)中, 从而缩小程序的规模。 如果不太好理解,这里就举个最经典的五子棋场景。在编写一个五子棋的程序当中,要实现存盘退出和续上盘的功能。如下图所示: 存在一个棋盘, 退出保存的时候应该怎么存储呢? 使用一个二维数组, 1表示红棋, 2表示黑棋, 0表示没有下过的棋。 因为该二维数组的大部分值都是默认的0, 因此记录了很多没有意义的数据, 这个时候就可以使用到 稀疏数组。 实现思路 步骤一 创建二维数组作为源数据, 我们看下源数据效果: 步骤二 遍历原始的二维数组, 将源数据中的有效数据转换为稀疏数组, 并把总行数、总列数和默认值也作为一个元素写入到稀疏数组第一个元素位置。可以看到, 实际有效数据有7个, 但是会产生8个元素, 用来记录总行总列。 步骤三 将稀疏数组复原为源数据, 先单独处理第一行, 创建原始的默认二维数组, 然后再读取稀疏数组中的其他行数据并赋值给二维数组即可。 废话不多说, 直接上代码(Go语言编写) package main import ( "fmt" ) /******* 稀疏数组 - 五子棋案例 *******/ const ( ROW = 11 COL = 11 ) var ( classmap [ROW][COL]int ) type nodeval struct { row int col int val interface{} } func main() { // 输出原始数据 originalData() // 转换为稀疏数组存档 sparsearray := transformationSparseArrayStorage() // 转换为原始数据 transformationOriginalData(sparsearray) } // 原始数据 func originalData() { classmap[1][3] = 2 classmap[2][3] = 1 classmap[5][2] = 1 classmap[6][8] = 2 classmap[7][3] = 1 classmap[4][5] = 1 classmap[6][6] = 2 // 查看数据 for _, v := range classmap { for _, v1 := range v { fmt.Printf("%d\t", v1) } fmt.Println() // 换行输出, 避免影响数据显示 } // 输出: /** 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 2 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 */ } // 转换为稀疏数组存档 func transformationSparseArrayStorage() []nodeval { // 转为稀疏数组 (这里使用切片实现) var sparsearray []nodeval // 创建数据模型 sparsearray = append(sparsearray, nodeval{ row: ROW, col: COL, val: 0, }) // 存档 for row, val := range classmap { for col, val1 := range val { if val1 == 0 { continue } // 加入到切片 sparsearray = append(sparsearray, nodeval{ row: row, col: col, val: val1, }) } } fmt.Println(sparsearray) // 此数据可存档入文件、缓存、数据库等, 输出: [{11 11 0} {1 3 2} {2 3 1} {4 5 1} {5 2 1} {6 6 2} {6 8 2} {7 3 1}] return sparsearray } // 转换为原始数据 func transformationOriginalData(sparsearray []nodeval) { var arr [][]int for key, nv := range sparsearray { var v int switch nv.val.(type) { case int: v = nv.val.(int) default: } if key == 0 { // 制作原始表格初始数据 for i := 0; i < nv.row; i++ { var temp []int for j := 0; j < nv.col; j++ { temp = append(temp, v) } arr = append(arr, temp) } // fmt.Println(arr) /** [ [0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0] ] */ } else { // 棋盘数组内容赋值 arr[nv.row][nv.col] = v } } // 输出数据 for _, v := range arr { for _, v1 := range v { fmt.Printf("%d\t", v1) } fmt.Println() // 换行输出, 避免影响数据显示 } /** 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 2 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 */ }
2023年-3月-1日
187 阅读
0 评论
基础原理
置顶
HTTP 常见面试题
在面试过程中,HTTP 被提问的概率还是比较高的。目前提及最多的问题大概如下: HTTP 基本概念 HTTP 是什么? HTTP 是 超文本传输协议,也就是 HyperText Transfer Protocol。 能否详细解释「超文本传输协议」? HTTP的名字 「超文本协议传输」,它可以拆成三个部分: 1.「协议」 在生活中,我们也能随处可见「协议」,例如: 刚毕业时会签一个「三方协议」; 找房子时会签一个「租房协议」; 生活中的协议,本质上与计算机中的协议是相同的,协议的特点: 「协」 字,代表的意思是 必须有两个以上的参与者。例如三方协议里的参与者有三个:你、公司、学校三个;租房协议里的参与者有两个:你和房东。 「议」 字,代表的意思是 对参与者的一种行为约定和规范。例如三方协议里规定试用期期限、毁约金等;租房协议里规定租期期限、每月租金金额、违约如何处理等。 针对 HTTP 协议,我们可以这么理解。 HTTP 是一个用在 计算机世界里的协议。它使用计算机能够理解的语言确立了一种计算机之间交流通信的规范(两个以上的参与者),以及相关的各种控制和错误处理方式(行为约定和规范)。 2.「传输」 所谓的「传输」,很好理解,就是把一堆东西从 A 点搬到 B 点,或者从 B 点 搬到 A 点。 别轻视了这个简单的动作,它至少包含两项重要的信息。 HTTP 协议是一个双向协议。 我们在上网冲浪时,浏览器是请求方 A ,百度网站就是应答方 B。双方约定用 HTTP 协议来通信,于是浏览器把请求数据发送给网站,网站再把一些数据返回给浏览器,最后由浏览器渲染在屏幕,就可以看到图片、视频了。 数据虽然是在 A 和 B 之间传输,但 允许中间有中转或接力。 就好像第一排的同学想传递纸条给最后一排的同学,那么传递的过程中就需要经过好多个同学(中间人),这样的传输方式就从「A < --- > B」,变成了「A <-> N <-> M <-> B」。 而在 HTTP 里,需要中间人遵从 HTTP 协议,只要不打扰基本的数据传输,就可以添加任意额外的东西。 针对传输,我们可以进一步理解了 HTTP。 HTTP 是一个在计算机世界里专门用来 在两点之间传输数据的约定和规范。 3.「超文本」 HTTP 传输的内容是「超文本」。 我们先来理解「文本」,在互联网早期的时候只是简单的字符文字,但现在「文本」的涵义已经可以扩展为图片、视频、压缩包等,在 HTTP 眼里这些都算作「文本」。 再来理解「超文本」,它就是 超越了普通文本的文本,它是文字、图片、视频等的混合体,最关键有超链接,能从一个超文本跳转到另外一个超文本。 HTML 就是最常见的超文本了,它本身只是纯文字文件,但内部用很多标签定义了图片、视频等的链接,再经过浏览器的解释,呈现给我们的就是一个文字、有画面的网页了。 OK,经过了对 HTTP 里这三个名词的详细解释,就可以给出比「超文本传输协议」这七个字更准确更有技术含量的答案: HTTP 是一个在计算机世界里专门 在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。 那「HTTP 是用于从互联网服务器传输超文本到本地浏览器的协议 ,这种说法正确吗? 这种说法是 不正确 的。因为也可以是「服务器< -- >服务器」,所以采用 两点之间 的描述会更准确。
2023年-2月-2日
182 阅读
0 评论
基础原理
2023-1-12
数据结构与算法之冒泡排序
概念 冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。 原理 比较两个相邻的元素,将值大的元素交换到右边。如果遇到相等的值不进行交换,那这种排序方式是稳定的排序方式。 思路 依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面 1) 比较第1和第2个数,将小数放在前面,将大数放在后面。 2) 比较第2和第3个数,将小数放在前面,将大数放在后面。 ...依此类推 3) 当比较到第一轮最后两个数时候,将小数放在前面,将大数放在后面;其中最后一个数肯定是整个数组中的最大数,所以在接下来的轮次中是不需要参与比较的。 4) 在上面一轮比较完成后,需要重复如上步骤,每一轮比较次数依次减少,并且每一轮过后都会有一个不需要参与下轮比较的值,直至全部排序完成。 图解冒泡排序 需要参与冒泡排序的数组: [24, 0, 300, 69, 80, 4, 293, 57, 13] 算法分析可以看到: 逻辑处理: N个数字要排序完成,总共进行 N-1 轮次排序,每一次的排序次数为(N-i)次。所以可以用双重循环语句,外层控制循环总轮次,内层控制每一轮的循环次数。 规则原理: 每进行一轮次排序,就会少比较一次,因为每进行一轮排序都会找出一个较大值。如上例: 第一轮比较之后,排在最后的一个数一定是最大的一个数,第二趟排序的时候,只需要比较除了最后一个数以外的其他的数,同样也能找出一个最大的数排在参与第二轮比较的数后面,第三轮比较的时候,只需要比较除了最后两个数以外的其他的数,以此类推…… 也就是说,每进行一轮比较,每一轮少比较一次,一定程度上减少了算法的量。 时间复杂度: (1) 如果我们的数据正序,只需要走一轮即可完成排序。所需的比较次数C和记录移动次数M均达到最小值,即:Cmin=n-1; Mmin=0; 所以,冒泡排序最好的时间复杂度为O(n)。 (2) 如果很不幸我们的数据是反序的,则需要进行n-1轮次排序。每轮次排序要进行n-i次比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值。 冒泡排序总的平均时间复杂度为:O(n^2) [n的平方] , 时间复杂度和数据状况无关。 接下来用 Go 实现冒泡排序算法代码 package main import "fmt" /******* 冒泡排序法 *******/ func main() { var arr = [9]int{24, 0, 300, 69, 80, 4, 293, 57, 13} for j := 0; j < len(arr) - 1; j++ { var oldArr = arr var nextTotalLen = len(arr) - j - 1 for i := 0; i < len(arr); i++ { if i < nextTotalLen { var nextKey = i + 1 if arr[i] > arr[nextKey] { // 前面值大于后面值时, 位置置换 var nextVal = arr[i] arr[i] = arr[nextKey] arr[nextKey] = nextVal } } } // 值完全相同则不需要继续循环下去了 if oldArr == arr { break } } fmt.Println(arr) /** 输出: [0 4 13 24 57 69 80 293 300] */ }
2023年-1月-12日
183 阅读
0 评论
基础原理
2022-2-17
单体架构和微服务系统架构优缺点
随着业务的发展我们的项目从简单的单体结构逐渐的演化成微服务结构, 我们为什么要拆分成微服务呢?那我们就来说说微服务和单体架构的优缺点吧。 单体架构 单体架构优点 部署容易: 如 php 写的项目,只要一个文件夹复制到支持 php 的环境就可以了,java 只需要一个 jar 包 测试容易: 我们整体项目只要改了一个地方马上就可以测试得出结果 负载均衡就可以解决: 快速部署多个一模一样的项目在不同的机器运行分流 技术单一: 项目不需要复杂的技术栈,往往一套熟悉的技术栈就可以完成开发 用人成本低: 单个程序员可以完成业务接口及数据库的整个流程 单体架构缺点 部署的问题: 对于 php 来说这点还好,但是对于 java 的项目来说,我们需要重新打包整个项目耗费的时间是很长的 代码维护: 由于所有的代码都写在一个项目里面,要想要修改某一个功能点那么需要对项目的整体逻辑和设计有较深的理解,否则代码耦合严重,导致维护难,特别对于新入职的员工来说这将是最容易出现问题的地方 开发效率低: 随着项目需求的不断改变和新的功能新增,老旧的代码又不敢随便删除,导致整个项目变得笨重,这将会增加你阅读代码的时间 系统启动慢: 一个进程包含了所有的业务逻辑,涉及到的启动模块过多,导致系统的启动时间周期过长 可伸缩性差: 系统的扩容只能对这个应用进行扩容,不能做到对某个功能点进行扩容。在高并发的情况下,我们往往不是整个项目的每一个功能都处于高流量高请求的情况下的,很多时候都是某一个功能模块使用的人数比较多,在单体结构下我们没有办法针对单个功能实现分布式扩展,必须整个项目一起部署 系统错误隔离性差: 可用性差,任何一个模块的错误均可能造成整个系统的宕机 线上问题修复周期长: 任何一个线上问题修复需要对整个应用系统全面升级 微服务架构 在2014年被提出,现在国内很多公司已经使用,微服务是一种架构设计,并不是说什么框架或者代替什么。微服务做的事情是按照项目颗粒度进行服务的拆分,把模块单独拿出来做成每一个单独的小项目。微服务的主要特点有:每一个功能模块是一个小项目、独立运行在不同进程或者机器上、不同功能可以又不同的人员开发独立开发不松耦合、独立部署不需要依赖整体项目就可以启动单个服务、分布式管理。每一个服务只要做好自己的事情就好了。在设计微服务的时候还需要考虑到数据库的问题,是所有微服务使用共同一个数据库还是每一个服务单个数据库。 服务调用 每个服务间也可以相互调用(即:双向流调用), 可以通过 RPC、GRPC 这类远程调用方式。调用者与服务之间的通讯,传输协议可基于 TCP、UDP 或者 HTTP 实现,但是更推荐选择 TCP。 例如调用者需要调用商品的服务就可以通过 RPC 或者 RESTful API 来调用,那么 RPC 调用和 RESTful API 两者之间的区别在哪呢? TCP 支持长连接,当调用服务的时候不需要每次都进行三次握手才实现。从性能和网络消耗来说 RPC 都具备了很好的优势 RESTful API 基于 HTTP 的,也就是说每次调用服务都需要进行三次握手建立起通信才可以实现调用,当我们的并发量高的时候这就会浪费很多带宽资源 服务对外的话采用 RESTful API 会比 RPC 更具备优势,因此看自己团队的服务是对内还是对外 RPC 最主要的作用就是用于服务调用 微服务架构优点 易于开发和维护: 一个微服务只会关注一个特定的业务功能,所以他的业务清晰,代码量少,开发和维护单个微服务相当简单,而整个应用是若干个微服务构建而成的,所以整个应用也被维持在一个可控状态。拆分业务,把整体大项目分割成不同小项目运行在不同进程或者机器上实现数据隔离 技术栈不受限: 每个服务可以由不同的团队或者开发者进行开发,外部调用人员不需要操心具体怎么实现的,只需要类似调用自己方法一样或者接口一样按照服务提供者给出来的参数传递即可 独立部署: 每一个服务独立部署,部署一个服务不会影响整体项目,如果部署失败最多是这个服务的功能缺失,并不影响其他功能的使用 按需部署: 针对不同的需求可以给不同的服务自由扩展服务器,根据服务的规模部署满足需求的实例 局部修改容易部署: 当一个服务有新需求或者其他修改,不需要修改整体项目只要管好自己的服务就好了 单个微服务启动较快: 单个微服务代码量较少,所以启动会比较快 按需收缩: 可根据需求,实现细粒度的扩展,例如:系统中的某个微服务遇到了瓶颈,可以结合这个微服务的业务特点,增加内存,升级CPU或者增加节点 可以承受高并发 微服务架构缺点 运维要求较高: 更多的服务意味着更多的运维投入,在单体架构中,只需要保证一个应用的正常运行,而在微服务中,需要保证几十甚至几百个服务正常运行与协作,这给运维带来了很大的挑战 分布式固有的复杂性: 使用微服务构建的是分布式系统,对于一个分布式系统,系统容错,网络延迟等都会带来巨大的挑战 接口调整成本高: 微服务之间通过接口进行通信,如果修改某一微服务API,则所有使用该接口的微服务都需要调整
2022年-2月-17日
195 阅读
0 评论
基础原理
2021-1-10
缓存穿透、缓存击穿、缓存雪崩 解决方案
前言 设计一个缓存系统,就不得不要考虑:缓存穿透、缓存击穿与缓存雪崩。 缓存穿透 缓存穿透 是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。 缓存击穿 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常 “热点” 的数据。这个时候,需要考虑一个问题:缓存被 “击穿” 的问题,这个和·缓存雪崩·的区别在于这里针对某一 key 缓存,前者则是很多 key。 缓存在某个时间点过期的时候,恰好在这个时间点对这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。 使用互斥锁 (mutex key) 业界比较常用的做法,使用 mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去 load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如 Redis 的 SETNX 或者 Memcache 的 ADD)去 set 一个 mutex key,当操作返回成功时,再进行 load db 的操作并回设缓存;否则,就重试整个 get 缓存的方法。 SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。 "提前" 使用互斥锁(mutex key) 在 value 内部设置1个超时值 (timeout1), timeout1 比实际的 memcache timeout (timeout2) 小。当从 cache 读取到 timeout1 发现它已经过期时候,马上延长 timeout1 并重新设置到 cache。然后再从数据库加载数据并设置到 cache 中。 "永远不过期" 这里的 “永远不过期” 包含两层意思: (1) 从 Redis 上看,确实没有设置过期时间,这就保证了,不会出现热点 key 过期问题,也就是“物理”不过期。 (2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期 从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。 资源保护 采用 netflix 的 hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。 缓存雪崩 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。 缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就是将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 总结 针对业务系统,永远都是具体情况具体分析,没有最好,只有最合适。最后,对于缓存系统常见的缓存满了和数据丢失问题,需要根据具体业务分析,通常我们采用LRU策略处理溢出,Redis的RDB和AOF持久化策略来保证一定情况下的数据安全。
2021年-1月-10日
154 阅读
0 评论
基础原理
2019-2-22
网络编程-协议和网络应用程序设计模式
聊聊什么是协议? 从应用程序角度出发,协议可以理解为 “规则”,是数据传输和数据解释的规则。 举个例子, A、B 双方欲传输文件。规定: 第一次,传输文件名,接收方接收到文件名,应答OK给传输方; 第二次,发送文件的尺寸,接收方接收到该数据再次应答一个OK; 第三次,传输文件内容。同样,接收方接收数据完成后应答OK表示文件内容接收成功。 由此,无论A、B之间传递何种文件,都是通过三次数据传输来完成。A、B之间形成了一个最简单的数据传输规则。双方都按此规则发送、接收数据。A、B之间达成的这个相互遵守的规则即为协议。 这种仅在A、B之间被遵守的协议称之为原始协议。当此协议被更多的人采用,不断的增加、改进、维护、完善。最终形成一个稳定的、完整的文件传输协议,被广泛应用于各种文件传输过程中。该协议就成为一个标准协议。最早的FTP协议就是由此衍生而来。 TCP协议注重数据传输。HTTP协议着重于数据解释。 典型协议 TCP传输控制协议 (Transmission Control Protocol) 是一种 面向连接 的、可靠的 、基于 字节流 的传输层通信协议。 UDP用户数据报协议 (User Datagram Protocol) 是OSI参考模型中一种 无连接 传输层协议,提供 面向事务 、简单、不可靠信息 传送服务。 HTTP超文本传输协议 (Hyper Text Transfer Protocol) 是互联网上应用最为广泛的一种网络协议。 FTP文件传输协议 (File Transfer Protocol) IP协议 是 因特网互联协议(Internet Protocol) ICMP协议 是 Internet控制报文协议(Internet Control Message Protocol) 它是 TCP/IP协议 族的一个 子协议 ,用于在 IP主机、路由器之间传递控制消息。 IGMP协议 是 Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在 主机 和 组播路由器 之间。 ARP协议 是 正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。 RARP 是 反向地址转换协议,通过 MAC地址 确定 IP地址。 网络应用程序设计模式 C/S 模式 客户机 client / 服务器 server (需要在通讯两端各自部署客户机和服务器来完成数据通信。) 优点 : 协议选用灵活 (自定义协议, 协议由服务端和客户端商定即可) 缓存数据 (数据可缓存到本地, 如App应用 可缓存到手机) 缺点 : 对用户安全构成威胁 (需要将客户端安插到用户主机上) 开发工作量大, 调试困难 (需要客户端和服务端的开发人员, 且调试时候也是如此) B/S 模式 浏览器 browser / 服务器 server (只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输) 优点 : 开发工作量小, 调试方便 (只需开发服务器端即可, 且调试时候只需要PC端浏览器即可) 跨平台 (采用浏览器显示数据, 所以不受平台限制) 缺点 : 使用第三方浏览器, 网络应用受限制 缓存数据不尽人意 (由于客户端没有放到对方主机上, 从而传输数据量受到限制) 采用标准HTTP协议进行通信 (必须与浏览器一样, 协议选择不灵活)
2019年-2月-22日
174 阅读
0 评论
基础原理
2018-5-27
深入理解串行、并行、并发的区别
理解并发、并行的例子 先举例子来理解这2个概念的区别。 老师让两个同学去办公室谈话。如果这两同学(进程)是并列跨过办公室门(CPU)的,那么就是并行。如果同学A先进同学B后进入(或者先B后A),或者两人并列同时进入,但是在办公室外的路人甲(用户)看来,同学A和同学B同时都在办公室内,这是并发。 其实这个例子不合理,因为真正的并行是多核CPU下的概念,但上面这个简单的例子非常有助于理解。 如果举例要精确一点,那么大概是这样的:进办公室有两个门(两CPU),如果两同学分别从不同的门进入,不管先后性,两者互相独立,那么是并行;如果两同学不管以什么方式进入,在路人甲看来,他两同时都在办公室内,就是并发。 我不信到现在还不理解并发和并行。 并发和并行的理论性解释 为什么操作系统上可以同时运行多个程序而用户感觉不出来? 这是因为无论是单CPU还是多CPU,操作系统都营造出了可以同时运行多个程序的 假象。实际的过程操作系统对进程的调度以及CPU的快速上下文切换实现的:每个进程执行一会就先停下来,然后CPU切换到下个被操作系统调度到的进程上使之运行。因为切换的很快,使得用户认为操作系统一直在服务自己的程序。 再来解释并发就容易理解多了。 并发(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。但这不是重点,在描述并发的时候也不会去扣这种字眼是否精确,并发的重点在于它是一种现象。并发描述的是多进程同时运行的现象。但实际上,对于单核心CPU来说,同一时刻只能运行一个进程。所以,这里的"同时运行"表示的不是真的同一时刻有多个进程运行的现象,这是并行的概念,而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占CPU的,而是执行一会停一会。 所以,并发和并行的区别就很明显了。它们虽然都说是"多个进程同时运行",但是它们的"同时"不是一个概念。并行的"同时"是同一时刻可以多个进程在运行(处于running),并发的"同时"是经过上下文快速切换,使得看上去多个进程同时都在运行的现象,是一种OS欺骗用户的现象。 实际上,当程序中写下多进程或多线程代码时,这意味着的是并发而不是并行。并发是因为多进程/多线程都是需要去完成的任务,不并行是因为并行与否由操作系统的调度器决定,可能会让多个进程/线程被调度到同一个CPU核心上。只不过调度算法会尽量让不同进程/线程使用不同的CPU核心,所以在实际使用中几乎总是会并行,但却不能以100%的角度去保证会并行。也就是说,并行与否程序员无法控制,只能让操作系统决定。 再次注明,并发是一种现象,之所以能有这种现象的存在,和CPU的多少无关,而是和进程调度以及上下文切换有关的。 理解了概念,再来深入扩展下。 串行、并行和并发 任务描述 任务是将左边的一堆柴全部搬到右边烧掉,每个任务包括三个过程:取柴,运柴,放柴烧火。 这三个过程分别对应一个函数: func get { geting } func carry { carrying } func unload { unloading } 串行模式 串行表示所有任务都一一按先后顺序进行。串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。 和稍后所解释的并行相对比,串行是一次只能取得一个任务,并执行这个任务。 假设这堆柴需要运送4次才能运完,那么当写下的代码类似于下面这种时,那么就是串行非并发的模式: for (i = 0; i < 4; i++) { get() carry() unload() } 或者,将三个过程的代码全部集中到一个函数中也是如此: func task { geting carrying unloading } for (i = 0; i < 4; i++) { task() } 这两种都是串行的代码模式。画图描述: 并行模式 并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。 正如前面所举的两同学进办公室的例子,串行的方式下,必须1个同学进入后第二个同学才进入,队列长度为2,而并行方式下可以同时进入,队列长度减半了。 并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核CPU。 对于单进程/单线程,由于只有一个进程/线程在执行,所以尽管同时执行所取得的多个任务,但实际上这个进程/线程是不断的在多任务之间切换,一会执行一下这个,一会执行一下那个,就像是一个人在不同地方之间来回奔波。所以,单进程/线程的并行,效率比串行更低。 对于多进程/多线程,各进程/线程都可以执行各自所取得的任务,这是真正的并行。 但是,还需要考虑硬件层次上CPU核心数,如果只有单核CPU,那么在硬件角度上这单核CPU一次也只能执行一个任务,上面多进程/多线程的并行也并非真正意义上的并行。只有多核CPU,并且多进程/多线程并行,才是真正意义上的并行。 如下图,是多进程/多线程(2个工作者)的并行: 并发模式 并发表示多个任务同时都要执行的现象,更详细的概念前面已经说面的够具体了。 其实,很多场景下都会使用并发的概念。比如同时500个http请求涌向了web服务器,比如有时候说并发数是1000等。 有时候也将并发当成任务,比如500并发数意味着500个任务,表示的是在一个特定的时间段内(约定俗成的大家认为是1秒)可以完成500个任务。这500个任务可以是单进程/单线程方式处理的,这时表示的是并发不并行的模式(coroutine就是典型的并发不并行),即先执行完一个任务后才执行另一个任务,也可以是多进程/多线程方式处理的,这时表示的是并发且并行模式。 要解决大并发问题,通常是将大任务分解成多个小任务。很典型的一个例子是处理客户端的请求任务,这个大任务里面包含了监听并建立客户端的连接、处理客户端的请求、响应客户端。但基本上所有这类程序,都将这3部分任务分开了:在执行任何一个小任务的时候,都可以通过一些手段使得可以执行其它小任务,比如在处理请求的时候,可以继续保持监听状态。 由于操作系统对进程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。这可能会出现一些现象: 可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用队列或类似的数据结构来存放各个小任务的成果。比如负责监听的进程已经执行了多次,建立了多个连接,但是还没有调度到处理请求的进程去处理任何一个请求。 可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用或异步的方式,比如只有准备好产生了事件通知才执行某个任务。比如还没有和任何一个客户端建立连接时,就去执行了处理请求的进程。 可以多进程/多线程的方式并行执行这些小任务。也可以单进程/单线程执行这些小任务,这时很可能要配合多路复用才能达到较高的效率 看图非常容易理解: 上图中将一个任务中的三个步骤取柴、运柴、卸柴划分成了独立的小任务,有取柴的老鼠,有运柴的老鼠,有卸柴烧火的老鼠。 如果上图中所有的老鼠都是同一只,那么是串行并发的,如果是不同的多只老鼠,那么是并行并发的。 总 结 串行: 一次只能取得一个任务并执行这一个任务 并行: 可以同时通过多进程/多线程的方式取得多个任务,并以多进程或多线程的方式同时执行这些任务 注意点: 如果是单进程/单线程的并行,那么效率比串行更差 如果只有单核cpu,多进程并行并没有提高效率 从任务队列上看,由于同时从队列中取得多个任务并执行,相当于将一个长任务队列变成了短队列 并发: 是一种现象, 同时运行多个程序或多个任务需要被处理的现象。这些任务可能是并行执行的,也可能是串行执行的,和CPU核心数无关,是 操作系统进程调度和CPU上下文切换达到的结果。 解决大并发的一个思路是将大任务分解成多个小任务: 可能要使用一些数据结构来避免切分成多个小任务带来的问题 可以多进程/多线程并行的方式去执行这些小任务达到高效率 或者以单进程/单线程配合多路复用执行这些小任务来达到高效率
2018年-5月-27日
164 阅读
0 评论
基础原理