diff --git a/Python/网络编程/Select socket.md b/Python/网络编程/Select socket.md new file mode 100644 index 00000000..27572577 --- /dev/null +++ b/Python/网络编程/Select socket.md @@ -0,0 +1,58 @@ + +### 过程分析 +* 用户进程创建socket对象,拷贝监听的fd到内核空间,每一个fd会对应一张系统文件表,内核空间的fd响应到数据后,就会发送信号给用户进程数据已到; +* 用户进程再发送系统调用,比如(accept)将内核空间的数据copy到用户空间,同时作为接受数据端内核空间的数据清除,这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除) + +### 优点 +* 相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。 + +### 缺点 +* 首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。 +* 很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。 +* 如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异, +* 所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。 +* 其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。 + +### 代码实现 +``` +#服务端 +from socket import * +import select + +s=socket(AF_INET,SOCK_STREAM) +s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) +s.bind(('127.0.0.1',8081)) +s.listen(5) +s.setblocking(False) #设置socket的接口为非阻塞 +read_l=[s,] +while True: + r_l,w_l,x_l=select.select(read_l,[],[]) + print(r_l) + for ready_obj in r_l: + if ready_obj == s: + conn,addr=ready_obj.accept() #此时的ready_obj等于s + read_l.append(conn) + else: + try: + data=ready_obj.recv(1024) #此时的ready_obj等于conn + if not data: + ready_obj.close() + read_l.remove(ready_obj) + continue + ready_obj.send(data.upper()) + except ConnectionResetError: + ready_obj.close() + read_l.remove(ready_obj) + +#客户端 +from socket import * +c=socket(AF_INET,SOCK_STREAM) +c.connect(('127.0.0.1',8081)) + +while True: + msg=input('>>: ') + if not msg:continue + c.send(msg.encode('utf-8')) + data=c.recv(1024) + print(data.decode('utf-8')) +``` \ No newline at end of file diff --git a/Python/网络编程/非阻塞socket.md b/Python/网络编程/非阻塞socket.md new file mode 100644 index 00000000..837a54da --- /dev/null +++ b/Python/网络编程/非阻塞socket.md @@ -0,0 +1,46 @@ + +``` +#服务端 +from socket import * +import time +s=socket(AF_INET,SOCK_STREAM) +s.bind(('127.0.0.1',8080)) +s.listen(5) +s.setblocking(False) #设置socket的接口为非阻塞 +conn_l=[] +del_l=[] +while True: + try: + conn,addr=s.accept() + conn_l.append(conn) + except BlockingIOError: + print(conn_l) + for conn in conn_l: + try: + data=conn.recv(1024) + if not data: + del_l.append(conn) + continue + conn.send(data.upper()) + except BlockingIOError: + pass + except ConnectionResetError: + del_l.append(conn) + + for conn in del_l: + conn_l.remove(conn) + conn.close() + del_l=[] + +#客户端 +from socket import * +c=socket(AF_INET,SOCK_STREAM) +c.connect(('127.0.0.1',8080)) + +while True: + msg=input('>>: ') + if not msg:continue + c.send(msg.encode('utf-8')) + data=c.recv(1024) + print(data.decode('utf-8')) +``` \ No newline at end of file diff --git a/code_segment/xioayu2.cpp b/code_segment/xioayu2.cpp new file mode 100644 index 00000000..938ea30a --- /dev/null +++ b/code_segment/xioayu2.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include +using namespace std; + +class Solution{ + + public: + std::vector scoresort(std::vector names,std::vector scores){ + map m; + for(int i=0;i res; + for(auto beg=m.rbegin();beg!=m.rend();beg++){ + res.push_back(names[beg->second]); + } + return res; + } +}; + +int main(){ + + Solution s; + using namespace std; + vector names{"1","2","3"}; + vector scores{"1,2,3","3,4,5","6,7,8"}; + + vector res = s.scoresort(names,scores); + for(auto a:res){ + cout< 参考文献 +> * [Linux五种IO模型](https://www.cnblogs.com/cainingning/p/9556642.html) + > linux实现IO过程的方法有很多。这里只对五种模型进行介绍。如果要实现五种IO过程,在linux系统编程部分进行学习。 ## 0 I/O 模型 +### 背景介绍 +同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?这个问题其实不同的人给出的答案都可能不同,比如wiki,就认为asynchronous IO和non-blocking IO是一个东西。这其实是因为不同的人的知识背景不同,并且在讨论这个问题的时候上下文(context)也不相同。所以,为了更好的回答这个问题,我先限定一下本文的上下文。 + +本文讨论的背景是**Linux环境下的network IO**,也包括其他设备的IO过程。本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ”,Stevens在这节中详细说明了各种IO的特点和区别,如果英文够好的话,推荐直接阅读。Stevens的文风是有名的深入浅出,所以不用担心看不懂。本文中的流程图也是截取自参考文献。 + +### 概述 + * 一个输入操作通常包括两个阶段: - - 等待数据准备好 - - 从内核向进程复制数据 + - 等待数据准备好(Waiting for the data to be ready) + - 从内核向进程复制数据(Copying the data from the kernel to the process) * 对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。 @@ -44,7 +54,19 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * ![](image/2021-03-30-22-02-42.png) +* 非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。 + +非阻塞IO存在的问题 + +1. 循环调用recv()将大幅度推高CPU占用率;这也是我们在代码中留一句time.sleep(2)的原因,否则在低配主机下极容易出现卡机情况 +2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。 + + + + + ## 3 I/O 复用(事件驱动IO) +* IO multiplexing这个词可能有点陌生,但是如果我说select/epoll,大概就都能明白了。有些地方也称这种IO方式为事件驱动IO(event driven IO)。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。 * I/O多路复用。I/O指的是I/O事件(包括I/O读写、I/O异常等事件),多路指多个独立连接(或多个Channel),复用指多个事件复用一个控制流(线程或进程)。串起来理解就是很多个独立I/O事件的处理依赖于一个控制流。 * 主要是select、poll、epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对**多个IO端口进行监听**; * I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。 @@ -54,6 +76,7 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * ![](image/2021-03-30-22-02-51.png) + ## 4 信号驱动 I/O * 应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。 diff --git a/操作系统/image/2021-09-06-15-30-55.png b/操作系统/image/2021-09-06-15-30-55.png new file mode 100644 index 00000000..828cd731 Binary files /dev/null and b/操作系统/image/2021-09-06-15-30-55.png differ diff --git a/操作系统/image/2021-09-06-15-31-02.png b/操作系统/image/2021-09-06-15-31-02.png new file mode 100644 index 00000000..311bde95 Binary files /dev/null and b/操作系统/image/2021-09-06-15-31-02.png differ diff --git a/操作系统/image/2021-09-06-17-00-23.png b/操作系统/image/2021-09-06-17-00-23.png new file mode 100644 index 00000000..d8289705 Binary files /dev/null and b/操作系统/image/2021-09-06-17-00-23.png differ diff --git a/操作系统/附录12 并发编程.md b/操作系统/附录12 并发编程.md new file mode 100644 index 00000000..67e8f575 --- /dev/null +++ b/操作系统/附录12 并发编程.md @@ -0,0 +1,138 @@ +# 并发编程 + +## 1 并发概述 + +### 问题重述 + +1. 什么是并发, +2. 并发编程的应用场景。并发编程与网络编程的关系,并发编程与进程同步、进程通信、设备IO的关系,并发与并行的关系,并发与同步异步的关系 +3. 并发编程的核心问题(并发机制、并发同步、并发通信) +4. 并发编程的具体实例(在各种系统和场景下的表现) + +### 并发的概念 +并发和独占对应。 + + +### 并发编程与其他领域的关系 +* 并发编程与网络编程的关系 + * 并发编程主要是应用在**服务端**。并发编程与网络编程不通过。并发编程主要实现的是服务端如何满足大量并发的请求。而网络编程考虑的是客户端和服务器如何建立通信链接,和通信链接的过程和方式。 +* 并发编程与设备IO、进程同步、进程通信的关系 + * 是基于Linux的以上理论实现了并发的机制、并发的控制、并发的通信。分别对应设备IO、进程同步、进程通信。 +* 并发与并行的关系 + * 并发是宏观上的,并行是微观上的同时运行。 +* 并发与同步异步的关系 + * 与同步异步没有任何关系。但是并发的实现方案中,用到了同步思想和异步思想。 + * 关于这一个,可能就是造成一系列困惑的原因,网上众说纷纭。有观点认为,同步和异步都是并发的实现方式。有人认为,并发是异步的。真的恶心。可能这几个观念,从中文的角度,就是很那区分。 + * 同步异步是提交任务的方式,同步指同步提交,是串行的,一个任务结束另一个任务才能提交执行;异步指异步提交,多个任务可以并发运行 + + +### 并发编程的体系结构 +并发编程与进程同步、进程通信、设备IO有这直接关系。正在在这些理论的基础上实现了服务端的并发编程。 + +```plantuml +@startuml +object 进程同步 +object 进程通信 +object 设备IO +object 并发编程 +object 网络编程 +进程同步 --> 并发编程 +进程通信 --> 并发编程 +设备IO --> 并发编程 +设备IO --> 网络编程 + +并发编程 --> 并发机制 +并发编程 --> 并发控制_并发同步 +并发编程 --> 并发通信 + +并发机制 --> 多进程 +并发机制 --> 多线程 +并发机制 --> 多协程 +并发机制 --> 多路复用_事件驱动 + +并发控制_并发同步 --> 锁_信号量 +并发控制_并发同步 --> MVVC + +并发通信 --> 共享内存 +并发通信 --> 消息队列IPC +并发通信 --> 管道通信 +@enduml + +``` + + +### 并发编程中的核心问题 + +1. 并发机制。如何实现并发。这里简单概述一些常见的方案 + 1. 多进程机制 + 2. 多线程机制 + 3. 多协程机制 + 4. 单线程的IO多路复用(基于事件响应机制方案) +2. 并发控制。并发任务之间的同步方案,实现资源互斥、操作顺序。 + 1. 锁与信号量 + 2. MVVC多版本并发控制 +3. 并发通信。并发任务之间的数据交换方案 + 1. 共享内存 + 2. 管道文件 + 3. 消息队列 + +### 并发编程的具体实例 +> 针对每一个具体实例,在其相应的学习和开发模块中,都有对应是说明。这里就不再赘述了,有空的话学习Linux的并发编程和网络编程。C++并发编程 + +> 针对每个领域、每个系统、每个数据库、每个语言。都要解决并发编程的三个核心问题。包括并发机制、并发控制和并发通信问题。 + +1. Linux并发编程 +2. MySQL的并发方案 +3. Redis的并发方案 +4. C++并发编程 +5. Java并发编程 +6. Python并发编程 +7. Go并发编程 + +## 2 并发机制 + +> 这里所谓的什么机制、什么方法。都是设计模式的一部分。通过某种设计模式,实现并发编程:异步回调模式、事件回调模式等。 + + +### 基于多进程、多线程、多协程的并发 + +1. 在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。 +2. 开启多进程或都线程的方式,在遇到要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态。 +3. 很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。 +4. “线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。 +5. 对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。 + + + + +### 基于IO多路复用、事件响应机制、事件驱动IO的并发 + + + + + + +## 3 并发控制 + + +### 锁与互斥(操作系统和编程语言中实现并发同步的方法) + + + +### 信号量与同步(操作系统和编程语言中实现并发同步的方法) + + + + +### MVVC(在数据库中实现事务同步的方法) + + + + +## 4 并发通信 + +### 共享内存 + +### 管道文件 + +### 消息队列 diff --git a/操作系统/附录13 网络编程.md b/操作系统/附录13 网络编程.md new file mode 100644 index 00000000..33bbe717 --- /dev/null +++ b/操作系统/附录13 网络编程.md @@ -0,0 +1,72 @@ +# 网络编程 + +## 网络编程概述 + +### 问题重述 + +1. 什么是网络编程,解决了那些问题? +2. 网络编程与同步异步、阻塞非阻塞的关系 +3. 网络编程的核心机制 +4. 网络编程的具体实现 + + +### 什么是网络编程 + +* 既是进程线程通信的一部分 +* 也是设备IO(network IO)的一部分。 +* 当然也是独立的一部分。 +* 解决了两个客户端的通信问题 + + +### 网络编程与同步异步的关系 +1. 没有绝对关系,只是在其中一两个步骤中用到了同步思想或者异步思想。无法用同步异步的方式或者阻塞非阻塞的方式,来区分网络编程的类型。 + + +### 网络编程的核心机制 +> 这里简单介绍一下网络编程通用的核心的原理和模型。 + +1. 数据交换和通信方式 + 1. socket编程 + 2. 消息队列模式 + + +### 网络编程的具体实现 + +> 每个场景每种语言都有自己的具体实现方案。可以对应到笔记中的其他模块了解网络编程的具体实现。 + +1. Linux 网络编程 +2. C++网络编程库 +3. Java网络编程 +4. Python网络编程 +5. Go网络编程 + + + +## 原理和模型 + + +### 阻塞通信 + +* 普通的socket编程,连接建立过程是阻塞的、读写过程也是阻塞的。 + +### 非阻塞通信 + +* 非阻塞的socket, + + +### 消息队列 + +使用消息队列将调用异步化,可改善网站的扩展性,使用消息队列将调用异步化,可改善网站的扩展性,还可改善网站系统的性能。 + +不使用消息队列: + +![](image/2021-09-06-15-30-55.png) + +使用消息队列: + +![](image/2021-09-06-15-31-02.png) + +在不使用消息队列的情况下,用户的请求数据直接写入数据库,在高并发的情况下,会对数据库造成巨大的压力, 同时也使得响应延迟加剧。在使用消息队列后,用户请求的数据发送给消息队列后立即返回,再由消息队列的消费者进程(通常情况下, 该进程 +通常独立部署在专门的服务器集群上)从消息队列中获取数据, 异步写入数据库。由于消息队列服务器处理速度远快于数据库(消息队列服务器也比数据库具有更好的伸缩性),因此用户的响应延迟可得到有效改善 + +## 1 Linux \ No newline at end of file diff --git a/操作系统/附录8 同步异步、阻塞非阻塞.md b/操作系统/附录8 同步异步、阻塞非阻塞.md index e915418f..bf6f83ad 100644 --- a/操作系统/附录8 同步异步、阻塞非阻塞.md +++ b/操作系统/附录8 同步异步、阻塞非阻塞.md @@ -6,13 +6,65 @@ > * [https://blog.csdn.net/jolin678/article/details/49611587](https://blog.csdn.net/jolin678/article/details/49611587) > * [https://blog.csdn.net/qq_40989769/article/details/110481553](https://blog.csdn.net/qq_40989769/article/details/110481553) +## 0 问题概述 +### 背后的思想 + +* 我觉得,同步异步,更像是思想,是一种哲学思想,而不是某种具体的技术或者方案,它在不同的环节,能够表现出不同的形式。 +* 我觉得,在这个问题上,我过度思考了,太想把这两个次应用到不同的领域,因为在不同的领域都见过。通信就是通信,有很多种通信方式和机制。设备IO就是设备IO也有很多不同的实现方案,没有必要非得对应到同步和异步上。像,通信,就那几种机制,直到就行,干嘛非得划分为同步通信或者异步通信? +* 不应该以同步和异步来划分领域,而应该说明相关领域中的同步异步操作。这里只是在对各个领域中涉及到的同步异步思想进行整理。而非对并发编程、网络编程等进行分类。 + +### 需要回答以下问题 + +1. 同步异步的机制原理有哪些?抽象的模型和方法,包括怎样的设计模式? +2. 同步异步的应用场景有哪些?具体使用了怎样的机制? +3. 同步异步的实现方式有哪些?具体到每种语言、操作系统、数据库、计算机网络,进行说明。 + +### 问题归化 + +针对该领域的问题,可以归化为三种场景:**同步互斥问题**,**进程通信问题**,**设备IO问题**。 + +1. 同步互斥问题的目标是解决资源访问的问题,核心是某类公共资源。实现多个用户进程,对临界资源的访问,包括互斥访问和顺序访问,临界资源一般为共享内存或者某个数据结构。 +2. 进程通信问题的目标是解决实现多个进程、线程之间的数据交换问题,核心是交换数据。是多个用户进程线程之间,实现通信的方式。也包括网络通信问题 +3. 设备IO问题的目标是解决用户进程与内核进程之间的通信问题。设备IO中两种典型的例子是:磁盘读写、网络通信。 + +其中进程通信问题和设备IO问题可以规约为一种,例如网络通信,就可以看做网卡的IO问题和整体上的远程用户进程通信,当然,我觉得把这种东西抽象出来,将远程通信作为一种特例,是否能够更好的理解呢,它本身也可以看做一种特殊的情况。另外还有同步调用和异步调用的区别,侧重的是调用过程,而不是通信过程? -## 1 进程通信的方式——同步异步、阻塞非阻塞 +### 同步调用与异步调用 + +同步异步思想应用在函数调用上的一种体现。 + +同步调用链。我感觉,无论是单机还是网络通信,是直接调用还是并行通信,都存在一个同步调用链,当这个链路上的任何一个环节,变成异步,整个过程看起来就是异步的,能够感知到这个异步过程的部分,才会觉得是异步,例如服务器上虽然通过异步线程和事件响应机制处理,是异步的,但是客户端却感知不到,得到结果的过程还是同步的。 + +而其他部分如果包含了这个异步过程,看起来就是异步的,如果不包含,看起来就是同步的。还是例如网络通信,可以在主机的网卡通信过程中实现异步,直接返回一个信号,两方继续执行即可(没人会这么干吧,网络上异步代价太大了),也可以直接实现单个设备IO的异步,例如客户端用户进程继续执行,而内核进程等待返回结果。服务器启动额外的进程处理结果,异步的方式返回处理结果(服务器启动的多个用户进程之间的并行,通过消息通知主线程,结果得到了,然后返回给客户端消息)这里是局部的异步执行,但是整体上反而是同步的,其他线程处理好后,通知主线程,通过事件响应机制或者其他方式,告诉主线程,然后返回结果。 + + +更准确的说,异步的过程实现了并发,同步的过程实现了顺序。无论是怎样通信,同步异步只是这个过程中的一部分,这条调用链的任何一部分都可以改成异步的,然后这里就可以实现并发处理。这也是并发的一部分,我应该从并发入手,从同步异步的角度,探讨问题,已经够深入了。 + + + + +### 吐槽 +为什么这个问题这么难以理解呢,因为几个名词之间的层次与关系太乱。同步与互斥可以对应,实现了进程同步。同步异步可以对应,实现了通信和IO。同步异步对应,还能实现并发。同步异步讲的既能是多线程,也能是单线程,同步调用?还能是同步通信。待会把这里搞明白。同步是一种机制,可以在资源互斥访问上,可以在并行通信上,可以在函数调用上,也可以在设备IO上,含义相似又不完全相同。虽然其本身几句话就能说清楚何为同步何为异步,但是另一方面,却很难将这两个概念对应到具体的应用场景和过程中,它的表现形式过于复杂。 + +## 1 进程同步里的——同步互斥 + +### 概述 + +> 针对两种制约关系,合作制约关系和互斥制约关系,需要通过同步机制实现。 + +- 直接制约关系(合作)。由于多个进程相互合作产生,使得进程有一定的先后执行关系。 +- 间接制约关系(互斥)。由于多个进程资源共享产生,多个进程在同一时刻只有一个进程能进入临界区。 + + +> 显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。也就是说互斥是两个任务之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要安照某种次序来运行相应的线程(也是一种互斥)!因此互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的,而同步的任务之间则有顺序关系。 + + +## 2 进程通信里的——同步异步、阻塞非阻塞 ### 同步异步 -* 同步与异步关注的是消息通信机制。是**两个进程之间的关系**。 +* 同步与异步关注的是数据通信机制。是**两个进程之间的关系**。 * 同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。 * 异步,当一个异步过程调用发出后,调用就直接返回,调用者不会立刻得到结果。被调用者通过状态、通知和回调函数来通知调用者。 @@ -23,7 +75,7 @@ * 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。 * 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。 -## 2 设备IO的方式——同步异步、阻塞非阻塞 +## 3 设备IO里的——同步异步、阻塞非阻塞 ### nodejs中相关定义 * **同步式I/O(Synchronous I/O)或阻塞式I /O (Blocking I/O)**。线程/进程在执行中如果遇到磁盘读写或网络通信(统称为I/O 操作),通常要耗费较长的时间,这时 操作系统会剥夺这个线程/进程的CPU 控制权,使其暂停执行,同时将资源让给其他的工作线程,这种线程调度方式称为阻塞 。当I/O 操作完毕时,操作系统将 这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。 @@ -39,7 +91,7 @@ * 阻塞方式下读取或者写入函数将一直等待, * 非阻塞方式下,读取或者写入函数会立即返回一个状态值。 -## 3 进程通信和设备IO的关系 +## 4 进程通信和设备IO的关系 1. 关于同步异步、阻塞非阻塞。交互方式,分别有两种应用。一种用在**进程间的同步和通信**上;一种用在**设备IO过程**中。必须要进行区分。 @@ -47,8 +99,12 @@ 3. 所以在某种程度上。可以把两个进程的通信,看成某一个用户进程的IO过程,而另一个进程看成是内核。 -## 4 实现方式 -### 同步阻塞通信 + +在这里进行简单说明:进程同步问题和进程通信问题,都应该作为“并发编程”内容的一部分。设备IO问题作为“网络编程”的一部分进行详解。 + + +## 5 同步异步在不同语言中表现(实例) +### 同步阻塞 > 在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。 * java 传统的IO和socket网络编程,都是阻塞通信。接收端在接受到数据之前,一直处于阻塞状态。通过多线程实现并行编程。 @@ -61,7 +117,7 @@ * 用户进程轮训IO信号,虽然没有阻塞,但是在循环中一直确认IO是否完成。效率很低。 -### 异步非阻塞通信 +### 异步非阻塞 > 在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。目前Java中还没有支持此种IO模型 * node.js是单线程的异步非阻塞(事件循环检测通知的机制); @@ -70,15 +126,7 @@ * ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕 -### 异步阻塞通信 +### 异步阻塞 > 此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性! - -### 实现并发编程的几种方法 - -1. java 多线程。 -2. nodejs 单线程异步回调。非阻塞异步回调。Proactor模式 -3. linux 单线程IO复用。select/poll/epoll事件回调。多路堵塞,同步回调。reactor模式 - -> 这里所谓的什么机制、什么方法。都是设计模式的一部分。通过某种设计模式,实现并发编程:异步回调模式、事件回调模式等。 \ No newline at end of file diff --git a/数据库/MySQL/附录4 MySQL事务管理.md b/数据库/MySQL/附录4 MySQL事务管理.md index 231d9a57..8564ea9c 100644 --- a/数据库/MySQL/附录4 MySQL事务管理.md +++ b/数据库/MySQL/附录4 MySQL事务管理.md @@ -1,3 +1,10 @@ +# MySQL事务管理 + +> 参考文献 +> * [Mysql并发控制](https://zhuanlan.zhihu.com/p/133823461) + +> 本章主要讲了MySQL的事务管理,即MySQL的并发控制,MySQL在并发过程中如何进行同步,即MySQL保证事物的原子性、隔离性、一致性、持久性的方法。也属于并发机制的一部分。 + ## 4 多级锁协议 diff --git a/数据库/MySQL/附录7 MySQL并发机制.md b/数据库/MySQL/附录7 MySQL并发机制.md new file mode 100644 index 00000000..18415dba --- /dev/null +++ b/数据库/MySQL/附录7 MySQL并发机制.md @@ -0,0 +1,369 @@ + +## 1 并发机制 + +1. 什么是并发,并发与多线程有什么关系? + +1. 先从广义上来说,或者从实际场景上来说. + 1. 高并发通常是海量用户同时访问(比如:12306买票、淘宝的双十一抢购),如果把一个用户看做一个线程的话那么并发可以理解成多线程同时访问,高并发即海量线程同时访问。(ps:我们在这里模拟高并发可以for循环多个线程即可) + +2. 从代码或数据的层次上来说。多个线程同时在一条相同的数据上执行多个数据库操作。 + +## 2 并发分类 +> 参考文献 +> * [锁与并发](https://www.cnblogs.com/yaopengfei/p/8399358.html) +### 积极并发 +积极并发(乐观并发、乐观锁):无论何时从数据库请求数据,数据都会被读取并保存到应用内存中。数据库级别没有放置任何显式锁。数据操作会按照数据层接收到的先后顺序来执行。 + +积极并发本质就是允许冲突发生,然后在代码本身采取一种合理的方式去解决这个并发冲突,常见的方式有: + +1. 忽略冲突强制更新:数据库会保存最后一次更新操作(以更新为例),会损失很多用户的更新操作。 +2. 部分更新:允许所有的更改,但是不允许更新完整的行,只有特定用户拥有的列更新了。这就意味着,如果两个用户更新相同的记录但却不同的列,那么这两个更新都会成功,而且来自这两个用户的更改都是可见的。(EF默认实现不了这种情况) +3. 询问用户:当一个用户尝试更新一个记录时,但是该记录自从他读取之后已经被别人修改了,这时应用程序就会警告该用户该数据已经被某人更改了,然后询问他是否仍然要重写该数据还是首先检查已经更新的数据。(EF可以实现这种情况,在后面详细介绍) +4. 拒绝修改:当一个用户尝试更新一个记录时,但是该记录自从他读取之后已经被别人修改了,此时告诉该用户不允许更新该数据,因为数据已经被某人更新了。 + + +### 消极并发 + +消极并发(悲观并发、悲观锁):无论何时从数据库请求数据,数据都会被读取,然后该数据上就会加锁,因此没有人能访问该数据。这会降低并发出现问题的机会,缺点是加锁是一个昂贵的操作,会降低整个应用程序的性能。 + +消极并发的本质就是永远不让冲突发生,通常的处理凡是是只读锁和更新锁。 + +1. 当把只读锁放到记录上时,应用程序只能读取该记录。如果应用程序要更新该记录,它必须获取到该记录上的更新锁。如果记录上加了只读锁,那么该记录仍然能够被想要只读锁的请求使用。然而,如果需要更新锁,该请求必须等到所有的只读锁释放。同样,如果记录上加了更新锁,那么其他的请求不能再在这个记录上加锁,该请求必须等到已存在的更新锁释放才能加锁。总结,这里我们可以简单理解把并发业务部分用一个锁(如:lock,实质是数据库锁,后面章节单独介绍)锁住,使其同时只允许一个线程访问即可。 + +2. 加锁会带来很多弊端: + 1. 应用程序必须管理每个操作正在获取的所有锁; + 2. 加锁机制的内存需求会降低应用性能 + 3. 多个请求互相等待需要的锁,会增加死锁的可能性。 + + +## 3 并发问题的解决方案(提高并发的方法) + +并发机制的解决方案 + +1. 从架构的角度去解决(大层次 如:12306买票) + +  nginx负载均衡、数据库读写分离、多个业务服务器、多个数据库服务器、NoSQL, 使用队列来处理业务,将高并发的业务依次放到队列中,然后按照先进先出的原则, 逐个处理(队列的处理可以采用 Redis、RabbitMq等等) + +  (PS:在后面的框架篇章里详细介绍该方案) + +2. 从代码的角度去解决(在服务器能承载压力的情况下,并发访问同一条数据) + +  实际的业务场景:如进销存类的项目,涉及到同一个物品的出库、入库、库存,我们都知道库存在数据库里对应了一条记录,入库要查出现在库存的数量,然后加上入库的数量,假设两个线程同时入库,假设查询出来的库存数量相同,但是更新库存数量在数据库层次上是有先后,最终就保留了后更新的数据,显然是不正确的,应该保留的是两次入库的数量和。 + +(该案例的实质:多个线程同时在一条相同的数据上执行多个数据库操作) + +事先准备一张数据库表: + + + +解决方案一:(最常用的方式) + +  给入库和出库操作加一个锁,使其同时只允许一个线程访问,这样即使两个线程同时访问,但在代码层次上,由于锁的原因,还是有先有后的,这样就保证了入库操作的线程唯一性,当然库存量就不会出错了. + +总结:该方案可以说是适合处理小范围的并发且锁内的业务执行不是很复杂。假设一万线程同时入库,每次入库要等2s,那么这一万个线程执行完成需要的总时间非常多,显然不适合。 + + (这种方式的实质就是给核心业务加了个lock锁,这里就不做测试了) + + + +解决方案二:EF处理积极并发带来的冲突 + +1. 配置准备 + +  (1). 针对DBFirst模式,可以给相应的表额外加一列RowVersion,数据库中为timestamp类型,对应的类中为byte[]类型,并且在Edmx模型上给该字段的并发模式设置为fixed(默认为None),这样该表中所有字段都监控并发。 + +如果不想监视所有列(在不添加RowVersion的情况下),只需在Edmx模型是给特定的字段的并发模式设置为fixed,这样只有被设置的字段被监测并发。 + +  测试结果: (DBFirst模式下的并发测试) + +  事先在UserInfor1表中插入一条id、userName、userSex、userAge均为1的数据(清空数据)。 + +测试情况1: + +  在不设置RowVersion并发模式为Fixed的情况下,两个线程修改不同字段(修改同一个字段一个道理),后执行的线程的结果覆盖前面的线程结果. + +  发现测试结果为:1,1,男,1 ; 显然db1线程修改的结果被db2线程给覆盖了. (修改同一个字段一个道理) + + +``` + 1 { + 2 //1.创建两个EF上下文,模拟代表两个线程 + 3 var db1 = new ConcurrentTestDBEntities(); + 4 var db2 = new ConcurrentTestDBEntities(); + 5 + 6 UserInfor1 user1 = db1.UserInfor1.Find("1"); + 7 UserInfor1 user2 = db2.UserInfor1.Find("1"); + 8 + 9 //2. 执行修改操作 +10 //(db1的线程先执行完修改操作,并保存) +11 user1.userName = "ypf"; +12 db1.Entry(user1).State = EntityState.Modified; +13 db1.SaveChanges(); +14 +15 //(db2的线程在db1线程修改完成后,执行修改操作) +16 try +17 { +18 user2.userSex = "男"; +19 db2.Entry(user2).State = EntityState.Modified; +20 db2.SaveChanges(); +21 +22 Console.WriteLine("测试成功"); +23 } +24 catch (Exception) +25 { +26 Console.WriteLine("测试失败"); +27 } +28 } +``` +测试情况2: + +  设置RowVersion并发模式为Fixed的情况下,两个线程修改不同字段(修改同一个字段一个道理),如果该条数据已经被修改,利用DbUpdateConcurrencyException可以捕获异常,进行积极并发的冲突处理。测试结果如下: + +  a.RefreshMode.ClientWins: 1,1,男,1 + +  b.RefreshMode.StoreWins: 1,ypf,1,1 + +  c.ex.Entries.Single().Reload(); 1,ypf,1,1 + +``` + 1 { + 2 //1.创建两个EF上下文,模拟代表两个线程 + 3 var db1 = new ConcurrentTestDBEntities(); + 4 var db2 = new ConcurrentTestDBEntities(); + 5 + 6 UserInfor1 user1 = db1.UserInfor1.Find("1"); + 7 UserInfor1 user2 = db2.UserInfor1.Find("1"); + 8 + 9 //2. 执行修改操作 +10 //(db1的线程先执行完修改操作,并保存) +11 user1.userName = "ypf"; +12 db1.Entry(user1).State = EntityState.Modified; +13 db1.SaveChanges(); +14 +15 //(db2的线程在db1线程修改完成后,执行修改操作) +16 try +17 { +18 user2.userSex = "男"; +19 db2.Entry(user2).State = EntityState.Modified; +20 db2.SaveChanges(); +21 +22 Console.WriteLine("测试成功"); +23 } +24 catch (DbUpdateConcurrencyException ex) +25 { +26 Console.WriteLine("测试失败:" + ex.Message); +27 +28 //1. 保留上下文中的现有数据(即最新,最后一次输入) +29 //var oc = ((IObjectContextAdapter)db2).ObjectContext; +30 //oc.Refresh(RefreshMode.ClientWins, user2); +31 //oc.SaveChanges(); +32 +33 //2. 保留原始数据(即数据源中的数据代替当前上下文中的数据) +34 //var oc = ((IObjectContextAdapter)db2).ObjectContext; +35 //oc.Refresh(RefreshMode.StoreWins, user2); +36 //oc.SaveChanges(); +37 +38 //3. 保留原始数据(而Reload处理也就是StoreWins,意味着放弃当前内存中的实体,重新到数据库中加载当前实体) +39 ex.Entries.Single().Reload(); +40 db2.SaveChanges(); +41 } +42 } +``` + +测试情况3: + +  在不设置RowVersion并发模式为Fixed的情况下(也不需要RowVersion这个字段),单独设置userName字段的并发模式为Fixed,两个线程同时修改该字段,利用DbUpdateConcurrencyException可以捕获异常,进行积极并发的冲突处理,但如果是两个线程同时修改userName以外的字段,将不能捕获异常,将走EF默认的处理方式,后执行的覆盖先执行的。 + +  a.RefreshMode.ClientWins: 1,ypf2,1,1 + +  b.RefreshMode.StoreWins: 1,ypf,1,1 + +  c.ex.Entries.Single().Reload(); 1,ypf,1,1 + + View Code +  (2). 针对CodeFirst模式,需要有这样的一个属性 public byte[] RowVersion { get; set; },并且给属性加上特性[Timestamp],这样该表中所有字段都监控并发。如果不想监视所有列(在不添加RowVersion的情况下),只需给特定的字段加上特性 [ConcurrencyCheck],这样只有被设置的字段被监测并发。 + +  除了再配置上不同于DBFirst模式以为,是通过加特性的方式来标记并发,其它捕获并发和积极并发的几类处理方式均同DBFirst模式相同。(这里不做测试了) + +1. 积极并发处理的三种形式总结: + +  利用DbUpdateConcurrencyException可以捕获异常,然后: + +    a. RefreshMode.ClientWins:保留上下文中的现有数据(即最新,最后一次输入) + +    b. RefreshMode.StoreWins:保留原始数据(即数据源中的数据代替当前上下文中的数据) + +    c.ex.Entries.Single().Reload(); 保留原始数据(而Reload处理也就是StoreWins,意味着放弃当前内存中的实体,重新到数据库中加载当前实体) + +3. 该方案总结: + +  这种模式实质上就是获取异常告诉程序,让开发人员结合需求自己选择怎么处理,但这种模式是解决代码层次上的并发冲突,并不是解决大数量同时访问崩溃问题的。 + +解决方案三:利用队列来解决业务上的并发(架构层次上其实也是这种思路解决的) + +1.先分析: + +  前面说过所谓的高并发,就是海量的用户同时向服务器发送请求,进行某个业务处理(比如定时秒杀的抢单),而这个业务处理是需要 一定时间的。 + +2.处理思路: + +  将海量用户的请求放到一个队列里(如:Queue),先不进行业务处理,然后另外一个服务器从线程中读取这个请求(MVC框架可以放到Global全局里),依次进行业务处理,至于处理完成后,是否需要告诉客户端,可以根据实际需求来定,如果需要的话(可以借助Socket、Signalr、推送等技术来进行). + +  特别注意:读取队列的线程是一直在运行,只要队列中有数据,就给他拿出来. + +  这里使用Queue队列,可以参考:http://www.cnblogs.com/yaopengfei/p/8322016.html + +  (PS:架构层次上的处理方案无非队列是单独一台服务器,执行从队列读取的是另外一台业务服务器,处理思想是相同的) + +队列单例类的代码: + + +``` + 1 /// + 2 /// 单例类 + 3 /// + 4 public class QueueUtils + 5 { + 6 /// + 7 /// 静态变量:由CLR保证,在程序第一次使用该类之前被调用,而且只调用一次 + 8 /// + 9 private static readonly QueueUtils _QueueUtils = new QueueUtils(); +10 +11 /// +12 /// 声明为private类型的构造函数,禁止外部实例化 +13 /// +14 private QueueUtils() +15 { +16 +17 } +18 /// +19 /// 声明属性,供外部调用,此处也可以声明成方法 +20 /// +21 public static QueueUtils instanse +22 { +23 get +24 { +25 return _QueueUtils; +26 } +27 } +28 +29 +30 //下面是队列相关的 +31 System.Collections.Queue queue = new System.Collections.Queue(); +32 +33 private static object o = new object(); +34 +35 public int getCount() +36 { +37 return queue.Count; +38 } +39 +40 /// +41 /// 入队方法 +42 /// +43 /// +44 public void Enqueue(object myObject) +45 { +46 lock (o) +47 { +48 queue.Enqueue(myObject); +49 } +50 } +51 /// +52 /// 出队操作 +53 /// +54 /// +55 public object Dequeue() +56 { +57 lock (o) +58 { +59 if (queue.Count > 0) +60 { +61 return queue.Dequeue(); +62 } +63 } +64 return null; +65 } +66 +67 } +``` + +PS:这里的入队和出队都要加锁,因为Queue默认不是线程安全的,不加锁会存在资源竞用问题从而业务出错,或者直接使用ConcurrentQueue线程安全的队列,就不需要加锁了,关于队列线程安全问题详见:http://www.cnblogs.com/yaopengfei/p/8322016.html + +临时存储数据类的代码: + +``` + 1 /// + 2 /// 该类用来存储请求信息 + 3 /// + 4 public class TempInfor + 5 { + 6 /// + 7 /// 用户编号 + 8 /// + 9 public string userId { get; set; } +10 } +``` + +模拟高并发入队,单独线程出队的代码: + +``` + 1 { + 2 //3.1 模拟高并发请求 写入队列 + 3 { + 4 for (int i = 0; i < 100; i++) + 5 { + 6 Task.Run(() => + 7 { + 8 TempInfor tempInfor = new TempInfor(); + 9 tempInfor.userId = Guid.NewGuid().ToString("N"); +10 //下面进行入队操作 +11 QueueUtils.instanse.Enqueue(tempInfor); +12 +13 }); +14 } +15 } +16 //3.2 模拟另外一个线程队列中读取数据请求标记,进行相应的业务处理(该线程一直运行,不停止) +17 Task.Run(() => +18 { +19 while (true) +20 { +21 if (QueueUtils.instanse.getCount() > 0) +22 { +23 //下面进行出队操作 +24 TempInfor tempInfor2 = (TempInfor)QueueUtils.instanse.Dequeue(); +25 +26 //拿到请求标记,进行相应的业务处理 +27 Console.WriteLine("id={0}的业务执行成功", tempInfor2.userId); +28 } +29 } +30 }); +31 //3.3 模拟过了一段时间(6s后),又有新的请求写入 +32 Thread.Sleep(6000); +33 Console.WriteLine("6s的时间已经过去了"); +34 { +35 for (int j = 0; j < 100; j++) +36 { +37 Task.Run(() => +38 { +39 TempInfor tempInfor = new TempInfor(); +40 tempInfor.userId = Guid.NewGuid().ToString("N"); +41 //下面进行入队操作 +42 QueueUtils.instanse.Enqueue(tempInfor); +43 +44 }); +45 } +46 } +47 } +``` +3.下面案例的测试结果: + +  一次输出100条数据,6s过后,再一次输出100条数据。 + + + +1. 总结: + +  该方案是一种迂回的方式处理高并发,在业内这种思想也是非常常见,但该方案也有一个弊端,客户端请求的实时性很难保证,或者即使要保证(比如引入实时通讯技术), + + 也要付出不少代价. \ No newline at end of file