多线程操作 协程
- 进程和线程的比较
- GIL全局解释器锁(重要理论)
- 互斥锁
- 线程队列(线程里使用队列)
- 进程池和线程池的用法
- 协程理论
- 如何使用协程
- 基于协程的高并发城程序
进程和线程的比较
1. 进程的开销比线程的开销大很多
2. 进程之间的数据是隔离的,但是,线程之间的数据不隔离
3. 多个进程之间的线程数据不共享----->还是让进程通信(IPC)------->进程下的线程也通信了---->队列
GIL全局解释器锁(重要理论)
首先需要明确的一点是 GIL 并不是 Python 的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比 C++ 是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码
GIL全局解释器锁是存在于CPython中
GIL保护的是解释器级的数据
了解前提
在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
python解释器的种类
CPython
这个解释器是用C语言开发的
当前市场使用的最多(95%)的解释器就是CPython解释器
GIL全局解释器锁是存在于CPython中
IPython
IPython是基于CPython之上的一个交互式解释器
IPython只是在交互方式上有所增强,但是执行Python代码的功能和CPython是完全一样的,好比很多国产浏览器虽然外观不同,但内核其实是调用了IE。
PyPy
PyPy是另一个Python解释器,它的目标是执行速度,PyPy采用JIT技术,对Python代码进行动态编译,所以可以显著提高Python代码的执行速度
Jython
Jython是运行在Java平台上的Python解释器,可以直接把Python代码编译成Java字节码执行。
IronPython
IronPython和Jython类似,只不过IronPython是运行在微软.Net平台上的Python解释器,可以直接把Python代码编译成.Net的字节码。
对Python解释器的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线
程在运行"
介绍
GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。
可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。
首先确定一点:每次执行python程序,都会产生一个独立的进程。
在一个python的进程内,不仅有由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程
总之,所有线程都运行在这一个进程内,毫无疑问
想避免的问题是,出现多个线程抢夺资源的情况
怎么避免的这个问题,那就是在Python这门语言设计之处,就直接在解释器上添加了一把锁,这把锁就是为了让统一时刻只有一个线程在执行,言外之意就是哪个线程想执行,就必须先拿到这把锁(GIL), 只有等到这个线程把GIL锁释放掉,别的线程才能拿到,然后具备了执行权限.
得出结论:GIL锁就是保证在统一时刻只有一个线程执行,所有的线程必须拿到GIL锁才有执行权限
互斥锁
在多线程的情况下,同时执行一个数据,会发生数据错乱的问题
原理
拿时间换空间,空间换时间 时间复杂度
创建互斥锁
# 导入线程threading模块
import threading
# 创建互斥锁
mutex = threading.Lock()
锁定资源/解锁资源
acquire() — 锁定资源,此时资源是锁定状态,其他线程无法修改锁定的资源,直到等待锁定的资源释放之后才能操作;
release() — 释放资源,也称为解锁操作,对锁定的资源解锁,解锁之后其他线程可以对资源正常操作;
互斥锁例子
from threading import Thread
from threading import Lock
import time
n = 10
def task(lock):
lock.acquire()
global n
temp = n
time.sleep(0.5)#停顿0.5s
n = temp - 1
lock.release()
if __name__ == '__main__':
tt = []#存放线程
lock=Lock()
for i in range(10):#10多线程
t = Thread(target=task, args=(lock, ))
t.start()
tt.append(t)#添加到tt列表
for j in tt:
j.join()
print("主", n)
注意:互斥锁一旦锁定之后要记得解锁,否则资源会一直处于锁定状态;
面试题:既然有了GIL锁,为什么还要互斥锁? (多线程下)
比如:我起了2个线程,来执行a=a+1,a一开始是0
1. 第一个线程来了,拿到a=0,开始执行a=a+1,这个时候结果a就是1了
2. 第一个线程得到的结果1还没有赋值回去给a,这个时候,第二个线程来了,拿到的a是0,继续执行
a=a+1结果还是1
3. 加了互斥锁,就能够解决多线程下操作同一个数据,发生错乱的问题
线程队列
为什么线程中还有使用队列?
同一个进程下多个线程数据是共享的
为什么先同一个进程下还会去使用队列呢
因为队列是
管道 + 锁
所以用队列还是为了保证数据的安全
线程队列:
1. 先进先出
2. 后进先出
3. 优先级的队列
-
先进先出
import queue
#先进先出
q=queue.Queue() # 理论无限大、
q.put('first')
q.put('second')
q.put('third')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
-
后进先出(queue.LifoQueue())
import queue
# Lifo:last in first out
q=queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
-
优先级的队列(queue.PriorityQueue())
## 优先级队列
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())
进程池和线程池的用法
池的概念
池是保证计算机能够正常运行的前提下,能最大限度开采的资源的量,他降低的程序的运行效率,但是保证了计算机硬件的安全,从而让你写的程序可以正常运行。池就是一个量的单位,代表了你所能开采的资源的度,超过这个度,身体就跨了,首先要用,其次要有度。
进程池
提前定义好一个池子,然后,往这个池子里面添加进程,以后,只需要往这个进程池里面丢任务就行了,然后,有这个进程池里面的任意一个进程来执行任务
"""开进程池"""
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
def task(n, m):
return n+m
def task1():
return {'username':'kevin', 'password':123}
def callback(res):
print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
print(res.result()) # 3
def callback1(res):
print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
print(res.result()) # {'username': 'kevin', 'password': 123}
print(res.result().get('username'))
if __name__ == '__main__':
pool=ProcessPoolExecutor(3) # 定义一个进程池,里面有3个进程
## 2. 往池子里面丢任务
pool.submit(task, m=1, n=2).add_done_callback(callback)#将task丢到callbak
pool.submit(task1).add_done_callback(callback1)
pool.shutdown() # join + close
print(123)
线程池
提前定义好一个池子,然后,往这个池子里面添加线程,以后,只需要往这个线程池里面丢任务就行了,然后,有这个线程池里面的任意一个线程来执行任务
"""开进程/线池"""
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
def task(n, m):
return n+m
def task1():
return {'username':'kevin', 'password':123}
def callback(res):#回调
print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
print(res.result()) # 3
def callback1(res):
print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
print(res.result()) # {'username': 'kevin', 'password': 123}
print(res.result().get('username'))
if __name__ == '__main__':
# pool=ProcessPoolExecutor(3) # 定义一个进程池,里面有3个进程
pool=ThreadPoolExecutor(3)#定义一个线程池,里面有3个进程
## 2. 往池子里面丢任务
pool.submit(task, m=1, n=2).add_done_callback(callback)#将task丢到callbak
pool.submit(task1).add_done_callback(callback1)
pool.shutdown() # join + close
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)
我们的宗旨是:保证硬件能够正常工作的情况下,最大限度的利用资源。
协程理论
介绍
协程是基于单线程来实现并发,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发,为此我们需要先回顾下并发的本质:切换+保存状态
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
强调:
#1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
#2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
协程:
单线程实现并发
在应用程序里控制多个任务的切换+保存状态
优点:
应用程序级别速度要远远高于操作系统的切换
缺点:
多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地
该线程内的其他的任务都不能执行了
一旦引入协程,就需要检测单线程下所有的IO行为,
实现遇到IO就切换,少一个都不行,因为一旦一个任务阻塞了,整个线程就阻塞了,
其他的任务即便是可以计算,但是也无法运行了
协程特点:
1.必须在只有一个单线程里实现并发
2.修改共享数据不需加锁
3.用户程序里自己保存多个控制流的上下文栈
4.附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
目的
想要在单线程下实现并发
并发指的是多个任务看起来是同时运行的
并发 = 切换 + 保存状态
协程是最节省资源的,进程是最消耗资源的,其次是线程
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。
如何使用协程
pip install gevent
import gevent
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是*Greenlet*, 它是以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的返回值
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')
start_time = time.time()
g1=gevent.spawn(eat)
g2=gevent.spawn(play)
# gevent.joinall([g1,g2])
g1.join()
g2.join()
print('主', time.time() - start_time)
协程实现高并发
服务端:
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()