《python核心编程》读书笔记 第四章 多线程编程

一、当我们谈论多线程时我们在谈论什么

在多线程(MT)出现之前,计算机内部是串行的,也就是说,无论是需要按照顺序执行的任务还是相互独立的任务,都要按照一个一个来的顺序执行。

这种执行方法会带来两个问题。一是执行效率会比较低,二是如果我们遇到了这样的任务:本质上是异步的;需要多个并发活动;每个活动的处理顺序可能是不确定的,或者说是随机的、不可预测的。这种编程任务可以被组织或划分成多个执行流,其中每个执行流都有一个指定要完成的任务。根据应用的不同,这些子任务可能需要计算出中间结果,然后合并为最终的输出结果。比如说计算机要处理多个外部输入源,对每个输入源的输入做一些处理并写入,而外部输入源的输入是不定时的。那么对于一个单线程的串行程序,我们需要安排一个时间线,让它定时的去检查一下输入同时还要兼顾梳理和写入,并且I/O终端通道的读取不能是堵塞的,因为用户输入的到达时间是不确定的,这样就会造成非常复杂的控制流,难以理解和维护。

而使用多线程并行处理,我们可以这样解决这个问题:把变成任务规划成几个执行特定函数的线程。

• UserRequestThread:负责读取客户端输入,该输入可能来自I/O 通道。程序将创建多个线程,每个客户端一个,客户端的请求将会被放入队列中。

• RequestProcessor:该线程负责从队列中获取请求并进行处理,为第3 个线程提供输出。

• ReplyThread:负责向用户输出,将结果传回给用户(如果是网络应用),或者把数据写到本地文件系统或数据库中。

这样做的好处还在于可以降低程序的复杂性,每个线程的逻辑都不复杂,清晰、高效、简洁。

 

二、线程可是进程的小宝宝呀

进程:也称为重量级进程,是一个执行中的程序。

每个进程都拥有自己的地址空间、内存、数据栈和辅助数据等。不过也是因此,进程不能直接共享信息,只能进程间通信。

线程:也称轻量级进程,多线程在同一个进程下执行,共享相同的上下文,可以认为它们是迷你进程。一个进程中的各个线程与主线程共享同一片数据空间,因此相比于独立的进程而言,线程间的信息共享和通信更加容易。线程一般是以并发方式执行的,正是由于这种并行和数据共享机制,使得多任务间的协作成为可能。

当然,如果你用的是单核CPU,那么表面上的并行其实是经过规划之后的串行。

 

三、出生晚的好处就是别人给你造好了轮子

Python中,我们有thread和threading模块用来实现多线程。在例子中,假定需要执行两个函数,一个需要4秒来执行,一个需要3秒来执行,为了简化程序,这里就暂时使用sleep函数来完成延时,实际应用中当然是可以写入自己需要执行的内容的。

a.使用单线程执行

Python3代码如下:

from time import *

def loop():
    print('start loop 0 at:',ctime())
    sleep(4)
    print('loop 0 end at:',ctime())

def loop1():
    print('start loop1 at :',ctime())
    sleep(3)
    print('end loop1 at:',ctime())

def main():
    print('starting at:',ctime())
    loop()
    loop1()
    print('all done at:',ctime())

main()

执行结果:

显然花费了4+3总共是7秒

现在,假设loop0()和loop1()中的操作不是睡眠,而是执行独立计算操作的函数,所有结果汇总成一个最终结果。那么,让它们并行执行来减少总的执行时间是不是有用的呢?这就是现在要介绍的多线程编程的前提。

 

b.使用thread模块执行

Python 提供了多个模块来支持多线程编程,包括thread、threading 和Queue 模块等。程序是可以使用thread 和threading 模块来创建与管理线程。thread 模块提供了基本的线程和锁定支持;而threading 模块提供了更高级别、功能更全面的线程管理。使用Queue 模块,用户可以创建一个队列数据结构,用于在多线程之间进行共享。

在看thread模块之前,需要声明,除非一个经验非常丰富的python工程师需要使用底层的功能,否则我们尽可能地需要避免使用thread模块,因为它是线程不安全的。Thread的功能少,而且。当主线程结束时,所有其他线程也都强制结束,不会发出警告或者进行适当的清理。

Python3代码如下:

from time import *
import _thread as thread

def loop(): #这是需要执行的第一个函数
    print('start loop0 at:',ctime())
    sleep(4)
    print('end loop0 at:',ctime())

def loop1(): #这是需要执行的第二个函数
    print('start loop1 at :',ctime())
    sleep(3)
    print('end loop1 at:',ctime())

def main():
    print('starting at:',ctime())
    
    thread.start_new_thread(loop,()) #start_new_thread (function, args, kwargs=None),派生一个新的线程,使用给定的args 和可选的kwargs 来执行function
    thread.start_new_thread(loop1,())
    sleep(6)  #如果没有这一步,主线程会直接运行结束
    print('all done at:',ctime())

