不管哪种编程语言,多线程都是必不可少的。这种提高工作效率的神器,怎么重视都不过分。多线程,就是将多个线性顺序执行的过程变成并行运行。并行的数量越多,效率就越高。如果需要放空一个水池的水,打开多个放水孔的效率显然要比打开一个放水孔的效率高。这种做法是典型的以资源换时间。
1、Project 1分析
首先设计一个简单的程序threadingOrderRun.py,打开Putty连接到Linux,执行命令:
cd code/crawler
vi threadingOrderRun.py
threadingOrderRun.py的代码如下:
1 #!/usr/bin/env python3
2 #-*- coding:utf-8 -*-
3 __author__ = 'hstking hst_king@hotmail.com'
4
5 import time
6
7 def showName(name):
8 nowTime = time.strftime('%H:%M:%S', time.localtime(time.time()))
9 print('My name is function-%s, now time: %s ' %(name, nowTime))
10 time.sleep(1)
11
12
13 if __name__ == '__main__':
14 for i in range(20): #没有开多线程的情况下,执行20次操作
15 showName(i)
这个函数的功能很简单,就是从列表中提取名字作为showName的参数,然后显示名字和当前时间。最后的time.sleep(1)是因为这个过程太快了,所以休眠1秒以便于观察。这是一种理所当然的顺序操作方法,但理所当然的操作并不是最有效率的操作。这种方法完全就是线性操作,从列表中取出一个元素,就用函数处理一个元素。现在运行程序试试,执行命令:
time python3 threadingOrderRun.py
执行结果如图1所示。
图1 线性顺序执行
time命令是Linux特有的命令,只能在Linux下执行。Windows下执行时可以查看打印出的时间。 从图1中可以看出,该程序执行时间为20.070秒。如果列表比较小,那还可以忍受。如果这个列表比较大呢?1000个元素至少得1000秒,那就完全无法接受了。明明计算机有多余的资源,却花费了这么多的时间。完全可以利用资源来换取时间,用多线程操作。在同一时间内运行多个线程(事实上线程并不是同时运行的,是基于时间片轮转的方式执行。但对于操作者来说,跟同时运行并没有什么区别)。这样虽然占用了一定的资源,但大大地节省了时间。毕竟绝大多数的情况下都是希望时间优先的。 2、Project 1实施
在Python中,多线程的模块是threading模块。如果要完全深入了解threading模块,恐怕得好好地读一下Python的说明文档。如果只是要使用,那就很简单了。这里需要注意的是,使用threading模块进行多线程操作有两种方法。一种是以函数的形式调用,一种是以类的方式调用。
先以函数的形式使用多线程。打开Putty连接到Linux,执行命令:
cd code/crawler
vi threadingOfFunction.py
threadingOfFunction.py的代码如下:
1 #!/usr/bin/env python3
2 #-*- coding:utf-8 -*-
3 __author__ = 'hstking hst_king@hotmail.com'
4
5 import time
6 import threading
7
8 def showName(threadNum ,name):
9 nowTime = time.strftime('%H:%M:%S', time.localtime(time.time()))
10 print('I am thread-%d ,My name is function-%s, now time: %s '%(threadNum, name, nowTime))
11 time.sleep(1)
12
13 if __name__ == '__main__':
14 print('I am main ...')
15 names = range(20)
16 threadNum = 1 #threadNum 指的是线程执行的批次。
17 threadPool = [] #线程池
18 while names:
19 for i in range(6):
20 try: #这里需要考虑列表已经读取完毕的情况
21 name = names.pop()
22 except IndexError as e:
23 print('The list is empty')
24 break
25 else:
26 t = threading.Thread(target=showName, args=(i, name, ))
27 threadPool.append(t)
28 t.start()
29 while threadPool: #也可以用for循环,然后清空threadPool线程池
30 t = threadPool.pop()
31 t.join() #使用join是为了阻塞主函数,意思是必须将t这个函数执行完毕后才能继续执行主函数
32 threadNum += 1
33 print('----------------------\r\n')
34 print('main is over ...')
以函数的形式使用多线程,就是用threading.Thread(target=functionName, args=(arguments,))的方法代入需要进行多线程操作的函数和函数所需的参数(如果没有参数更好)。
如果代入的函数有一个参数,那么args要写成args=(arg1, )。如果有两个参数,那么args就要写成args=(arg1, arg2, )。总之在参数元组的最后要留出一个空位。
在程序的第29~31行,使用了join函数是为了阻塞主函数,意思是主线程必须等待多线程执行完毕后才能正常结束。其实这里也可以不用join函数,多线程的daemon属性默认是false。这种情况下主线程本来就是要等待多线程执行完毕才会结束的。Join函数更多的是用在多进程。
执行程序,运行命令:
time python3 threadingOfFunction.py
执行结果如图2所示。
图2 函数式多线程
从图2可以看出,这次操作只花费了4.002秒。与顺序操作的20.045秒相比节约了一大半的时间。在计算机可以承受的范围内,调大线程的数量,还可以将运行时间进一步缩短。
以类的方式使用多线程。打开Putty连接到Linux,执行命令:
cd code/crawler
vi threadingOfClass.py
threadingOfClass.py的代码如下:
1 #!/usr/bin/env python3
2 #-*- coding:utf-8 -*-
3 __author__ = 'hstking hst_king@hotmail.com'
4
5 import time
6 import threading
7
8 class ShowName(threading.Thread): #这里的类名要大写,该类继承于threading.Thread类
9 def __init__(self, threadNum ,name):
10 threading.Thread.__init__(self) #这一步是必不可少的
11 self.name = name
12 self.threadNum = threadNum
13
14 def run(self):
15 nowTime = time.strftime('%H:%M:%S', time.localtime(time.time()))
16 print('I am thread-%d ,My name is function-%s, now time: %s '%(self.threadNum, self.name, nowTime))
17 time.sleep(1)
18
19 if __name__ == '__main__':
20 print('I am main ...')
21 names = [x for x in range(20)]
22 threadNum = 1
23 threadPool = []
24 while names:
25 for i in range(6):
6 try: #考虑线程已经读取完毕的情况
27 name = names.pop()
28 except IndexError as e:
29 print('The List is empty')
30 break
31 else:
32 t = ShowName(i, name) #这里调用ShowName几乎与直接调用threading.Thread是一样的。
33 threadPool.append(t)
34 t.start()
35 while threadPool:
36 t = threadPool.pop()
37 t.join()
38 threadNum += 1
39 print('===============\r\n')
40 print('main is over ...')
执行程序,运行命令:
time python3 threadingOfClass.py
执行结果如图3所示。
图3 类式多线程
从图3中可以看到,threadingOfClass.py的运行时间是4.019秒,跟threadingOfFunction.py的4.002相当接近,说明这两种方法从效率上来说没有什么区别,任选一种使用都可以。
这两种方法都是对类的调用。所谓函数式的调用是对threading.Thread类的直接调用,而类式的调用则是先继承threading.Thread类,重载子类的函数后再调用子类的方式调用,只是前者看起来更像函数调用而已。
3、Project 2分析
多线程的好处在于可以并行运行重复性的工作,大大地减少了运行时间,但使用多线程也难免会忙中出错。例如,重复地往一个文件内写入单行内容。如果单线程的线性执行,很难出错。如果多线程执行时那就不一定了。当多个线程同时向文件内写入内容时,会不会造成一个线程写入成功、其他的线程都做了无用功呢?不妨测试一下。
打开Putty连接到Linux,执行命令:
cd code/crawler
vi threadingWithoutLock.py
threadingWithoutLock.py的代码如下:
1 #!/usr/bin/env python3
2 #-*- coding:utf-8 -*-
3 __author__ = 'hstking hst_king@hotmail.com'
4
5 import time
6 import threading
7 import codecs
8
9 def showName(threadNum ,name):
10 with codecs.open('test.txt', 'a', 'utf-8') as fp:
11 nowTime = time.strftime('%H:%M:%S',time.localtime(time.time()))
12 fp.write('I am thread-%d ,My name is function-%s, now time:%s\r\n ' %(threadNum, name, nowTime))
13 print('I am thread-%d ,My name is function-%s, now time: %s '%(threadNum, name, nowTime))
14 time.sleep(1)
15
16
17 if __name__ == '__main__':
18 with codecs.open('test.txt', 'w', 'utf-8') as fp:
19 fp.write('')
20 print('I am main ...')
21 names = [x for x in range(100)]
22 threadNum = 1
23 threadPool = []
24 while names:
25 for i in range(13):
26 try:
27 name = names.pop()
28 except IndexError as e:
29 print('The list is empty')
30 break
31 else:
32 t = threading.Thread(target=showName, args=(i, name, ))
33 threadPool.append(t)
34 t.start()
35 while threadPool:
36 t = threadPool.pop()
37 t.join()
38 threadNum += 1
39 print('main is over ...')
这个程序与之前的多线程程序基本上没什么区别,只是增加了总共线程的数量(100个),并将输出写入了一个文件中。当执行的总线程比较少,同时执行的线程也不多的情况下也许不会出现问题。一旦数量上去了,问题就比较突出了。理论上names列表有100个元素,那么就应该有100行字符串写入到了test.txt文本中。
执行程序测试一下。运行命令:
time python3 threadingWithoutLock.py
ls -l test.txt
wc -l test.txt
执行结果如图4所示。
图4 Linux下未使用线程锁的多线程
可以看出在Linux下还是正常的结果。现在将这个程序复制到Windows下执行,执行结果如图5所示。
图5 Windows下未使用线程锁的多线程
只有91行的数据写入到了文件内,还有9行数据丢失了。为了避免类似的情况,Python采用了线程锁的方法确保文件的安全。使用线程锁锁定资源,避免干扰。
4、Project 2实施
在Python的多线程中有两种锁,一种是互斥锁,另一种是可重入锁。这两者的区别是互斥锁只能锁定一次,解锁一次,而可重入锁可以锁定多次。一般都是使用的互斥锁。至于互斥锁的效果如何,可以将上个例子稍微修改一下,加上互斥锁测试一下即可。
打开Putty连接到Linux,执行命令:
cd code/crawler
vi threadingWithLock.py
threadingWithLock.py的代码如下:
1 #!/usr/bin/env python3
2 #-*- coding:utf-8 -*-
3 __author__ = 'hstking hst_king@hotmail.com'
4
5 import time
6 import threading
7 import codecs
8
9 def showName(threadNum ,name):
10 mutex.acquire()
11 with codecs.open('test.txt', 'a', 'utf-8') as fp: #写入到test.txt文件内
12 nowTime = time.strftime('%H:%M:%S', time.localtime(time.time()))
13 fp.write('I am thread-%d ,My name is function-%s, now time:%s\r\n ' %(threadNum, name, nowTime))
14 print('I am thread-%d ,My name is function-%s, now time: %s '
%(threadNum, name, nowTime))
15 mutex.release()
16 time.sleep(1)
17
18
19 if __name__ == '__main__':
20 with codecs.open('test.txt', 'w', 'utf-8') as fp:
21 fp.write('')
22 print('I am main ...')
23 mutex = threading.Lock()
24 names = [x for x in range(100)]
25 threadNum = 1
26 threadPool = []
27 while names:
28 for i in range(13):
29 try:
30 name = names.pop()
31 except IndexError as e:
32 print('The list is empty')
33 break
34 else:
35 t = threading.Thread(target=showName, args=(i, name, ))
36 threadPool.append(t)
37 t.start()
38 while threadPool:
39 t = threadPool.pop()
40 t.join()
41 threadNum += 1
42 print('main is over ...')
对比一下代码,很容易理解,就是所有线程在写入文件前加上一个互斥锁,锁定资源。
等写入完毕后再释放互斥锁。
这样就确保数据一定可以写入到文件内了。
执行程序测试一下。运行命令:
time python3 threadingWithLock.py
ls -l test.txt
wc -l test.txt
执行结果如图6所示。
图6 Linux下使用线程锁的多线程
检查一下test.txt文件,正好100行,符合设计的结果。再将threadingWithLock.py程序复制到Windows下运行一下。执行结果如图7所示。
图7 Windows下使用线程锁的多线程
通过对比可以看出使用线程锁的情况下数据才能保证安全。程序才能按照设计的方式运行。如果不使用线程锁,Linux下没什么问题(这只是特例,并不代表不使用线程锁Linux下就是安全的)。Windows下必定会出现这样那样的问题。