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这个模块,还不清楚其中遇到的输出问题,继续探究!
版权声明:《 Python3多线程与协程 》为DYBOY原创文章,转载请注明出处!
最后编辑:2019-3-10 13:03:53
2019-03-15 17:47
2019-03-12 18:58
2019-03-20 20:52