1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
目录
进程线程协程
可以看成是 CPU执行的具体的任务 。在操作系统当中,由于CPU的运行速度非常快,要比计算机当中的其他设备要快得多。比如内存、磁盘等等,所以如果CPU一次只执行一个任务,那么会导致CPU大量时间在等待这些设备,这样操作效率很低。为了提升计算机的运行效率,把机器的技能尽可能压榨出来,CPU是轮询工作的。也就是说 它一次只执行一个任务,执行一小段碎片时间之后立即切换 ,去执行其他任务。
所以在早期的单核机器的时候,看起来电脑也是并发工作的。我们可以一边听歌一边上网,也不会觉得卡顿。但实际上,这是CPU轮询的结果。在这个例子当中,听歌的软件和上网的软件对于CPU而言都是 独立的进程 。我们可以把进程简单地理解成运行的应用,比如在安卓手机里面,一个app启动的时候就会对应系统中的一个进程。当然这种说法不完全准确, 一个应用也是可以启动多个进程的 。
进程是对应CPU而言的,线程则更多针对的是程序。即使是CPU在执行当前进程的时候,程序运行的任务其实也是有分工的。举个例子,比如听歌软件当中,我们需要显示歌词的字幕,需要播放声音,需要监听用户的行为,比如是否发生了切歌、调节音量等等。所以,我们需要 进一步拆分CPU的工作 ,让它在执行当前进程的时候,继续通过轮询的方式来同时做多件事情。
进程中的任务就是线程,所以从这点上来说, 进程和线程是包含关系 。一个进程当中可以包含多个线程,对于CPU而言,不能直接执行线程,一个线程一定属于一个进程。所以我们知道,CPU进程切换切换的是执行的应用程序或者是软件,而进程内部的线程切换,切换的是软件当中具体的执行任务。
关于进程和线程有一个经典的模型可以说明它们之间的关系,假设CPU是一家工厂,工厂当中有多个车间。不同的车间对应不同的生产任务,有的车间生产汽车轮胎,有的车间生产汽车骨架。但是工厂的电力是有限的,同时只能满足一个厂房的使用。
为了让大家的进度协调,所以工厂个需要轮流提供各个车间的供电。 这里的车间对应的就是进程 。
一个车间虽然只生产一种产品,但是其中的工序却不止一个。一个车间可能会有好几条流水线,具体的生产任务其实是流水线完成的,每一条流水线对应一个具体执行的任务。但是同样的, 车间同一时刻也只能执行一条流水线 ,所以我们需要车间在这些流水线之间切换供电,让各个流水线生产进度统一。
这里车间里的 流水线自然对应的就是线程的概念 ,这个模型很好地诠释了CPU、进程和线程之间的关系。实际的原理也的确如此,不过CPU中的情况要比现实中的车间复杂得多。因为对于进程和CPU来说,它们面临的局面都是实时变化的。车间当中的流水线是x个,下一刻可能就成了y个。
了解完了线程和进程的概念之后,对于理解电脑的配置也有帮助。比如我们买电脑,经常会碰到一个术语,就是这个电脑的CPU是某某核某某线程的。比如我当年买的第一台笔记本是4核8线程的,这其实是在说这台电脑的CPU有 4个计算核心 ,但是使用了超线程技术,使得可以把一个物理核心模拟成两个逻辑核心。相当于我们可以用4个核心同时执行8个线程,相当于8个核心同时执行,但其实有4个核心是模拟出来的虚拟核心。
有一个问题是 为什么是4核8线程而不是4核8进程呢 ?因为CPU并不会直接执行进程,而是执行的是进程当中的某一个线程。就好像车间并不能直接生产零件,只有流水线才能生产零件。车间负责的更多是资源的调配,所以教科书里有一句非常经典的话来诠释: 进程是资源分配的最小单元,线程是CPU调度的最小单元 。
Python当中为我们提供了完善的threading库,通过它,我们可以非常方便地创建线程来执行多线程。
首先,我们引入threading中的Thread,这是一个线程的类,我们可以通过创建一个线程的实例来执行多线程。
from threading import Thread
t = Thread(target=func, name='therad', args=(x, y))
t.start()
简单解释一下它的用法,我们传入了三个参数,分别是 target,name和args ,从名字上我们就可以猜测出它们的含义。首先是target,它传入的是一个方法,也就是我们希望多线程执行的方法。name是我们为这个新创建的线程起的名字,这个参数可以省略,如果省略的话,系统会为它起一个系统名。当我们执行Python的时候启动的线程名叫MainThread,通过线程的名字我们可以做区分。args是会传递给target这个函数的参数。
我们来举个经典的例子:
import time, threading
# 新线程执行的代码:
def loop(n):
print('thread %s is running...' % threading.current_thread().name)
for i in range(n):
print('thread %s >>> %s' % (threading.current_thread().name, i))
time.sleep(5)
print('thread %s ended.' % threading.current_thread().name)
print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread', args=(10, ))
t.start()
print('thread %s ended.' % threading.current_thread().name)
我们创建了一个非常简单的loop函数,用来执行一个循环来打印数字,我们每次打印一个数字之后这个线程会睡眠5秒钟,所以我们看到的结果应该是每过5秒钟屏幕上多出一行数字。
我们在Jupyter里执行一下:
表面上看这个结果没毛病,但是其实有一个问题,什么问题呢? 输出的顺序不太对 ,为什么我们在打印了第一个数字0之后,主线程就结束了呢?另外一个问题是,既然主线程已经结束了,为什么Python进程没有结束 , 还在向外打印结果呢?
因为线程之间是独立的,对于主线程而言,它在执行了t.start()之后,并 不会停留,而是会一直往下执行一直到结束 。如果我们不希望主线程在这个时候结束,而是阻塞等待子线程运行结束之后再继续运行,我们可以在代码当中加上t.join()这一行来实现这点。
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)
join操作可以让主线程在join处挂起等待,直到子线程执行结束之后,再继续往下执行。我们加上了join之后的运行结果是这样的:
这个就是我们预期的样子了,等待子线程执行结束之后再继续。
我们再来看第二个问题,为什么主线程结束的时候,子线程还在继续运行,Python进程没有退出呢?这是因为默认情况下我们创建的都是用户级线程,对于进程而言, 会等待所有用户级线程执行结束之后才退出 。这里就有了一个问题,那假如我们创建了一个线程尝试从一个接口当中获取数据,由于接口一直没有返回,当前进程岂不是会永远等待下去?
这显然是不合理的,所以为了解决这个问题,我们可以把创建出来的线程设置成 守护线程 。
守护线程即daemon线程,它的英文直译其实是后台驻留程序,所以我们也可以理解成 后台线程 ,这样更方便理解。daemon线程和用户线程级别不同,进程不会主动等待daemon线程的执行, 当所有用户级线程执行结束之后即会退出。进程退出时会kill掉所有守护线程 。
我们传入daemon=True参数来将创建出来的线程设置成后台线程:
t = threading.Thread(target=loop, name='LoopThread', args=(10, ), daemon=True)
这样我们再执行看到的结果就是这样了:
这里有一点需要注意,如果你 在jupyter当中运行是看不到这样的结果的 。因为jupyter自身是一个进程,对于jupyter当中的cell而言,它一直是有用户级线程存活的,所以进程不会退出。所以想要看到这样的效果,只能通过命令行执行Python文件。
如果我们想要等待这个子线程结束,就必须通过join方法。另外,为了预防子线程锁死一直无法退出的情况, 我们还可以 在joih当中设置timeout ,即最长等待时间,当等待时间到达之后,将不再等待。
比如我在join当中设置的timeout等于5时,屏幕上就只会输出5个数字。
另外,如果没有设置成后台线程的话,设置timeout虽然也有用,但是 进程仍然会等待所有子线程结束 。所以屏幕上的输出结果会是这样的:
虽然主线程继续往下执行并且结束了,但是子线程仍然一直运行,直到子线程也运行结束。
关于join设置timeout这里有一个坑,如果我们只有一个线程要等待还好,如果有多个线程,我们用一个循环将它们设置等待的话。那么 主线程一共会等待N * timeout的时间 ,这里的N是线程的数量。因为每个线程计算是否超时的开始时间是上一个线程超时结束的时间,它会等待所有线程都超时,才会一起终止它们。
比如我这样创建3个线程:
ths = []
for i in range(3):
t = threading.Thread(target=loop, name='LoopThread' + str(i), args=(10, ), daemon=True)
ths.append(t)
for t in ths:
t.start()
for t in ths:
t.join(2)
最后屏幕上输出的结果是这样的:
所有线程都存活了6秒,不得不说,这个设计有点坑,和我们预想的完全不一样。
在今天的文章当中,我们一起简单了解了 操作系统当中线程和进程的概念 ,以及Python当中如何创建一个线程,以及关于创建线程之后的相关使用。今天介绍的只是最基础的使用和概念,关于线程还有很多高端的用法,我们将在后续的文章当中和大家分享。
多线程在许多语言当中都是至关重要的,许多场景下必定会使用到多线程。比如 web后端,比如爬虫,再比如游戏开发 以及其他所有需要涉及开发ui界面的领域。因为凡是涉及到ui,必然会需要一个线程单独渲染页面,另外的线程负责准备数据和执行逻辑。因此,多线程是专业程序员绕不开的一个话题,也是一定要掌握的内容之一。
SQL 、 NoSQL 和 NewSQL
- 统一标准的、非结构化查询语言。
- 坚持 ACID 准则 (原子性,一致性,隔离性,持久性)
不能满足现代数据库每秒处理的事务数量
NoSQL (Not Only SQL)
主要用于解决SQL的可扩展性问题。 没有架构、且建立在分布式系统上、易于扩展和分片。这些好处是以放宽ACID原则为代价的,在特定时间段内没有特定数据项的更新,则最终对其所有的访问都将返回最后更新的值。
关系型数据库
<1>关系数据库的特点是:
- - 数据关系模型基于关系模型,结构化存储,完整性约束。
- - 基于二维表及其之间的联系,需要连接、并、交、差、除等数据操作。
- - 采用结构化的查询语言(SQL)做数据读写。
- - 操作需要数据的一致性,需要事务甚至是强一致性。
<2>优点:
- - 保持数据的一致性(事务处理)
- - 可以进行join等复杂查询。
- - 通用化,技术成熟。
<3>缺点:
- - 数据读写必须经过sql解析,大量数据、高并发下读写性能不足。
- - 对数据做读写,或修改数据结构时需要加锁,影响并发操作。
- - 无法适应非结构化存储。
- - 扩展困难。
- - 昂贵、复杂。
NoSQL数据库
<1>NoSQL数据库的特点是:
- - 非结构化的存储。
- - 基于多维关系模型。
- - 具有特有的使用场景。
<2>优点:
- - 高并发,大数据下读写能力较强。
- - 基本支持分布式,易于扩展,可伸缩。
- - 简单,弱结构化存储。
<3>缺点:
- - join等复杂操作能力较弱。
- - 事务支持较弱。
- - 通用性差。
- - 无完整约束复杂业务场景支持较差。
虽然在云计算时代,传统数据库存在着先天性的弊端,但是NoSQL数据库又无法将其替代,NoSQL只能作为传统数据的补充而不能将其替代,所以规避传统数据库的缺点是目前大数据时代必须要解决的问题。
Docker-容器间通信
通过虚拟IP,进行容器间的通信
- Docker会为每个创建的容器自动分配一个虚拟IP,虚拟IP无法从容器外侧进行访问,是Docker环境内部容器间彼此通信的标识
- 缺点:配置的Ip地址需要不断的变更,并重启服务
容器间单向通信
# 重新创建webserver容器 --link 要链接的容器名称 (--link 可以使新生成的容器链接到已在运行的容器)
docker run -d --name webserver --link database servername
webserver数据库连接配置文件中的url中的IP地址即可换成database
容器间双向通信
网桥充当了Docker容器和外界宿主机之间的通信源,容器内部也可以访问互联网
网桥也可以用于容器内部,实现容器间的双向通信,就是对容器从网络层面进行分组
# 创建网桥
docker network create -d bridge my-bridge
docker network connect my-bridge webserver
docker network connect my-bridge database
网桥的原理:创建一个网桥,就相当于在宿主机上安装了一个虚拟网卡,这个虚拟网卡也承担了网关的作用。虚拟网卡和物理网卡之间需要做地址转换
HTTP API 与Restful API 关系及区别
关系及区别
- HTTP协议 是 REST架构 子模式。
- HTTP API实现了Restful API。
- Restful API是HTTP API的基础,提供了标准接口。
REST主要原则:
- 1. C/S模型,通过统一接口通讯
- 2. 层次化,可与多个服务器通讯
- 3. 无状态,服务器不保存客户状态,发送请求时需包含足够信息
- 4. Cache,C/S之间可使用缓存
- 5. 唯一资源标识来标识资源
- 6. 传递消息的自身描述
- 7. 返回资源的自身描述
- 8. 可选的超媒体即应用状态引擎(HATEOAS)
Flask 和Django的区别与比较
Django 是一个重量级的框架
- 提供项目工程管理的自动化脚本工具
- 数据库ORM支持(对象关系映射,英语:Object Relational Mapping)
- 模板
- 表单
- Admin管理站点
- 文件管理
- 认证权限
- session机制
- 缓存
- 遵循MVC设计
Flask是一个轻量型的框架
Flask 本身相当于一个内核,路由模块:Werkzeug(),模板引擎:Jinja2(),其他几乎所有的功能都要用到扩展.
Flask 和 Django 路由映射的区别:
1. 在Django 中, 路由是浏览器访问服务器时,先访问的项目中的url, 再由项目中的url找到应用中url, 然后找到我们的视图函数; 视图函数然后直接或者间接的继承了Django提供的父类View,配置路由时,使用类视图的as_view()
方法来添加;
2. 在Flask 中,路由是通过装饰器给每个视图函数提供的, 而且根据请求方式的不同可以一个 url 用于不同的作用;
Python字典的底层原理
python字典及其特性
字典是Python的一种可变、无序容器数据结构,它的元素以键值对的形式存在,键值唯一,它的特点搜索速度很快
哈希表
Python字典的底层实现是哈希表。什么是哈希表,简单来说就是一张带索引和存储空间的表,对于任意可哈希对象,通过哈希索引的计算公式:hash(hashable)%k(对可哈希对象进行哈希计算,然后对结果进行取余运算),可将该对象映射为0到k-1之间的某个表索引,然后在该索引所对应的空间进行变量的存储/读取等操。
Python字典如何运用哈希表
插入
对键进行哈希和取余运算,得到一个哈希表的索引,如果该索引所对应的表地址空间为空,将键值对存入该地址空间;
查询
对键进行哈希和取余运算,得到一个哈希表的索引,如果该索引所对应的地址空间中健与要更新的健一致,那么就更新该健所对应的值;对要查找的健进行哈希和取余运算,得到一个哈希表的索引,如果该索引所对应的地址空间中健与要查询的健一致,那么就将该键值对取出来;
更新
对键进行哈希和取余运算,得到一个哈希表的索引,如果该索引所对应的地址空间中健与要更新的健一致,那么就更新该健所对应的值;
扩容
字典初始化的时候,会对应初始化一个有k个空间的表,等空间不够用的时候,系统就会自动扩容,这时候会对已经存在的键值对重新进行哈希取余运算(重新进行插入操作)保存到其它位置;
哈希碰撞
有时候对于不同的键,经过哈希取余运算之后,得到的索引值一样,这时候怎么办?这时采用公开寻址的方式,运用固定的模式将键值对插入到其它的地址空间,比如线性寻址:如果第i个位置已经被使用,我们就看看第i+1个,第i+2个,第i+3个有没有被使用…直到找到一个空间或者对空间进行扩容。