并发
并发和并行
并行,parallel,某时刻,同时做几件事
并发,concurrency,时间段内有事情要处理
并发解决方案
并发必须解决,所有程序几乎都要面对,特别是面对用户是互联网用户时,高并发更需要处理
打饭模型,中午十二点,食堂涌入大量人,如果人很多,这就是高并发。
1,队列,排队,
假设资源只有一个,队列或者列表是较好的方式
可以使用优先队列或者双队列
队列就是缓冲区buffer,代价较小,但是在面对高并发时可能会不够用
作用是平滑请求
例如queue模块的类,Queue,LifoQueue,PriorityQueue
2、争抢
挤到窗口的,会一直占据着窗口,可以视为锁定窗口,不在为其他人服务,视为一种锁机制
争抢是一种排他性的锁,其他人只能等。
对于某些请求者,可能永远抢不到资源
有争抢就要有锁
3、预处理
如果排长队的原因,是由于每个人的等候时间太长,就要考虑提前将热门的80%的饭菜做好,保证供应,其他的现做,这样就算锁定窗口,也能很快就释放。
提前加载用户需要的数据的思路,就是预处理思想,缓存常用。
高并发常用解决思想,大规模时使用分布式缓存
4、并行
分配问题,轮询(最简单的模型),要考虑队伍长短的解决方案,可以实时看队列的情况来看放入那个队列。
开窗口就像扩大食堂,会造成成本上升
日常可以通过购买更多服务器,或者开多线程,多进程并行实现
水平扩展思想
线程在单cpu时,就不是并行了
并行只是并发的解决方案之一
5、提速
提高打饭速度,也是解决方案
提高单个CPU性能,或者服务器安装更多cpu
垂直扩展思想
6、消息中间件
地铁站外的安检走廊,缓冲人流,进去后还要重新排队
队列作用是缓冲,解耦
常用第三方队列,也就是消息中间件,常用的有RabbitMQ、ActiveMQ(Apache)、RocketMQ(阿里Apache)、kafka(Apache)等
解决高并发的方法还有很多,一般会根据不同的场景用不同的策略,而策略可以是多种方式的优化组合。
可以多开食堂(多地),也可以就近,建到宿舍附近(就近),技术来源生活。
线程,进程
实现了线程的操作系统中,线程是操作系统能够运算调度的最小单位。线程包含在进程中,是进程的实际运作单位,一个程序的执行实例就是进程。
进程是计算机中程序关于某数据集合的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统的基础。
ABI (Application Binary Interface) 应用程序二进制接口
程序源代码编译后的文件存放在磁盘中,当程序被操作系统加载到内存中时,就是进程,进程中存放着指令和数据(资源),进程也是线程的容器
Linux中有父进程,子进程,Windows的进程是平等关系
操作系统必须有进程管理
线程 有时被称为轻量级进程LWP(Lightweight Process)是程序执行流的最小单元
一个标准的线程由线程的ID,当前指令指针(PC:程序计数器),寄存器集合和堆栈组成。
理解为:
进程就是独立的王国,不会共享数据
线程就是省份,同一个进程内的线程可以共享进程的资源,每一个线程有自己的数据,独立的堆栈
线程状态
ready 就绪 线程能够运行,但在等待调度
running 运行 线程正在运行
blocked 阻塞 线程等待外部事件的发生而无法运行,如I/O操作
terminal 终止,线程完成或者退出,或者取消
以上是较常见的几种状态,不同的操作系统可能会不同
线程的状态转换
虚拟化:把一个资源当做多个资源用
分时的想法,将cpu的运算时间细分,时间切分的十分的小,几微秒处理一个线程,让人感觉像是同时在处理。可以加入优先的思想,让人感觉这个处理的较快,这些全部由操作系统调度。这里就利用了虚拟化的思想。
时间片
多核,可以同时执行;单核,基本都是分时的。
Python的线程开发
Python的进程会启动解释器进程,线程共享一个解释器进程
Python线程的开发使用标准库threading
Thread类
#签名 def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None):
参数:
target 线程调用的函数,就是目标函数
name 线程的名字
args 目标函数的参数,实参,要求必须是元组
kwargs 为目标函数关键字传参,字典
线程目标创建后并不会启动,要使用start方法
线程就是执行代码的,函数是最简单的封装,函数执行完就退出,如果要不退出,就要使用while
import threading,time def worker(x,y): count = 0 while count < 5: print('I am working',x,y) time.sleep(2) count += 1 print('finish') t = threading.Thread(target=worker,name='work1',args=(4,),kwargs={'y':2}) t.start() print('+++end+++')
线程退出
python没有提供线程退出的方法,结束的方法就是语句执行完或者抛出未处理的异常
python的线程没有优先级,不能被销毁,停止,挂起,也没有恢复,中断
线程的传参和函数的传参相同。
threading的属性方法
current_thread() 返回当前线程对象
main_thread() 返回主线程对象
active_thread() 返回处于alive状态的线程的个数
enumerate() 返回所有或者的线程的列表,不包括已经终止的线程和未开始的线程
get_ident() 返回当前线程的ID,非0整数
threading 实例的属性和方法
name 名字,标识,可以重名,getaName()、setName()获取设置这个名词
ident 线程ID,非零整数,线程启动才有ID,未启动未None。线程退出后,此ID依旧可以访问,此ID可以重复使用。注意ID只有线程退出后才能在利用,线程运行时ID唯一。
is_alive 返回线程是否活着
同一个线程只能被调用一次,再使用就重新创建
t.start() 启动线程,只能用一次,创建线程对象,start会调用run
t.run() 运行线程函数,在当前线程执行,顺序执行就是在主线程中调用了一个普通的函数而已
run不会开启新的线程,start会启动一个新线程。
启用线程使用start方法,才能启动多个线程。
串行化,并行化,对于硬件设备,并口速度快于串口,但是成本增高,并且处理难度上升。所以一般都使用串口。系统的进程都会开启多线程,但是开启越多,需要的管理成本,维护成本越高。所以线程开启合适的数目就好,并不是多多益善。
串口形容一下就是 一条车道,而并口就是有8个车道同一时刻能传送8位(一个字节)数据。 但是并不是并口快,由于8位通道之间的互相干扰。传输时速度就受到了限制。而且当传输出错时,要同时重新传8个位的数据。串口没有干扰,传输出错后重发一位就可以了。所以要比并口快。
多线程
主线程,一般来协调管理,工作线程工作,一般是这样
当使用start方法启动线程后,进程内有多个活动的线程并行的工作,就是多线程。
一个进程至少有一个线程,并作为程序的入口,这个线程就是主线程。进程至少有一个主线程
函数在不同的线程中压栈,每次调用都是不同的对象。
线程安全
在IPython中演示可以看到效果,python命令行,pycharm都不能
import threading def worker(): for x in range(100): print("{} is running.".format(threading.current_thread().name)) for x in range(1,5): name = "worker{}".format(x) t = threading.Thread(name=name,target=worker) t.start()
产生以上问题的原因,就是在print函数执行时,函数是分两段执行的,先打印内容,再打印换行,在这之间发生了线程的切换
这说明print函数是线程不安全的
线程执行一段代码,不会产生不确定结果,那么这段代码就是线程安全的
解决可以使print打印字符串这个不可变的整体,而将print换行,或者使用logging模块,日志处理模块,生产环境代码都使用logging
import threading import logging,time def worker(): for x in range(10): time.sleep(0.3) #print("{} is running. ".format(threading.current_thread().name), end='') logging.info("{} is running.".format(threading.current_thread().name)) for x in range(1,5): name = "worker{}".format(x) t = threading.Thread(name=name,target=worker) t.start()
logging info 级别低于经警告
daemon线程和non-daemon线程
deamon不是Linux中的守护进程,有翻译之为后台进程,但感觉不够准确。
主线程退出时,看工作线程的daemon,daemon为False就等待。
只在主线程结束时才看daemon,有non-daemon就等待non-daemon所在的线程结束后结束主线程。
主线程是第一个启动的线程
父线程:如果线程A中启动了线程B,A就是B的父线程
子线程:B就是A的子线程
python中构建线程时,可以设置daemon的属性,这个属性必须在start方法前设置好。
没给定deamon,用父线程的deamon,python中在构建Thread时,会根据是否提供daemon,来判断是否是daemon线程
#源码中__init__方法中
if daemon is not None: self._daemonic = daemon #用户传入的bool值 else: self._daemonic = current_thread().daemon
import threading,time def foo(n): for i in range(n): print(i) time.sleep(1) t1 = threading.Thread(target=foo,args=(10,),daemon=True) t1.start() t2 = threading.Thread(target=foo,args=(20,),daemon=False) t2.start()
daemon属性 表示线程是否时daemon属性,这个值必须在start之前设置,否则引发RuntimeError异常
isDaemon() 是否是daemon线程
setDaemon 设置为daemon线程,必须在start方法之前设置
要点:
线程都有daemon属性,不设置默认为None,设置为True 或者False
不设置daemon,就取父线程的daemon来设置
主线程是non-daemon线程,即daemon = False
从主线程创建的所有线程,不设置dameon就都会取主线程的daemon,也就是non-daemon。
python中主线程只有在没有活着的non-daemon时才会退出,否则只有等待。
daemon的应用场景
daemon 的出现可以简化程序员工作,让他们不用去记录和管理那些后台线程。
这个daemon概念,就是可以把线程设置为随主线程结束而结束的线程。
主要应用场景有:
1、后台任务,如发送心跳包、监控,这种场景最多
2、主线程工作才有用到线程,应当随主线程的结束而结束,比如主线程中的公共资源,如果主线程退出,那么工作线程使用的资源也就没有用了,一起随主线程结束。
3、随时可以被终止的程序,如果主线程退出,想要其他工作线程一起退出。比如开启线程定时判断WEB情况的线程,没有必要一直记得,只要设置成daemon,就可以在关闭主线程是结束它
这样可以简化程序员手动关闭线程。
join 方法
import time import threading def foo(n): for i in range(n): print(i) time.sleep(1) t1 = threading.Thread(target=foo,args=(10,),daemon=True) t1.start() t1.join() print("Main Thread Exiting")
上例中,主线程调用t1的join后,主线程被阻塞了,等daemon线程执行完了,主线程才退出。
join(timeout=None)是线程的标准方法,
一个线程可以被join多次
一个线程调用另一个线程的join,就是调用者被阻塞,直到被调用的线程结束。
timeout可以指定调用者等待的时间,没有设置就是None,一直等到线程结束。
调用谁的join方法,就是要在调用的线程里等谁。
如果在主线程中,join 方法可以使daemon失效,即使是daemon线程,也要等到运行完才结束。
threading.local类
import time import threading class A: def __init__(self): self.x = 0 global_data = A() def work(): #x = 0 ## x不是全局的,能实现分别计算 #global x ## 全局变量下,会在一个变量上一直计算 global_data.x = 0 ##等同于上一个,该实例的属性x只有一个 for i in range(100): time.sleep(0.0001) global_data.x += 1 print(threading.current_thread(),global_data.x) for i in range(10): threading.Thread(target=work).start() print("Main Thread Exiting")
使用多线程,每个线程完成不同的计算任务,x是局部变量时,互不干扰
如果将x设置为全局变量就会相互干扰
python提供了threading.local类,将这个实例化得到一个全局对象,对于不同的线程使用该对象存储的数据对其他线程不可见。
import time import threading global_data = threading.local() def work(): global_data.x = 0 #每个线程调用的对象都不是同一个,只是名字相同 for i in range(100): time.sleep(0.0001) global_data.x += 1 print(threading.current_thread(),global_data.x) for i in range(10): threading.Thread(target=work).start() print("Main Thread Exiting")
执行结果和使用局部变量的效果是一样的。
log = [] mydata = threading.local() mydata.x = 123 def f(): items = sorted(mydata.__dict__.items()) log.append(items) mydata.number = 11 log.append(mydata.number) print(mydata.number) print(mydata.x) ##会报错,线程不共享该local对象在其他线程的属性,该属性与该线程绑定 import threading thread = threading.Thread(target=f) thread.start() thread.join() print(log) print(mydata.number) ##会报错,线程不共享其他线程内对象的属性
定时器 Timer延迟执行
类,延迟执行,继承自Thread,用来定义延迟多久之后执行一个函数
Timer提供了cancel方法,重写了run函数,就是Thread的子类,就是线程类,
cancal方法本质上是Event类实现,只有线程执行的目标函数未执行前,才能cancal