理解 Python 中的多线程及资源竞争
本篇是从网上摘来的一个老外写的一篇文章的译文,主要通过几个示例来了解python 的线程及如何避免线程之间的竞争。 当然在提到python的多线程问题时,很多人会提到GIL的问题,不过本篇要展示的内容不做GIL的讨论。
一、单线程请求与多线程请求
例1:单线程请求
1#!/usr/bin/env python
2import time
3import urllib2
4def get_responses():
5 urls = [
6 'http://www.amazon.com',
7 'http://www.ebay.com',
8 'http://www.alibaba.com',
9 'http://www.reddit.com'
10 ]
11 start = time.time()
12 for url in urls:
13# print url
14 resp = urllib2.urlopen(url)
15 print url,resp.getcode()
16 print "Elapsed time: %s" % (time.time()-start)
17get_responses()
其输出结果是:
http://www.amazon.com 200
http://www.ebay.com 200
http://www.alibaba.com 200
http://www.reddit.com 200
Elapsed time: 3.0814409256
解释:
- url顺序的被请求
- 除非cpu从一个url获得了回应,否则不会去请求下一个url
- 网络请求会花费较长的时间,所以cpu在等待网络请求的返回时间内一直处于闲置状态。
####
例2:多线程请求
1#!/usr/bin/env python
2import urllib2
3import time
4from threading import Thread
5class GetUrlThread(Thread):
6 def __init__(self, url):
7 self.url = url
8 super(GetUrlThread, self).__init__()
9 def run(self):
10 resp = urllib2.urlopen(self.url)
11 print self.url, resp.getcode()
12def get_responses():
13 urls = [
14 'http://www.amazon.com',
15 'http://www.ebay.com',
16 'http://www.alibaba.com',
17 'http://www.reddit.com'
18 ]
19 start = time.time()
20 threads = []
21 for url in urls:
22 t = GetUrlThread(url)
23 threads.append(t)
24 t.start()
25 for t in threads:
26 t.join()
27 print "Elapsed time: %s" % (time.time()-start)
28get_responses()
运行结果:
http://www.reddit.com 200
http://www.amazon.com 200
http://www.alibaba.com 200
http://www.ebay.com 200
Elapsed time: 0.689890861511
解释:
- 意识到了程序在执行时间上的提升我们写了一个多线程程序来减少cpu的等待时间,当我们在等待一个线程内的网络请求返回时,这时cpu可以切换到其他线程去进行其他线程内的网络请求。
- 我们期望一个线程处理一个url,所以实例化线程类的时候我们传了一个url。
- 线程运行意味着执行类里的
run()
方法。 - 无论如何我们想每个线程必须执行
run()
。 - 为每个url创建一个线程并且调用
start()
方法,这告诉了cpu可以执行线程中的run()
方法了。 - 我们希望所有的线程执行完毕的时候再计算花费的时间,所以调用了
join()
方法。 join()
可以通知主线程等待这个线程结束后,才可以执行下一条指令。- 每个线程我们都调用了
join()
方法,所以我们是在所有线程执行完毕后计算的运行时间。
关于线程:
- cpu可能不会在调用
start()
后马上执行run()
方法。 - 你不能确定
run()
在不同线程建间的执行顺序。 - 对于单独的一个线程,可以保证
run()
方法里的语句是按照顺序执行的。 - 这就是因为线程内的url会首先被请求,然后打印出返回的结果。
二、多线程之间的资源竞争
例3:资源竞争问题
1#!/usr/bin/env python
2from threading import Thread
3#define a global variable
4some_var = 0
5class IncrementThread(Thread):
6 def run(self):
7 #we want to read a global variable
8 #and then increment it
9 global some_var
10 read_value = some_var
11 print "some_var in %s is %d" % (self.name, read_value)
12 some_var = read_value + 1
13 print "some_var in %s after increment is %d" % (self.name, some_var)
14def use_increment_thread():
15 threads = []
16 for i in range(50):
17 t = IncrementThread()
18 threads.append(t)
19 t.start()
20 for t in threads:
21 t.join()
22 print "After 50 modifications, some_var should have become 50"
23 print "After 50 modifications, some_var is %d" % (some_var,)
24use_increment_thread()
多次运行这个程序,你会看到多种不同的结果。
解释:
- 有一个全局变量,所有的线程都想修改它。
- 所有的线程应该在这个全局变量上加 1 。
- 有50个线程,最后这个数值应该变成50,但是它却没有。
为什么没有达到50?
- 在
some_var
是15
的时候,线程t1
读取了some_var
,这个时刻cpu将控制权给了另一个线程t2
。 t2
线程读到的some_var
也是15
t1
和t2
都把some_var
加到16
- 当时我们期望的是
t1
t2
两个线程使some_var + 2
变成17
- 在这里就有了资源竞争。
- 相同的情况也可能发生在其它的线程间,所以出现了最后的结果小于
50
的情况。
例4:解决资源竞争问题
1#!/usr/bin/env python
2from threading import Lock, Thread
3lock = Lock()
4some_var = 0
5class IncrementThread(Thread):
6 def run(self):
7 #we want to read a global variable
8 #and then increment it
9 global some_var
10 lock.acquire()
11 read_value = some_var
12 print "some_var in %s is %d" % (self.name, read_value)
13 some_var = read_value + 1
14 print "some_var in %s after increment is %d" % (self.name, some_var)
15 lock.release()
16def use_increment_thread():
17 threads = []
18 for i in range(50):
19 t = IncrementThread()
20 threads.append(t)
21 t.start()
22 for t in threads:
23 t.join()
24 print "After 50 modifications, some_var should have become 50"
25 print "After 50 modifications, some_var is %d" % (some_var,)
26use_increment_thread()
再次运行这个程序,达到了我们预期的结果。
解释:
- Lock 用来防止竞争条件
- 如果在执行一些操作之前,线程
t1
获得了锁。其他的线程在t1
释放Lock之前,不会执行相同的操作 - 我们想要确定的是一旦线程
t1
已经读取了some_var
,直到t1
完成了修改some_var
,其他的线程才可以读取some_var
- 这样读取和修改
some_var
成了逻辑上的原子操作。
接下来再看两个非全局变量的例子
例5:非多线程资源竞争
1#!/usr/bin/env python
2from threading import Thread
3import time
4class CreateListThread(Thread):
5 def run(self):
6 self.entries = []
7 for i in range(10):
8 time.sleep(1)
9 self.entries.append(i)
10 print self.entries
11def use_create_list_thread():
12 for i in range(3):
13 t = CreateListThread()
14 t.start()
15use_create_list_thread()
time.sleep()可以使一个线程挂起,强制线程切换发生。运行几次后发现并没有打印出争取的结果。当一个线程正在打印的时候,cpu切换到了另一个线程,所以产生了不正确的结果。我们需要确保print self.entries
是个逻辑上的原子操作,以防打印时被其他线程打断。
例6:非多线资源竞争解决
1#!/usr/bin/env python
2from threading import Thread, Lock
3import time
4lock = Lock()
5class CreateListThread(Thread):
6 def run(self):
7 self.entries = []
8 for i in range(10):
9 time.sleep(1)
10 self.entries.append(i)
11 lock.acquire()
12 print self.entries
13 lock.release()
14def use_create_list_thread():
15 for i in range(3):
16 t = CreateListThread()
17 t.start()
18use_create_list_thread()
使用Lock()后,这次我们看到了正确的结果。证明了一个线程不可以修改其他线程内部的变量(非全局变量)。
捐赠本站(Donate)
如您感觉文章有用,可扫码捐赠本站!(If the article useful, you can scan the QR code to donate))
- Author: shisekong
- Link: https://blog.361way.com/python-thread/3425.html
- License: This work is under a 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. Kindly fulfill the requirements of the aforementioned License when adapting or creating a derivative of this work.