时间: 2019-03-18
开发Python多线程程序时,主要涉及到3个模块,分别是thread,threading和Queue。
1.串行执行例子
下面举一个简单的例子,没有使用多线程的情况:
import time
def loop1():
print 'start loop1 at ', time.ctime()
time.sleep(4)
print 'end loop1 at ', time.ctime()
def loop2():
print 'start loop2 at ', time.ctime()
time.sleep(2)
print 'end loop2 at ', time.ctime()
def main():
print 'start main at ', time.ctime()
loop1()
loop2()
print 'end main at ', time.ctime()
if __name__ == '__main__':
main()
这个串行执行的例子,先执行loop1 ,再执行loop2 。
thread模块提供了基本的线程和锁定支持;而threading模块提供了更高级别、功能更全面的线程管理。
应该避免使用thread模块,尽量使用threading模块,原因如下:
(1)threading模块更加先进,有更好的线程支持,并且thread模块中的一些属性会和threading模块有冲突.
(2)thread模块拥有的同步原语很少,而threading模块有很多。
(3)thread模块对于进程何时退出没有控制。当主线程结束时,所有其他线程也都强制结束,不会发出警告或者进行适当的清理。
2.thread.start_new_thread实现多线程
由于使用thread模块时,主线程结束时,所有其他线程也都强制结束。下面例子采取让主线程sleep的方式来保证子线程先完成。
import thread
import time
def loop1():
print 'start loop1 at ', time.ctime()
time.sleep(4)
print 'end loop1 at ', time.ctime()
def loop2():
print 'start loop2 at ', time.ctime()
time.sleep(2)
print 'end loop2 at ', time.ctime()
def main_thread():
print 'start main at ', time.ctime()
thread.start_new_thread(loop1, ())
thread.start_new_thread(loop2, ())
time.sleep(6)
print 'All done at ', time.ctime()
if __name__ == '__main__':
main_thread()
由于loop1和loop2函数过于简单,我们可以预估loop1和loop2的完成时间,然后在main_thread函数直接相加两个时间,同时,在4s时,loop1和loop2都已经执行完了,没有实现多线程的优势。在实际情况中,这种方法是十分不可取的。
3.使用锁机制实现多线程
import time
import thread
loops = [44, 42]
def loop(nsecs, loopname, lock):
print 'start ', loopname, ' at: ', time.ctime()
time.sleep(nsecs)
print 'end ', loopname, ' at: ', time.ctime()
lock.release()
def main():
print 'start main at ', time.ctime()
# locks保存锁对象的列表
locks = []
nloops = range(len(loops))
for i in nloops:
lock = thread.allocate()
lock.acquire()
locks.append(lock)
for i in nloops:
thread.start_new_thread(loop, (loops[i], ('loop' + str(i)), locks[i]))
for i in nloops:
while locks[i].locked():
pass
print 'All done at: ', time.ctime()
if __name__ == '__main__':
main()
每个线程将被分配一个已获得的锁。当sleep()的时间到了的时候,释放对应的锁,向主线程表明该线程已完成。
通过使用thread.allocate_lock()函数得到锁对象,然后通过acquire()方法取得(每个锁)。取得锁效果相当于“把锁锁上”。
最后一个循环只是坐在那里等待(暂停主线程),直到所有锁都被释放之后才会继续执行。
4. Threading模块实现多线程
上边提到,应该使用更高级的threading模块实现多线程,这里研究使用Thread类来实现多线程,threading模块支持守护线程。使用Thread类,有多种创建线程的方法:
(1)创建Thread 的实例,传给它一个函数。
(2)创建Thread 的实例,传给它一个可调用的类实例。
(3)派生Thread 的子类,并创建子类的实例。
Thread类的常用方法:
方法 | 说明 |
---|---|
init(group=None, tatget=None, name=None, args=(),kwargs ={}, verbose=None, daemon=None) | 实例化一个线程对象,需要有一个可调用的target,以及其参数args或kwargs。还可以传递name 或group 参数,不过后者还未实现。此外, verbose 标志也是可接受的。而daemon 的值将会设定thread.daemon 属性/标志 |
start() | 开始执行该线程 |
run() | 定义线程功能的方法(通常在子类中被应用开发者重写) |
join(timeout=None) | 直至启动的线程终止之前一直挂起;除非给出了timeout(秒),否则会一直阻塞 |
4.1 创建一个Thread的实例,并传给它一个函数
在第3节的多线程例子中,需要自己管理锁来实现多线程,这样其实比较麻烦。使用threading.Thread类时,就省去了锁的管理,实例化Thread(调用Thread())和调用thread.start_new_thread()的最大区别是新线程不会立即开始执行。
from time import ctime
from time import sleep
import threading
sleepsecs = [4, 8]
def loop(loopname, secs):
print 'start loop',loopname, ' at: ', ctime()
sleep(secs)
print 'end loop',loopname, ' at: ', ctime()
if __name__ == '__main__':
# Thread对象列表
threads = []
for i in range(len(sleepsecs)):
thread = threading.Thread(target=loop, args=(i, sleepsecs[i]))
threads.append(thread)
print 'start main at :', ctime()
# 逐个启动线程
for i in range(len(sleepsecs)):
threads[i].start()
sleep(0.001)
for i in range(len(sleepsecs)):
threads[i].join()
print 'end main at ', ctime()
4.2 创建一个Thread的实例,传给它一个可调用的类实例
下面添加一个ThreadFunc类,在实例化Thread类时,实际做了2次实例化,包括Thread和ThreadFunc。
当创建新线程时,Thread 类的代码将调用ThreadFunc 对象,此时会调用call()这个特殊方法。
from time import sleep
from time import ctime
from threading import Thread
class ThreadFunc(object):
# func: 多线程要执行的函数 该参数就是一个函数对象
# loopname: 执行函数名称
# secs: sleep等待时间
def __init__(self, func, loopname, secs):
self.func = func
self.loopname = loopname
self.secs = secs
def __call__(self):
self.func(self.loopname, self.secs)
def loop(loopname, secs):
print 'start loop ', loopname, ' at: ', ctime()
sleep(secs)
print 'end loop ', loopname, ' at: ', ctime()
if __name__ == '__main__':
threads = []
secs = [4, 8]
print 'start main at: ', ctime()
for i in range(len(secs)):
thread = Thread(target=ThreadFunc(loop, i, secs[i]))
threads.append(thread)
for i in range(len(secs)):
threads[i].start()
sleep(0.001)
for i in range(len(secs)):
threads[i].join()
print 'end main at:', ctime()
4.3 派生Thread的子类,并创建子类的实例
派生子类时,只需要创建子类的实例即可,不用再创建Thread类实例。需要注意下边2点:
1.MyThread子类的构造函数必须先调用其基类的构造函数。
2.必须在子类中定义run()方法。
from threading import Thread
from time import ctime
from time import sleep
class MyThread(Thread):
# func:该参数是多线程执行的函数
# args:func函数的参数列表,多个参数的列表
# name:名称
def __init__(self, func, args, name):
# MyThread子类的构造函数必须先调用其基类的构造函数
Thread.__init__(self)
self.func = func
self.args = args
self.name = name
# 必须在子类中定义run()方法
def run(self):
self.func(*self.args)
def loop(nloop, secs):
print 'start loop ', nloop, ' at: ', ctime()
sleep(secs)
print 'end loop ', nloop, ' at: ', ctime()
if __name__ == '__main__':
seconds = [4, 8]
threads = []
print 'start main at: ', ctime()
# 创建线程,但是未启动
for i in range(len(seconds)):
thread = MyThread(loop, (i, seconds[i]), '')
threads.append(thread)
# 启动线程
for i in range(len(seconds)):
threads[i].start()
sleep(0.001)
for i in range(len(seconds)):
threads[i].join()
print 'end main at: ', ctime()
4.4 独立抽象myThread模块
为了应用更加方便,将MyThread类独立抽象出来,后边直接引入就可以使用,并且保存下返回结果,做得更加通用。
from threading import Thread
from time import ctime
class MyThread(Thread):
def __init__(self, func, args, name):
Thread.__init__(self)
self.func = func
self.args = args
self.name = name
def getResult(self):
return self.res
def run(self):
print 'starting ', self.name, ' at: ', ctime()
self.func(*self.args)
print 'finish ', self.name , ' at: ', ctime()
5 斐波那契、阶乘、求和的 多线程实现
为了展示多线程的实现结果,在每次计算的时候,sleep 0.1秒,并试图用多线程优化。
from time import sleep
from time import ctime
from MyThread import MyThread
# 递归斐波那契
def recursion_fib(x):
sleep(0.1)
if x < 3:
return 1
else:
return recursion_fib(x-2) + recursion_fib(x-1)
# 递归阶乘
def recursion_multiply(x):
sleep(0.1)
if x < 2:
return 1
else:
return x*recursion_multiply(x-1)
# 递归实现时,只需要考虑这个函数在不同条件下的返回值,需要有终止条件
def recursion_sum(x):
sleep(0.1)
if x < 2:
return 1
else:
return x+recursion_sum(x-1)
if __name__ == '__main__':
func_names = ['fib', 'recMul', 'recSum']
funcs = [recursion_fib, recursion_multiply, recursion_sum]
threads = []
print 'start Single Thread -------------------'
for i in range(len(funcs)):
print 'start function ', func_names[i], ' at: ', ctime()
funcs[i](10)
print 'end function ', func_names[i], ' at: ', ctime()
print 'end Single Thread -------------------'
print 'start Multiple Thread -------------------'
for i in range(len(funcs)):
thread = MyThread(funcs[i], (10,), func_names[i])
threads.append(thread)
for i in range(len(funcs)):
threads[i].start()
sleep(0.001)
for i in range(len(funcs)):
threads[i].join()
print threads[i].getResult()
print 'end Multiple Thread -------------------'
6 多线程实际应用项目
实现多线程肯定有自己的实际应用场景,在实际使用过程中,遇到一个示例,对于一些I/O密集型的程序,使用python程序可以发挥很大的效果。
给定一批POI名称(共4万5千个左右),通过GD提供的公开搜索API接口,补全POI的id、地址、分类、经纬度等信息。
import random
import urllib2
import json
import AttrN
import Const
import time
import pandas as pd
from threading import Thread
class Regeo:
def __init__(self):
pass
# GD POI搜索接口 key id
AMAP_POI_SEARCH_URL = "https://restapi.amap.com/v3/place/text?key=%s&keywords=%s&city=%s&output=json&&extensions=all"
PAGE_SIZE = 20
STATUS = 'status'
POIS = 'pois'
COUNT = 'count'
REGEOCODE = 'regeocode'
POIID = 'id'
PARENT_POIID = 'parent'
POINAME = 'name'
POITYPE = 'type'
POITYPECODE = 'typecode'
POIADDR = 'address'
POILOCATION = 'location'
POILNG = 'poi_lng'
POILAT = 'poi_lat'
POIADCODE = 'adcode'
POIADNAME = 'adname'
NAVI_POIID = 'navi_poiid'
ENTR_LOCATION = 'entr_location'
ENTR_LNG = 'entr_lng'
ENTR_LAT = 'entr_lat'
EXIT_LOCATION = 'exit_location'
EXIT_LNG = 'exit_lng'
EXIT_LAT = 'exit_lat'
BUSINESSAREA = 'business_area'
def load_amap_key(amapkey_path):
amap_keys = []
fin = open(amapkey_path)
while True:
line = fin.readline()
if not line:
break
amap_keys.append(line.strip("\n"))
return amap_keys
def random_amap_key(amap_keys):
"""
从amap_key库中随机获取一个amap_key
:return: amap_key
"""
return amap_keys[random.randint(0, len(amap_keys)) - 1]
def recurAmapParentIDSearch(poi_name,city_id):
amap_key_search = random_amap_key(load_amap_key("datafiles/key.rtf"))
if( city_id == 10 ):
city_name = 'shanghai'
else:
city_name = 'nanjing'
amap_restapi_search_url = Regeo.AMAP_POI_SEARCH_URL % (amap_key_search, poi_name, city_name)
for j in range(5):
try:
response = urllib2.urlopen(amap_restapi_search_url)
ret = json.load(response)
status = ret[Regeo.STATUS]
if (int(status) != 1):
return "null", "null", "null", "null", "null"
pois = ret[Regeo.POIS]
poi = pois[0] # 搜索结果第一
name = poi[AttrN.NAME].encode(Const.UTF8)
id = poi[AttrN.ID].encode(Const.UTF8)
type = poi[AttrN.TYPE].encode(Const.UTF8)
loc = poi[AttrN.LOCATION].encode(Const.UTF8)
if isinstance(poi[AttrN.ADDRESS], list):
if len(poi[AttrN.ADDRESS]) == 0:
addr = "null"
else:
addr = poi[AttrN.ADDRESS][0].encode(Const.UTF8)
else:
addr = poi[AttrN.ADDRESS].encode(Const.UTF8)
return id, name, type, addr, loc
except Exception, e:
if j<4 :
time.sleep(0.1) # sleep 1s
continue
else:
return "null", "null", "null", "null", "null"
def threadCallingDumpFilesSearch(pd_table, data_range, file_name):
dump_filename = file_name + '_' + data_range.split(',')[0] + '_' + data_range.split(',')[1]
start_p = int(data_range.split(',')[0])
end_p = int(data_range.split(',')[1])
i = start_p
with open(dump_filename, 'w') as f:
for index, row in pd_table.iloc[start_p:end_p, :].iterrows():
id, name, type, addr, loc = recurAmapParentIDSearch(row['poi_name'], row['city_id'])
f.write(row['geohash'] + '\t' + row['poi_name'] + '\t' + type + '\t' + id + '\t' + name + '\n')
print "第" + str(i) + "行已写入!!"
if (i % 10000) == 0:
time.sleep(1)
i = i + 1
f.close()
if __name__ == '__main__':
geo_pois = pd.read_table(filepath_or_buffer='datafiles/poi_name_for_search_buchong_final.txt',sep='\t').iloc[0:5000, :]
data_range = ['0,1000',
'1000,2000',
'2000,3000',
'3000,4000',
'4000,5000']
# 线程列表
threads = []
for i in range(len(data_range)):
thread = Thread(target=threadCallingDumpFilesSearch, args=(geo_pois, data_range[i], 'datafiles/poi_name_for_search_buchong_final_output'))
threads.append(thread)
for i in range(len(data_range)):
threads[i].start()
for i in range(len(data_range)):
threads[i].join()
7 同步原语
上边部分介绍了多线程的实现方式,多线程中还有一个非常重要的内容,就是关于竞态。当多个线程同时访问并修改一个变量(或者同时写入,更新同一个文件、记录)时,如果不对资源先进行锁定的话,就有可能出现错误。这就是多线程程序需要保持同步的原因。
Python有多种同步类型来实现同步,包括锁机制、信号量等等。