python多线程threading thread的基本使用 python多线程例子_python多线程


不管哪种编程语言,多线程都是必不可少的。这种提高工作效率的神器,怎么重视都不过分。多线程,就是将多个线性顺序执行的过程变成并行运行。并行的数量越多,效率就越高。如果需要放空一个水池的水,打开多个放水孔的效率显然要比打开一个放水孔的效率高。这种做法是典型的以资源换时间。


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所示。

python多线程threading thread的基本使用 python多线程例子_python多线程_02

图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所示。

python多线程threading thread的基本使用 python多线程例子_多线程_03

图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所示。

python多线程threading thread的基本使用 python多线程例子_python 多线程_04

图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所示。

python多线程threading thread的基本使用 python多线程例子_python 多线程_05

图4  Linux下未使用线程锁的多线程

可以看出在Linux下还是正常的结果。现在将这个程序复制到Windows下执行,执行结果如图5所示。

python多线程threading thread的基本使用 python多线程例子_线程锁_06

图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所示。

python多线程threading thread的基本使用 python多线程例子_python多线程_07

图6  Linux下使用线程锁的多线程

检查一下test.txt文件,正好100行,符合设计的结果。再将threadingWithLock.py程序复制到Windows下运行一下。执行结果如图7所示。

python多线程threading thread的基本使用 python多线程例子_python多线程_08

图7  Windows下使用线程锁的多线程

通过对比可以看出使用线程锁的情况下数据才能保证安全。程序才能按照设计的方式运行。如果不使用线程锁,Linux下没什么问题(这只是特例,并不代表不使用线程锁Linux下就是安全的)。Windows下必定会出现这样那样的问题。