Python 线程进阶篇 | 多线程下载网页

阅读 690
收藏 58
2017-04-02
原文链接:www.jianshu.com

Python的并发编程前面只讲了进程和线程的入门篇,线程里面还有很多有用而好玩的招式,我们还没有讲,我一直主张学一门语言一定要有兴趣,带着玩Python的心态去学,会轻松很多,打个比方打扫卫生和打羽毛球,可能都很累,为啥你打球就很high,而在家打扫卫生就很累,因为你enjoy打球哈哈,好言归正传,今天我们就来讲讲多线程编程~~

要点:

线程的同步

线程的锁

线程的通讯

1.线程如何同步

1).若主程序创建了一个线程之后,启动线程start().线程的一个关键特性是每个线程都是独立运行且状态不可预测

我们希望线程启动运行后,线程内部运行到某种情况的时候,主程序才执行

比如程序中的其他线程需 要通过判断某个线程的状态来确定自己下一步的操作

这时线程同步问题就会变得非常棘手,怎么破,很简单,利用线程threading.Event(),来设置信号量来控制


>>

Start thread1...

1

2

3

Start to run in main function

4

5

6

主程序创建一个Event()对象my_event,把my_event传到子线程里面

在子线程里面设置my_event.set()也就是说子线程运行到i==4时把信号量置为True而外边的主函数my_event.wait()一直阻塞在那里,傻傻的等着信号量的状态

一旦set()了意味着信号量为True了,外边主函数就可以开始行动了

2).同样我们假设有2个子线程,那两个子线程之间的同步也可以用Event来搞定


>>

Start thread1...

1

2

3

4

Ok,we can start thread2...

5

6

线程thread1和线程wait_for_thread1通过Event()对象来设置信号量,通过这个信号量来进行两个子线程的同步

2. 线程的锁

在多线程中,由于所有的变量都由所有线程共享,所以在运行的过程中,任何一个变量都可以被任何一个线程修改

因此,线程之间共享数据会有risk,很可能跑着跑着,数据就被别人改了,所以我们需要加锁来控制

举一个非常经典的例子,就是银行存取款的,相信很多同学都听说过这个例子,在传统的c++,java里面也大量用这个例子来说明锁的重要性,我们来看一下

1).假设银行存款有1000元,有两个线程对这笔钱进行操作

我再存1块钱,然后再取1块钱

我再存10块钱,然后再取10块钱

理论上讲无论多少次,这笔钱应该还是1000块,但是当两个线程同事进行大量频繁的操作的时候,诶情况就有变化了,钱多了,或者少了,不信你看


>>

1097

[Finished in0.3s]

哇多了97块钱,真是好事啊,哈哈,银行肯定不乐意了,要找原因了

11).我们在算钱的时候,做了两步

第一步:balance=balance+n

第二步:balance=balance-n

12).底层操作系统在执行的时候会分成好几步去执行,python是有C写的,C又会变成汇编语言去执行指令,所以当两个线程频繁的操作的时候,中间的指令会被打断,从而导致多个线程把同一个变量的内容改乱了(当然我们是希望钱变多哈哈)

2).所以一般对于多线程处理核心的数据,都是要加锁的

就是让某一个时刻,只能有一个线程改这个变量,让这个线程执行完了,释放锁,再让下一个线程执行,保证核心数据不会被改乱了~~比如我们执行10,100万次存取,都不会出差错


>>

1000

[Finished in1.1s]

创建一个threading.Lock()对象,在处理核心数据操作的时候,先锁起来,用完再释放锁,看现在反复操作100000次,钱还是1000,一分没有少,大家有没有发现一个问题,这个是有代价的,就是运行开销很大,原来是0.3s运行完,现在要1.1s才能搞定

3. 线程之间的通信

好比我有一个工作要做,一个线程要干6分钟,那么我创建3个线程,同时去处理这些事情,是不是只要2分钟.效率高了许多.

那么怎么样才能让这些线程之间实现安全的通信或者交换数据呢

Python里面有一个高效安全的Queue模块,我们主需要把数据塞进队列,它就会被所有的线程共享

然后用put()或者get()操作来给队列添加或者移除元素

我们举一个多线程爬网页的例子吧


1).首先创建一个DownloadThread类,这个类继承threading.Thread

2).类的初始化函数,留一个接口,把queue带进去,这样所有的线程都可以共享这个queue

3).类的run函数,设计成死循环,不断的从队列里面取数据,一旦这个线程把从这个队列里面的数据取完了,就调用queue.task_done()表示我已经干完了

4).类的download_file函数,其实就是具体去执行下载网页的工作,因为爬虫后面再讲,所以我这边只用了很简单的urllib.urlopen()函数,简单获取网页而已

接着看main函数


>>

Thread-3:begin dowbload http://wiki.python.org/moin/WebProgramming...

Thread-1:begin dowbload https://wiki.python.org/moin/WebFrameworks...

Thread-2:begin dowbload https://wiki.python.org/moin/BeginnersGuide...

Thread-2 download completed

Thread-2:begin dowbload https://wiki.python.org/moin/PythonBooks...

Thread-1 download completed

Thread-1:begin dowbload https://wiki.python.org/moin/PythonEvents...

Thread-2 download completed

Thread-1 download completed

Thread-3 download completed

Cost time:0:00:14.604000

1).main函数里面我们设定了有5个网页要爬

2).启动3个线程,并且设定为守护线程,就是当main()函数一结束,所有的子线程全部结束

3),把5个网页(也可以认为是5个要完成的task),这里是要爬网页,以后你可以变成其他数据结构,或者直接封装成类然后塞进queue里面,让这3个线程去共享完成.

4).queue会阻塞在哪里,等到所以的线程完成那一刻

5).大家看Thread2完成了之后,又会接着去爬下一个网页(真是个勤快的孩子),Thread1也是完成之后接着去爬下一个,看起来Thread3执行的网页爬的最慢

6).最后计算一下时间,大家改一下代码,可以比较一下一个线程爬5个网页和3个线程同时去快多少(大概是3倍)

补充说一下,这个爬网页urlopen()没有加任何异常保护,当然在实际操作中是肯定要保护,并且当出现异常的时候要设置一个flag来通知线程终止的.


需要源码后台留言

评论