多线程
多线程类似于同时执行多个不同程序
- 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
- 指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
- 线程可以被抢占(中断)。
- 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) 这就是线程的退让。
优点
- 使用线程可以把占据长时间的程序中的任务放到后台去处理。
- 用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
- 程序的运行速度可能加快。
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。
分类
- 内核线程:由操作系统内核创建和撤销。
- 用户线程:不需要内核支持而在用户程序中实现的线程。
步骤
- 新建
- 就绪
- 运行
- 阻塞
- 结束
import threading
from time import sleep
def download(n):
img = ['a.jpg','b.jpg','c.jpg']
for i in img:
print('downloading:',i)
sleep(n)#让出cpu使用权
print('donei',i)
def listenmusic():
musics=['11','22','33','44']
for m in musics:
print('listening:',m)
sleep(0.5)
print('donem',m)
if __name__ == '__main__':
t1 = threading.Thread(target=download,name='aa',args=(1,))
t1.start()
t2 = threading.Thread(target=listenmusic,name='aa')
t2.start()
"""
downloading: a.jpg
listening: 11
donem 11
listening: 22
donei a.jpg
downloading: b.jpg
donem 22
listening: 33
donem 33
listening: 44
donei b.jpg
downloading: c.jpg
donem 44
donei c.jpg
Process finished with exit code 0
"""
共享数据
线程可以共享全局变量,当共享数据时要考虑数据的安全问题。
共享数据:
- 如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个进程进行同步
同步:
一个一个完成,一个做完另一个才能进来。效率会降低
多线程的优势在于可以同时运行多个任务(python的多线程是伪多线程)
import threading
money =1000
def run1():
global money
for i in range(100):
money-=1
if __name__ == '__main__':
t1 = threading.Thread(target=run1,name='aa')
t2 = threading.Thread(target=run1,name='bb')
t1.start()
t2.start()
t1.join()
t2.join()
print(money)
#800
锁
当线程需要共享数据时,可能存在数据不同步的问题。为了避免这种情况,引入锁的概念。
python多线程存在全局解释器锁GIL,为保证数据的安全,线程默认加锁。一个线程执行完后才允许另一个线程执行,加锁后线程同步,速度慢但数据安全。
当数据运算足够大时,会默认把锁释放,此时数据不再安全。
import threading
money =0
def run1():
global money
for i in range(10000000):
money+=1
print('1:',money)
def run2():
global money
for i in range(10000000):
money+=1
print('2:',money)
if __name__ == '__main__':
t1 = threading.Thread(target=run1,name='aa')
t2 = threading.Thread(target=run2,name='bb')
t1.start()
t2.start()
t1.join()
t2.join()
print(money)
"""
1: 11693709
2: 12647085
12647085
"""
当计算密集计算量大时使用进程。
当有耗时操作时使用线程。如爬虫,下载,I/O操作等
线程同步
使用Thread对象的Lock和Rlock可以实现简单的线程同步
- lock.acquire()阻塞
- lock.release()释放
由于python默认上锁,所以在数据量运算小的情况下写不写lock都是带锁的
import threading
import time
lock = threading.Lock()
l1 = [0]*10
def task1():
#获取线程锁,如果以及上锁,则阻塞等待锁释放
lock.acquire()#阻塞
for i in range(len(l1)):
l1[i]=1
time.sleep(0.5)
lock.release()
def task2():
lock.acquire()#阻塞
for i in range(len(l1)):
print('-------->',l1[i])
time.sleep(0.5)
lock.release()
if __name__ == '__main__':
t1 = threading.Thread(target=task1)
t2 = threading.Thread(target=task2)
t2.start()
t1.start()
t2.join()
t1.join()
print(l1)
"""
--------> 0
--------> 0
--------> 0
--------> 0
--------> 0
--------> 0
--------> 0
--------> 0
--------> 0
--------> 0
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
"""
死锁
如果两个线程分别占用一部分资源并同时等待对方的资源,就会造成死锁
避免死锁方法
- 重构代码
- lock.acquire(timeout=)超时就会释放
- 砸掉电脑
import threading
from threading import Thread,Lock
import time
lock1 = threading.Lock()
lock2 = threading.Lock()
class MyThread1(Thread):
def run(self) :
if lock1.acquire():
print(self.name+'get 1')
time.sleep(0.1)
if lock2.acquire(timeout=2):
print(self.name+'get AB')
lock2.release()
lock1.release()
class MyThread2(Thread):
def run(self) :
if lock2.acquire():
print(self.name+'get 2')
time.sleep(0.1)
if lock1.acquire():
print(self.name+'get 1 2 ')
lock1.release()
lock2.release()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
#线程1:拿1,期待2,如果2等待超时,释放1
#线程2:拿2,期待1
"""
Thread-1get 1
Thread-2get 2
Thread-2get 1 2
"""
两个线程之间的通信:生产者和消费者模式
协程
微线程
适用于耗时操作:网络请求,网络下载(爬虫),IO操作
高效利用CPU
通过生成器完成
def task1():
for i in range(3):
print('A'+str(i))
yield
time.sleep(1)
def task2():
for i in range(3):
print('B'+str(i))
yield
time.sleep(2)
if __name__ == '__main__':
g1 = task1()
g2 = task2()
while True:
try:
next(g1)
next(g2)
except:
break
"""
A0
B0
A1
B1
A2
B2
"""
使用greenlet
不够智能,需要手动切换
- g1 = greenlet(a)
- switch()进行切换
from greenlet import greenlet
def a():
for i in range(5):
print('A' + str(i))
g2.switch()
time.sleep(0.5)
def b():
for i in range(5):
print('B' + str(i))
g3.switch()
time.sleep(0.5)
def c():
for i in range(5):
print('C' + str(i))
g1.switch()
time.sleep(0.5)
if __name__ == '__main__':
g1 = greenlet(a)
g2 = greenlet(b)
g3 = greenlet(c)
g1.switch()
使用gevent
底层还是greenlet,可以自动切换
g1 = gevent.spawn(a)
猴子补丁 monkey patch
检测到耗时操作,自动切换
- from gevent import monkey
- monkey.patch_all()
import time
import gevent
from gevent import monkey
monkey.patch_all()
def a():
for i in range(5):
print('A' + str(i))
time.sleep(0.5)
def b():
for i in range(5):
print('B' + str(i))
time.sleep(0.5)
def c():
for i in range(5):
print('C' + str(i))
time.sleep(0.5)
if __name__ == '__main__':
g1 = gevent.spawn(a)
g2 = gevent.spawn(b)
g3 = gevent.spawn(c)
g1.join()
g2.join()
g3.join()
案例
import urllib.request
import gevent
from gevent import monkey
monkey.patch_all()
def download(url):
response = urllib.request.urlopen(url)#耗时操作
content = response.read()
print('下载了{}的数据,长度:{}'.format(url,len(content)))
if __name__ == '__main__':
urls = ['https://www.qq.com','https://www.baidu.com','https://www.weibo.com']
g1 = gevent.spawn(download,urls[0])
g2 = gevent.spawn(download, urls[1])
g3 = gevent.spawn(download, urls[2])
g1.join()
g2.join()
g3.join()