Python3多线程与协程

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这个模块,还不清楚其中遇到的输出问题,继续探究!

发表评论 / Comment

用心评论~

金玉良言 / Appraise
西安seoLV 1
2019-03-15 17:47
听说前不久, Python正式登顶世界第一编程语言,哈哈 就是强大
免费节点LV 1
2019-03-12 18:58
顶顶顶!!!学习了~
头像
DYBOY站长已认证
2019-03-20 20:52
@免费节点:哈哈,一起学习!

Warning: Cannot modify header information - headers already sent by (output started at /www/wwwroot/blog.dyboy.cn/content/templates/dyblog/footer.php:56) in /www/wwwroot/blog.dyboy.cn/include/lib/view.php on line 23