main()

执行结果如下:

顺便可以看到因为并行,loop0和loop1的打印语句打了一架。因为主线程结束时,所有其他线程也都强制结束,所以用了一个睡眠6秒保证子线程完整执行了,那么我们在使用时就必须知道最长的子线程要执行多久,确保给它一个执行间隔,这显然既不方便也不安全。所以引入线程锁的概念。

线程锁是这样的一个对象,它可以用来锁定资源。在本例中,。当sleep()的时间到了的时候,释放对应的锁,向主线程表明该线程已完成。

from time import *
import _thread as thread
loops=[4,3]

def loop(nloop,nsec,lock): #需要执行的函数,我们对其进行了参数化
    print('start loop' ,nloop, 'at:',ctime())
    sleep(nsec)
    print('loop ',nloop,' end at:',ctime())
    lock.release() #执行完成时,释放对应索引的线程锁

def main():
    print('starting at:',ctime())
    locks=[]
    nloops=len(loops)
    for i in range(nloops): #第一个循环,生成了一个锁列表
        lock=thread.allocate_lock() #表示生成锁
        lock.acquire() #获得锁
        locks.append(lock)
    for i in range(nloops): #第二个循环,开启线程
        thread.start_new_thread(loop,(i,loops[i],locks[i]))
    for i in range(nloops): #第三个循环,等待所有的线程锁被释放,也就是所有线程执行完毕
        while locks[i].locked():
            pass
    print('all done at:',ctime())
main()

执行结果:

这个代码看起来就不那么低端了,但是阅读代码我们会发现,无论是创建锁、开启锁、使用锁、释放锁、等待释放完成,都需要我们自己敲代码完成,这,不行。因此我们来看更友好的threading模块。

重申一遍,这里的代码仅用于学习,实际应用中除非特殊情况,不要使用thread模块。

 

c.使用threading模块执行

因为Thread()类同样包含某种同步机制,所以锁原语的显式使用不再是必需的了。

threading 模块支持守护线程,其工作方式是:守护线程一般是一个等待客户端请求服务的服务器。如果没有客户端请求,守护线程就是空闲的。如果把一个线程设置为守护线程,就表示这个线程是不重要的,进程退出时不需要等待这个线程执行完成。如果主线程准备退出时,不需要等待某些子线程完成,就可以为这些子线程设置守护线程标记。该标记值为真时,表示该线程是不重要的,或者说该线程只是用来等待客户端请求而不做任何其他事情。要将一个线程设置为守护线程,需要在启动线程之前执行如下赋值语句:thread.daemon = True

使用thread类的三种方法:

1. 创建 Thread 的实例,传给它一个函数。

from time import *
import threading
loops=[4,3]

def loop(nloop,nsec):
    print('start loop' ,nloop, 'at:',ctime())
    sleep(nsec)
    print('loop ',nloop,' end at:',ctime())

def main():
    print('starting at:',ctime())
    nloops=len(loops)
    threads=[]
    for i in range(nloops):
        t=threading.Thread(target=loop,args=(i,loops[i])) #当实例化每个Thread 对象时,把函数(target)和参数(args)传进去,然后得到返回的Thread 实例
        threads.append(t)
    for i in range(nloops): #实例化Thread(调用Thread())和调用thread.start_new_thread()的最大区别是新线程不会立即开始执行。这是一个非常有用的同步功能,尤其是当你并不希望线程立即开始执行时。
        threads[i].start() 
    for i in range(nloops): #join()方法将等待线程结束,或者在提供了超时时间的情况下,达到超时时间。
        threads[i].join()
    print('all done at:',ctime())

if __name__=='__main__':
main()

执行结果:

 

2. 创建 Thread 的实例,传给它一个可调用的类实例

import threading
from time import sleep,ctime

loops=[4,2]

class ThreadFunc(object): #我们希望这个类更加通用,而不是局限于loop()函数,因此添加了一些新的东西,比如让这个类保存了函数的参数、函数自身以及函数名的字符串。而构造函数__init__()用于设定上述这些值。
    def __init__(self,func,args,name=''):
        self.name=name
        self.func=func
        self.args=args
    def __call__(self):
        self.func(*self.args)

def loop(nloop,nsec):
    print('start loop' ,nloop, 'at:',ctime())
    sleep(nsec)
    print('loop ',nloop,' end at:',ctime())
if __name__=='__main__':
    print('starting at :',ctime())
    threads=[]
    nloops=len(loops)
    for i in range(nloops): #当创建新线程时,Thread 类的代码将调用ThreadFunc 对象,此时会调用__call__()这个特殊方法。由于我们已经有了要用到的参数,这里就不需要再将其传递给Thread()的构造函数了,直接调用即可。
        t=threading.Thread(target=ThreadFunc(loop,args=(i,loops[i])))
        threads.append(t)
    for i in range(nloops):
        threads[i].start()
    for i in range(nloops):
        threads[i].join()
print('all done at:',ctime())

