python中的多线程非常的常用,之前一直糊里糊涂地使用,没有一些系统性的概念,记录一下~
0x001 多线程的优势:可将长时间占用的程序放到后台
可能会加速程序执行速度
能够实现一些类似同步执行的效果
0x002 线程线程是OS的执行单元
每个独立的线程都有一个程序运行的入口、顺序执行序列和程序出口,线程不能离开程序独立执行。
每个线程都有自己唯一的一组CPU寄存器(上下文),反映了线程上次运行时该CPU寄存器的状态。线程中指令指针和堆栈指针寄存器非常重要,线程在进程中得到上下文,这些地址用于标志拥有线程的进程地址空间中的内存
线程可被抢占
线程退让
0x01 分类:名字
说明
用户线程
不需内核支持而在用户程序中实现的线程
内核线程
操作系统内核创建和撤销
0x02 Py3中的threading模块:方法
说明
threading.current_thread()
返回当前的线程变量
threading.enumerate()
返回正在运行线程的list(启动后,结束前)
threading.active_count
等于len(threading.enumerate())
提供了 Thread类,所以还可以
方法
说明
run()
表示线程活动的方法
start()
启动线程活动
join([time])
阻塞式等待线程终止
isAlive()
线程是否活动
getName()
返回线程名
setName()
设置线程名
# -*- coding:utf-8 -*-
# 多线程
# DYBOY
# time:2019-3-10 09:37:48
import threading
import time
def printNum(endNum):
for i in range(1,endNum+1):
print(i, time.time())
# 创建线程
t = threading.Thread(target=printNum, name='printThread', args=(10,))
t.start()
t.join()
print("线程%s结束" % threading.current_thread().name)
0x003 多线程&多进程&线程锁多进程中同一变量,各自有拷贝到自己的进程中,互不影响,多线程中,变量由多个线程共享,因此多线程中变量的同步就需要的到控制
lock = threading.Lock()
def runThread():
for i in range(1000):
lock.acquire()
try:
#....执行函数
finally:
lock.release()
# -*- coding:utf-8 -*-
# 多线程
# DYBOY
# time:2019-3-10 09:37:48
import threading
import time
money = 0
lock = threading.Lock()
def chaneMoney(num):
global money
money += num
money -= num
def runThread(n):
for i in range(1000):
lock.acquire()
try:
chaneMoney(n)
finally:
lock.release()
t1 = threading.Thread(target=runThread, args=(100,))
t2 = threading.Thread(target=runThread, args=(50,))
t1.start()
t2.start()
t1.join()
t2.join()
print("余额:",money)
ps: 在实际的运行中,发现似乎线程锁没有起到作用,在线程中的join() 方法似乎是有影响的,
join():阻塞当前进程/线程,直到调用join方法的那个进程执行完,再继续执行当前进程。相当于线程守护,直到调用join()方法的线程执行完毕,才将控制权交给主进程。
0x04 问题?
从上,看到多线程中为了保证数据的一致性,使用了线程锁来实现类似同步的功能,然而这样反而多了获取锁和释放锁的步骤,所以在我看来。线程也没有加快程序的运行时间。
一个程序从执行到结束,首先会创建一个主进程,os的执行单元是线程,一个进程有至少一个或多个线程来实现其功能,在线程的创建和上下文切换是一个比较大的开销,提升多线程的优势就需要从其中来考虑:
无锁并发(减少数据关联度,更合理优化的实现方式)
减少并发(线程不能无限制的多)
减少上下文切换的开销(协程)
0x05 协程函数调用的时候,是使用栈的方式,比如A调用B,B调用C,C执行返回给B,B执行完后返回给A,是一个压栈出栈的过程
子程序(函数),总是一个入口,一次返回,调用的顺序永远如此,所以如果有比较频繁的函数调用,那么就用较多的上下文切换时间,利用协程(微线程)可以较好解决这个问题。
协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。(多进程)
# -*- coding:utf-8 -*-
# 协程 gevent
# DYBOY
# time:2019-3-10 09:37:48
# description: 下载图片到本地(普通版本)
# from gevent import monkey
# monkey.patch_all()
import requests,time,json
def get_save_pic(picUrl, name):
img = requests.get(picUrl)
with open('pic/'+name,'wb') as f:
f.write(img.content)
return None
if __name__ == '__main__':
sT = time.time()
jsonData = requests.get('http://img.top15.cn/piclist.php')
jsonData = json.loads(jsonData.text)
imgs = jsonData['data']
for img in imgs:
get_save_pic(img[0], img[1])
print("Success", time.time() - sT)
网络效果好的时候:
# -*- coding:utf-8 -*-
# 协程 gevent
# DYBOY
# time:2019-3-10 09:37:48
# description: 下载图片到本地
from gevent import monkey
monkey.patch_all()
import gevent,requests,time,json
def get_save_pic(picUrl, name):
img = requests.get(picUrl)
with open('pic/'+name,'wb') as f:
f.write(img.content)
return None
if __name__ == '__main__':
startTime = time.time()
jsonData = requests.get('http://img.top15.cn/piclist.php')
jsonData = json.loads(jsonData.text)
imgs = jsonData['data']
targetLists = []
for img in imgs:
targetLists.append(gevent.spawn(get_save_pic, img[0], img[1]))
gevent.joinall(targetLists)
print("Success!", time.time()-startTime)
# 不知道什么原因,没有输出,但是从执行的结果上来看
# 最后,所有图片同时在文件夹生成,非常迅速
2019-3-10 22:05:04 在命令行下可正常执行!
从肉眼可见的角度来看,还是协程的效果更好(在数据量不大下,感觉比较而得出的结论还是不是很有说服力,在数据量大的情况下,线程不能无限增加,协程的效果表现更优异,再加上多进程应该就更NICE了)。
0x06 总结
本次探究的是多线程与协程的区别,多线程不能无限创建,所以有的时候创建多线程在生产环境下是不可行的,在爬虫下载图片这部分是可以使用多线程去下载,多线程其实也是一个等待执行的过程,其与协程的差别主要是在上下文切换上,协程减少了上下文切换的时间,是程序自己控制的,而多线程的上下文切换是需要系统调用会耗费更多的时间,本次例子实现中使用了monkey这个模块,还不清楚其中遇到的输出问题,继续探究!