前言
面试题仅做学习参考,学习者阅后也要用心钻研其中的原理,重要知识需要系统学习、透彻学习,形成自己的知识链。以下五点建议希望对您有帮助,早日拿到一份心仪的offer。
- 做好细节工作,细致的人运气不会差
- 展现特别可以,但要建立在已充分展示实力的基础上
- 真诚比圆滑重要,请真诚地回答问题
- 把握当下,考场外的表现能起的作用微乎其微
- 没有通过不代表你不优秀,选人先考虑的是与岗位相匹配
Python 三程三器
- 进程
- 进程是资源分配的最小单位(内存、CPU、网络、io)
- 一个运行起来的程序就是一个进程
- 什么是程序(程序使我们存储在硬盘里的代码)
- 硬盘(256G)、内存条(8G)
- 当我们双击一个图标、打开程序的时候,实际上就是通过IO(读写)内存条里面
- 内存条就是我们所指的资源
- CPU分时
- CPU比你的手速快多了,分时处理每个线程,但是由于太快然你觉得每个线程都是独占cpu
- cpu是计算,只有时间片到了,获取cpu,线程真正执行
- 当你想使用 网络、磁盘等资源的时候,需要cpu的调度
- 进程具有独立的内存空间,所以没有办法相互通信
- 进程如何通信
- 进程queue(父子进程通信)
- pipe(同一程序下两个进程通信)
- managers(同一程序下多个进程通信)
- RabbitMQ、redis等(不同程序间通信)
- 进程如何通信
- 为什么需要进程池
- 一次性开启指定数量的进程
- 如果有十个进程,有一百个任务,一次可以处理多少个(一次性只能处理十个)
- 防止进程开启数量过多导致服务器压力过大
- 线程
- 有了进程为什么还需要线程
- 因为进程不能同一时间只能做一个事情
- 什么是线程
- 线程是操作系统调度的最小单位
- 线程是进程正真的执行者,是一些指令的集合(进程资源的拥有者)
- 同一个进程下的读多个线程共享内存空间,数据直接访问(数据共享)
- 为了保证数据安全,必须使用线程锁
- GIL全局解释器锁
- 在python全局解释器下,保证同一时间只有一个线程运行
- 防止多个线程都修改数据
- 线程锁(互斥锁)
- GIL锁只能保证同一时间只能有一个线程对某个资源操作,但当上一个线程还未执行完毕时可能就会释放GIL,其他线程就可以操作了
- 线程锁本质把线程中的数据加了一把互斥锁
- mysql中共享锁 & 互斥锁
- mysql共享锁:共享锁,所有线程都能读,而不能写
- mysql排它锁:排它,任何线程读取这个这个数据的权利都没有
- 加上线程锁之后所有其他线程,读都不能读这个数据
- mysql中共享锁 & 互斥锁
- 有了GIL全局解释器锁为什么还需要线程锁
- 因为cpu是分时使用的
- 死锁定义
- 两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去
- 多线程
- 为什么要使用多线程?
- 线程在程序中是独立的、并发的执行流。与分隔的进程相比,进程中线程之间的隔离程度要小,它们共享内存、文件句柄
- 和其他进程应有的状态。
- 因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程之中拥有独立的内存单元,而多个线程共享
- 内存,从而极大的提升了程序的运行效率。
- 线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性,多个线程共享一个进程的虚拟空间。线程的共享环境
- 包括进程代码段、进程的共有数据等,利用这些共享的数据,线程之间很容易实现通信。
- 操作系统在创建进程时,必须为改进程分配独立的内存空间,并分配大量的相关资源,但创建线程则简单得多。因此,使用多线程来实现并发比使用多进程的性能高得要多。
- 总结起来,使用多线程编程具有如下几个优点:?
- 进程之间不能共享内存,但线程之间共享内存非常容易。
- 操作系统在创建进程时,需要为该进程重新分配系统资源,但创建线程的代价则小得多。因此使用多线程来实现多任务并发执行比使用多进程的效率高
- python语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了python的多线程编程。
- 为什么要使用多线程?
- 有了进程为什么还需要线程
import threading import time def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) for i in range(10): t = threading.Thread(target=sayhi,args=('t-%s'%i,)) t.start()
-
- join/setDaemon
- 当一个进程启动之后,会默认产生一个主线程,因为线程是程序执行流的最小单元,当设置多线程时,主线程会创建多个子线程,在python中,默认情况下(其实就是setDaemon(False)),主线程执行完自己的任务以后,就退出了,此时子线程会继续执行自己的任务,直到自己的任务结束。
- 当我们使用setDaemon(True)方法,设置子线程为守护线程时,主线程一旦执行结束,则全部线程全部被终止执行,可能出现的情况就是,子线程的任务还没有完全执行结束,就被迫停止。此时join的作用就凸显出来了,join所完成的工作就是线程同步,即主线程任务结束之后,进入阻塞状态,一直等待其他的子线程执行结束之后,主线程在终止。
- join有一个timeout参数:
- 当设置守护线程时,含义是主线程对于子线程等待timeout的时间将会杀死该子线程,最后退出程序。所以说,如果有10个子线程,全部的等待时间就是每个timeout的累加和。简单的来说,就是给每个子线程一个timeout的时间,让他去执行,时间一到,不管任务有没有完成,直接杀死。
- 没有设置守护线程时,主线程将会等待timeout的累加和这样的一段时间,时间一到,主线程结束,但是并没有杀死子线程,子线程依然可以继续执行,直到子线程全部结束,程序退出。线程池
- 对于任务数量不断增加的程序,每有一个任务就生成一个线程,最终会导致线程数量的失控,例如,整站爬虫,假设初始只有一个链接a,那么,这个时候只启动一个线程,运行之后,得到这个链接对应页面上的b,c,d,,,等等新的链接,作为新任务,这个时候,就要为这些新的链接生成新的线程,线程数量暴涨。在之后的运行中,线程数量还会不停的增加,完全无法控制。所以,对于任务数量不端增加的程序,固定线程数量的线程池是必要的。
- join/setDaemon
import threading import time start_time = time.time() def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) t_objs = [] #将进程实例对象存储在这个列表中 for i in range(50): t = threading.Thread(target=sayhi,args=('t-%s'%i,)) t.start() #启动一个线程,程序不会阻塞 t_objs.append(t) print(threading.active_count()) #打印当前活跃进程数量 for t in t_objs: #利用for循环等待上面50个进程全部结束 t.join() #阻塞某个程序 print(threading.current_thread()) #打印执行这个命令进程 print("----------------all threads has finished.....") print(threading.active_count()) print('cost time:',time.time() - start_time)
import threading import time start_time = time.time() def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) for i in range(50): t = threading.Thread(target=sayhi,args=('t-%s'%i,)) t.setDaemon(True) #把当前线程变成守护线程,必须在t.start()前设置 t.start() #启动一个线程,程序不会阻塞 print('cost time:',time.time() - start_time)
import requests from concurrent.futures import ThreadPoolExecutor def fetch_request(url): result = requests.get(url) print(result.text) url_list = [ 'https://www.baidu.com', 'https://www.google.com/', #google页面会卡住,知道页面超时后这个进程才结束 'http://dig.chouti.com/', #chouti页面内容会直接返回,不会等待Google页面的返回 ] pool = ThreadPoolExecutor(10) # 创建一个线程池,最多开10个线程 for url in url_list: pool.submit(fetch_request,url) # 去线程池中获取一个线程,线程去执行fetch_request方法 pool.shutdown(True)
- 协程
- 什么是协程
- 协程微线程,纤程,本质是一个单线程
- 协程能在单线程处理高并发
- 线程遇到I/O操作会等待、阻塞,协程遇到I/O会自动切换(剩下的只有CPU操作)
- 线程的状态保存在CPU的寄存器和栈里而协程拥有自己的空间,所以无需上下文切换的开销,所以快、
- 为甚么协程能够遇到I/O自动切换
- 协程有一个gevent模块(封装了greenlet模块),遇到I/O自动切换
- 协程缺点
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上
- 线程阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
- 协程最大的优点
- 不仅是处理高并发(单线程下处理高并发)
- 特别节省资源(500日活,用php写需要两百多态机器,但是golang只需要二十多太机器)
- 200多台机器一年
- 二十多天机器一年
- 协程为何能处理大并发
- greeenlet遇到I/O手动切换
- 协程遇到I/O操作就切换,其实Gevent模块仅仅是对greenlet的封装,将I/O的手动切换变成自动切换
- 协程之所以快是因为遇到I/O操作就切换(最后只有CPU运算)
- 这里先演示用greenlet实现手动的对各个协程之间切换
- 其实Gevent模块仅仅是对greenlet的再封装,将I/O间的手动切换变成自动切换
- 什么是协程
from greenlet import greenlet def test1(): print(12) #4 gr1会调用test1()先打印12 gr2.switch() #5 然后gr2.switch()就会切换到gr2这个协程 print(34) #8 由于在test2()切换到了gr1,所以gr1又从上次停止的位置开始执行 gr2.switch() #9 在这里又切换到gr2,会再次切换到test2()中执行 def test2(): print(56) #6 启动gr2后会调用test2()打印56 gr1.switch() #7 然后又切换到gr1 print(78) #10 切换到gr2后会接着上次执行,打印78 gr1 = greenlet(test1) #1 启动一个协程gr1 gr2 = greenlet(test2) #2 启动第二个协程gr2 gr1.switch() #3 首先gr1.switch() 就会去执行gr1这个协程
-
-
- Gevent遇到I/O自动切换
- Gevent是一个第三方库,可以通过gevent实现并发同步或异步编程,Gevent原理是只要是遇到I/O操作就自动切换下一个协程
- Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程
- 在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程
- Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
- Gevent原理是只要遇到I/O操作就会自动切换到下一个协程
-
from urllib import request import gevent,time from gevent import monkey monkey.patch_all() #把当前程序所有的I/O操作给我单独做上标记 def f(url): print('GET: %s' % url) resp = request.urlopen(url) data = resp.read() print('%d bytes received from %s.' % (len(data), url)) #1 并发执行部分 time_binxing = time.time() gevent.joinall([ gevent.spawn(f, 'https://www.python.org/'), gevent.spawn(f, 'https://www.yahoo.com/'), gevent.spawn(f, 'https://github.com/'), ]) print("并行时间:",time.time()-time_binxing) #2 串行部分 time_chuanxing = time.time() urls = [ 'https://www.python.org/', 'https://www.yahoo.com/', 'https://github.com/', ] for url in urls: f(url) print("串行时间:",time.time()-time_chuanxing) # 注:为什么要在文件开通使用monkey.patch_all() # 1. 因为有很多模块在使用I / O操作时Gevent是无法捕获的,所以为了使Gevent能够识别出程序中的I / O操作。 # 2. 就必须使用Gevent模块的monkey模块,把当前程序所有的I / O操作给我单独做上标记 # 3.使用monkey做标记仅用两步即可: 第一步(导入monkey模块): from gevent import monkey 第二步(声明做标记) : monkey.patch_all()
- Gevent实现简单的自动切换小例子
- 注:在Gevent模仿I/O切换的时候,只要遇到I/O就会切换,哪怕gevent.sleep(0)也要切换一次
import gevent def func1(): print('