3. 派生 Thread 的子类,并创建子类的实例

import threading
from time import sleep,ctime

loops=[4,2]

class MyThread(threading.Thread):
    def __init__(self,func,args,name=''):
        threading.Thread.__init__(self)
        self.name=name
        self.func=func
        self.args=args
    def run(self):
        self.func(*self.args)

def loop(nloop,nsec):
    print('start loop' ,nloop, 'at:',ctime())
    sleep(nsec)
    print('loop ',nloop,' end at:',ctime())

if __name__=='__main__':
    print('starting at :',ctime())
    threads=[]
    nloops=len(loops)
    for i in range(nloops):
        t=MyThread(loop,(i,loops[i]))
        threads.append(t)
    for i in range(nloops):
        threads[i].start()
    for i in range(nloops):
        threads[i].join()
print('all done at:',ctime())

代码里有几点值得说道的地方。在第6行类的定义中,参数为threading.Thread,表明该类是它的子类,子类的构造方法中必须调用父类的构造方法,即第8行。第12行将__call__改为了run,可以看出整个代码中并没有显式地调用run方法,因为run是对原父类中函数的重写。我们调用了start方法,实际上就触发地调用了run。

最后,为了让 Thread 的子类更加通用,也可以将这个子类移到一个专门的模块中,并添加可调用的getResult()方法来取得返回值。

d.多线程实践

书里给了一个爬取亚马逊网站的例子,由于墙的原因,稍微修改了一下网址和正则表达式。代码运行时频繁返回503,百度后得知亚马逊是禁止爬虫的,因此添加了随机header,但是成功率依然不太高。

from atexit import register
from re import compile

from time import ctime
from urllib.request import urlopen,Request,build_opener
from threading import Thread

USER_AGENTS_LIST = [ "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
    "Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
    "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
    "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
    "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
    "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
    "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5",
    "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
    "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.11 TaoBrowser/2.0 Safari/536.11",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.71 Safari/537.1 LBBROWSER",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; LBBROWSER)",
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E; LBBROWSER)",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.84 Safari/535.11 LBBROWSER",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; QQBrowser/7.0.3698.400)",
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SV1; QQDownload 732; .NET4.0C; .NET4.0E; 360SE)",
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)",
    "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.89 Safari/537.1",
    "Mozilla/5.0 (iPad; U; CPU OS 4_2_1 like Mac OS X; zh-cn) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
    "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b13pre) Gecko/20110307 Firefox/4.0b13pre",
    "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:16.0) Gecko/20100101 Firefox/16.0",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11",
    "Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10"
]

#REGEX=compile('#([\d,]+) in Books ')
REGEX=compile('图书商品里排第([\d,]+)名')
AMZN='https://www.amazon.cn/dp/'

ISBNS={
    '0132269937':'core python programming',
    '0132356139':'python web development with Django',
    #'0137143419':'python fundamentals',
}

def getRanking(isbn):
    url='%s%s'%(AMZN,isbn)
    req = Request(url, headers = {
    'Connection': 'Keep-Alive',
    'Accept': 'text/html, application/xhtml+xml, */*',
    'Accept-Language': 'en-US,en;q=0.8,zh-Hans-CN;q=0.5,zh-Hans;q=0.3',
    'User-Agent': USER_AGENTS_LIST[random.randint(0,len(USER_AGENTS_LIST) - 1)]
        })
    oper = urlopen(req)
    data = oper.read()
    # page=urlopen('%s%s'%(AMZN,isbn))
    # data=page.read()
    # page.close()
    oper.close()
    #print(data)
    return REGEX.findall(data.decode())

def _showRanking(isbn):
    print('-%r ranked %s'%(ISBNS[isbn],getRanking(isbn)))

def main():
    print('At',ctime(),'on amazon')
    threads=[]
    for isbn in ISBNS:
        # _showRanking(isbn)
        t=Thread(target=_showRanking,args=(isbn,))
        threads.append(t)
    for i in range(len(ISBNS)):
        threads[i].start()
    for i in range(len(ISBNS)):
        threads[i].join()
@register
def _atexit():
    print('all done at:',ctime())
if __name__=='__main__':
    main()

理想情况下可以返回这样的结果:

另外,代码中用到了装饰器,现查了一下,这篇文章写的不错:

https://www.cnblogs.com/cicaday/p/python-decorator.html

e.生产者-消费者问题和Queue/queue 模块&多进程

就是通过创建一个队列,让不同的线程从同一个queue中拿数据。通常来说,多线程是一个好东西。不过,由于Python 的GIL 的限制,多线程更适合于I/O 密集型应用(I/O 释放了GIL,可以允许更多的并发),而不是计算密集型应用。对于后一种情况而言,为了实现更好的并行性,你需要使用多进程,以便让CPU 的其他内核来执行。

这些个功能我之前已经用过,所以没有细读。

猜你喜欢

转载自blog.csdn.net/weixin_39655021/article/details/85454525