进程和线程的比较
GIL全局解释器锁(重要理论)
互斥锁
线程队列(线程里使用队列)
进程池和线程池的用法
协程理论
如何使用协程
基于协程的高并发城程序
1.进程的开销比线程的开销大很多
2.进程之间的数据是隔离的,但是,线程之间的数据不隔离
3.多个进程之间的线程数据不共享----->还是让进程通信(IPC)------>进程下的线程也通信了----->队列
python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然python解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行
对python解释器的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行
背景
1.python代码运行在解释器上,由解释器来执行或解释
2.python解释器的种类:CPython IPython PyPy Jython IronPython
3.当前市场使用的最多的解释器就是CPython解释器
4.GIL全局解释器锁是存在于CPython中
5.结论是同一时刻只有一个线程在执行? 想避免的问题是,出现多个线程抢夺资源的情况
比如:现在起一个线程,来回收垃圾数据,回收a=1这个变量,另外一个线程也要使用这个变量a,当垃圾回收线程还没没有把变量a回收完毕,另一个线程就来抢夺这个变量a使用。
怎么避免的这个问题,那就是在Python这门语言设计之处,就直接在解释器上添加了一把锁,这把锁就是为了让统一时刻只有一个线程在执行,言外之意就是哪个线程想执行,就必须先拿到这把锁(GIL), 只有等到这个线程把GIL锁释放掉,别的线程才能拿到,然后具备了执行权限.
“GIL锁就是保证在同一时刻只有一个线程执行,所有的线程必须拿到GIL锁才有执行权限”
记忆问题
1.python有GIL锁的原因,同一个进程下多个线程实际上同一时刻,只有一个线程在执行
2.只有python上开进程用的多,其他语言一般不开多进程,只开多线程就够了
3.cpython解释器开多线程不能利用多核优势,只有开多进程才能利用多核优势,其他语言不存在这个问题
4.8核cpu电脑,充分利用8核,至少起8个线程,8条线程全是计算---->计算机cpu使用率是100%
5.如果不存在GIL锁,一个进程下,开启8个线程,它能够充分利用cpu资源,跑满cpu
6.cpython解释器中好多代码,模块都是基于GIL锁机制写起来的,改不了了--->我们不能有8个核,但我现在只能用1核---->开启多进程---->每个进程下开启的线程,可以被多个cpu调度执行
7.cpython解释器:io密集型使用多线程,计算机密集型使用多进程
io密集型,遇到io操作会切换cpu,假设开启了8个线程,8个线程都有io操作----> io操作不消耗cpu--->一段时间看上去,其实8个线程都执行了,选多线程好一些
计算机密集型,消耗cpu,如果开了8个线程,第一个线程会一直占着cpu,而不会调度到其他线程执行,其他7个线程根本没执行,所以我们开8个线程,每个进程有一个线程,8个进程下的线程会被8个cpu执行,从而效率高
计算密集型选多进程好一些,在其他语言中,都是选择多线程,而不是多进程
在多线程的情况下,同时执行一个数据,会发生数据错乱的问题
- n = 10
- from threading import Lock
- import time
- def task():
- global n
- temp = n
- time.sleep(0.5)
- n = temp - 1
- lock.release()
-
- from threading import Thread
-
- if __name__ == '__main__':
- tt = []
-
- for i in range(10):
- t = Thread(target=task,)
- t.start()
- tt.append(t)
- for j in tt:
- j.join()
- print("主",n)
- # 主 9
-
- n = 10
- from threading import Lock
- import time
- def task():
-
- global n
- temp = n
- time.sleep(0.5)
- n = temp - 1
-
-
-
- """拿时间换空间,空间换时间 时间复杂度"""
- from threading import Thread
-
- if __name__ == '__main__':
- tt = []
-
- for i in range(10):
- t = Thread(target=task, )
- t.start()
- tt.append(t)
- for j in tt:
- j.join()
-
- print("主", n)
- # 主,0
-
面试题:既然有了GIL锁,为什么还要互斥锁?(多线程下)
比如:起了2个线程,来执行a=a+1,a一开始是0
1.第一个线程来了,拿到0,开始执行a=a+1,这个时候结果a就是1了
2.第一个线程得到的结果1还没有赋值回去给a,这个时候,第二个线程来了,拿到a=0,继续执行 a=a+1
3.加了互斥锁,就能解决多线程下操作同一个数据,发生错乱的问题
同一个进程下多个线程数据是共享的,为什么先同一个进程下还会去使用队列呢?
因为队列是 管道+锁 所以用队列还是为了保证数据的安全
- class queue.Queue(maxsize=0)
- import queue
-
- q=queue.Queue()
- q.put('first')
- q.put('second')
- q.put('third')
-
- print(q.get())
- print(q.get())
- print(q.get())
- '''
- 结果(先进先出):
- first
- second
- third
- '''
- 进程Queue用于父进程与子进程(或同一父进程中多个子进程)间数据传递
- python自己的多个进程间交换数据或者与其他语言(如Java)进程queue就无能为力
-
- queue.Queue 的缺点是它的实现涉及到多个锁和条件变量,因此可能会影响性能和内存效率。
- class queue.LifoQueue(maxsize=0)
- import queue
-
- q=queue.LifoQueue()
- q.put('first')
- q.put('second')
- q.put('third')
-
- print(q.get())
- print(q.get())
- print(q.get())
- '''
- 结果(后进先出):
- third
- second
- first
- '''
class queue.PriorityQueue(maxsize=0)
- import queue
-
- q=queue.PriorityQueue()
- #put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
- q.put((20,'a'))
- q.put((10,'b'))
- q.put((30,'c'))
-
- print(q.get())
- print(q.get())
- print(q.get())
- '''
- 结果(数字越小优先级越高,优先级高的优先出队):
- (10, 'b')
- (20, 'a')
- (30, 'c')
- '''
进程池:提前定义好一个池子,然后,往这个池子里面添加进程,以后,只需要往这个进程池里面丢任务就行了,然后,有这个进程池里面的任意一个进程来执行任务
线程池:提前定义好一个池子,然后,往这个池子里面添加线程,以后,只需要往这个线程池里面丢任务就行了,然后,有这个线程池里面的任意一个线程来执行任务
- def task(n, m):
- return n + m
-
-
- from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
-
-
- def callback(res):
-
- print(res.result())
-
-
- if __name__ == '__main__':
- pool = ProcessPoolExecutor(3)
- pool.submit(task, m=1, n=2).add_done_callback(callback)
- pool.shutdown()
- print(123)
- import requests
-
- def get_page(url):
- res=requests.get(url)
- name=url.rsplit('/')[-1]+'.html'
- return {'name':name,'text':res.content}
-
- def call_back(fut):
- print(fut.result()['name'])
- with open(fut.result()['name'],'wb') as f:
- f.write(fut.result()['text'])
-
-
- if __name__ == '__main__':
- pool=ThreadPoolExecutor(2)
- urls=['http://www.baidu.com','http://www.cnblogs.com','http://www.taobao.com']
- for url in urls:
- pool.submit(get_page,url).add_done_callback(call_back)
协程:是单线程下的并发,又称微线程。一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
需要强调的是:
1.python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
2.单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(非io操作的切换与效率无关)
对比操作系统控制线程的切换,用户在单线程内控制协程的切换
优点如下:
1.协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2.单线程内就可以实现并发效果,最大限度利用cpu
缺点如下:
1.协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2.协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
总结协程特点:
1.必须在只有一个单线程里实现开发
2.修改共享数据不需要加锁
3.用户程序里自己保存多个控制流的上下文栈
4.附加:一个协程遇到io操作自动切换到其他协程
- 服务端:
- from gevent import monkey;
-
- monkey.patch_all()
- import gevent
- from socket import socket
- # from multiprocessing import Process
- from threading import Thread
-
-
- def talk(conn):
- while True:
- try:
- data = conn.recv(1024)
- if len(data) == 0: break
- print(data)
- conn.send(data.upper())
- except Exception as e:
- print(e)
- conn.close()
-
-
- def server(ip, port):
- server = socket()
- server.bind((ip, port))
- server.listen(5)
- while True:
- conn, addr = server.accept()
- # t=Process(target=talk,args=(conn,))
- # t=Thread(target=talk,args=(conn,))
- # t.start()
- gevent.spawn(talk, conn)
-
-
- if __name__ == '__main__':
- g1 = gevent.spawn(server, '127.0.0.1', 8080)
- g1.join()
-
- 客户端:
- import socket
- from threading import current_thread, Thread
-
-
- def socket_client():
- cli = socket.socket()
- cli.connect(('127.0.0.1', 8080))
- while True:
- ss = '%s say hello' % current_thread().getName()
- cli.send(ss.encode('utf-8'))
- data = cli.recv(1024)
- print(data)
-
-
- for i in range(5000):
- t = Thread(target=socket_client)
- t.start()
拥有在模块运行时替换的功能,例如:一个函数对象赋值给另外一个函数对象(把函数原本的执行的功能给替换了)
- class Monkey():
- def play(self):
- print('猴子在玩')
-
- class Dog():
- def play(self):
- print('狗子在玩')
- m=Monkey()
- m.play()
- m.play=Dog().play
- m.play()
这里有一个比较实用的例子,很多用到import json,后来发现ujson性能更高,如果觉得把每个文件的import json改成import ujson as json成本较高,或者说想测试一下ujson替换是否符合预期,只需要在入口加上:
- import json
- import ujson
-
- def monkey_patch_json():
- json.__name__ = 'ujson'
- json.dumps = ujson.dumps
- json.loads = ujson.loads
- monkey_patch_json()
- aa=json.dumps({'name':'lqz','age':19})
- print(aa)
gevent是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是gevent,它是以C扩展模块形式接入Python的轻量级协程。Greenlet全部运行在主程序操作系统进程内部,但它们被协作式地调度
- #用法
- g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
-
- g2=gevent.spawn(func2)
-
- g1.join() #等待g1结束
-
- g2.join() #等待g2结束
-
- #或者上述两步合作一步:gevent.joinall([g1,g2])
-
- g1.value#拿到func1的返回值
- import gevent
- def eat(name):
- print('%s eat 1' %name)
- gevent.sleep(2)
- print('%s eat 2' %name)
-
- def play(name):
- print('%s play 1' %name)
- gevent.sleep(1)
- print('%s play 2' %name)
-
-
- g1=gevent.spawn(eat,'lqz')
- g2=gevent.spawn(play,name='lqz')
- g1.join()
- g2.join()
- #或者gevent.joinall([g1,g2])
- print('主')
- '''
- 上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,
- 而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了
- from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前
- 或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头
- '''
- from gevent import monkey;monkey.patch_all()
-
- import gevent
- import time
- def eat():
- print('eat food 1')
- time.sleep(2)
- print('eat food 2')
-
- def play():
- print('play 1')
- time.sleep(1)
- print('play 2')
-
- g1=gevent.spawn(eat)
- g2=gevent.spawn(play_phone)
- gevent.joinall([g1,g2])
- print('主')
-
- # 我们可以用threading.current_thread().getName()来查看每个g1和g2,查看的结果为DummyThread-n,即假线程