Python标准库08 多线程与同步 (threading包)(转载)

Python标准库08 多线程与同步 (threading包)

 

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

 

Python主要通过标准库中的threading包来实现多线程。在当今网络时代,每个服务器都会接收到大量的请求。服务器可以利用多线程的方式来处理这些请求,以提高对网络端口的读写效率。Python是一种网络服务器的后台工作语言 (比如豆瓣网),所以多线程也就很自然被Python语言支持。

(关于多线程的原理和C实现方法,请参考我之前写的Linux多线程与同步,要了解race condition, mutex和condition variable的概念)

 

多线程售票以及同步

我们使用Python来实现Linux多线程与同步文中的售票程序。我们使用mutex (也就是Python中的Lock类对象) 来实现线程的同步:

复制代码
# A program to simulate selling tickets in multi-thread way
# Written by Vamei

import threading
import time
import os

# This function could be any function to do other chores.
def doChore():
    time.sleep(0.5)

# Function for each thread
def booth(tid):
    global i
    global lock
while True: lock.acquire()               # Lock; or wait if other thread is holding the lock if i != 0: i = i - 1                 # Sell tickets
print(tid,':now left:',i) # Tickets left doChore()                # Other critical operations else: print("Thread_id",tid," No more tickets") os._exit(0) # Exit the whole process immediately lock.release()               # Unblock doChore()                    # Non-critical operations # Start of the main function i = 100 # Available ticket number lock = threading.Lock() # Lock (i.e., mutex) # Start 10 threads for k in range(10): new_thread = threading.Thread(target=booth,args=(k,))   # Set up thread; target: the callable (function) to be run, args: the argument for the callable new_thread.start()                         # run the thread
复制代码

我们使用了两个全局变量,一个是i,用以储存剩余票数;一个是lock对象,用于同步线程对i的修改。此外,在最后的for循环中,我们总共设置了10个线程。每个线程都执行booth()函数。线程在调用start()方法的时候正式启动 (实际上,计算机中最多会有11个线程,因为主程序本身也会占用一个线程)。Python使用threading.Thread对象来代表线程,用threading.Lock对象来代表一个互斥锁 (mutex)。

有两点需要注意:

  • 我们在函数中使用global来声明变量为全局变量,从而让多线程共享i和lock (在C语言中,我们通过将变量放在所有函数外面来让它成为全局变量)。如果不这么声明,由于i和lock是不可变数据对象,它们将被当作一个局部变量(参看Python动态类型)。如果是可变数据对象的话,则不需要global声明。我们甚至可以将可变数据对象作为参数来传递给线程函数。这些线程将共享这些可变数据对象。
  • 我们在booth中使用了两个doChore()函数。可以在未来改进程序,以便让线程除了进行i=i-1之外,做更多的操作,比如打印剩余票数,找钱,或者喝口水之类的。第一个doChore()依然在Lock内部,所以可以安全地使用共享资源 (critical operations, 比如打印剩余票数)。第二个doChore()时,Lock已经被释放,所以不能再去使用共享资源。这时候可以做一些不使用共享资源的操作 (non-critical operation, 比如找钱、喝水)。我故意让doChore()等待了0.5秒,以代表这些额外的操作可能花费的时间。你可以定义的函数来代替doChore()。

 

OOP创建线程

上面的Python程序非常类似于一个面向过程的C程序。我们下面介绍如何通过面向对象 (OOP, object-oriented programming,参看Python面向对象的基本概念Python面向对象的进一步拓展) 的方法实现多线程,其核心是继承threading.Thread类。我们上面的for循环中已经利用了threading.Thread()的方法来创建一个Thread对象,并将函数booth()以及其参数传递给改对象,并调用start()方法来运行线程。OOP的话,通过修改Thread类的run()方法来定义线程所要执行的命令。

复制代码
# A program to simulate selling tickets in multi-thread way
# Written by Vamei

import threading
import time
import os

# This function could be any function to do other chores.
def doChore():
    time.sleep(0.5)

# Function for each thread
class BoothThread(threading.Thread):
    def __init__(self, tid, monitor):
        self.tid          = tid
        self.monitor = monitor
        threading.Thread.__init__(self)
def run(self): while True:  monitor['lock'].acquire()       # Lock; or wait if other thread is holding the lock     if monitor['tick'] != 0: monitor['tick'] = monitor['tick'] - 1 # Sell tickets
print(self.tid,':now left:',monitor['tick']) # Tickets left doChore()                   # Other critical operations else: print("Thread_id",self.tid," No more tickets") os._exit(0)      # Exit the whole process immediately monitor['lock'].release()      # Unblock doChore()      # Non-critical operations # Start of the main function monitor = {'tick':100, 'lock':threading.Lock()} # Start 10 threads for k in range(10): new_thread = BoothThread(k, monitor) new_thread.start()
复制代码

我们自己定义了一个类BoothThread, 这个类继承自thread.Threading类。然后我们把上面的booth()所进行的操作统统放入到BoothThread类的run()方法中。注意,我们没有使用全局变量声明global,而是使用了一个词典monitor存放全局变量,然后把词典作为参数传递给线程函数。由于词典是可变数据对象,所以当它被传递给函数的时候,函数所使用的依然是同一个对象,相当于被多个线程所共享。这也是多线程乃至于多进程编程的一个技巧 (应尽量避免上面的global声明的用法,因为它并不适用于windows平台)。

上面OOP编程方法与面向过程的编程方法相比,并没有带来太大实质性的差别。

 

其他

threading.Thread对象: 我们已经介绍了该对象的start()和run(), 此外:

  • join()方法,调用该方法的线程将等待直到改Thread对象完成,再恢复运行。这与进程间调用wait()函数相类似。

 

下面的对象用于处理多线程同步。对象一旦被建立,可以被多个线程共享,并根据情况阻塞某些进程。请与Linux多线程与同步中的同步工具参照阅读。

threading.Lock对象: mutex, 有acquire()和release()方法。

threading.Condition对象: condition variable,建立该对象时,会包含一个Lock对象 (因为condition variable总是和mutex一起使用)。可以对Condition对象调用acquire()和release()方法,以控制潜在的Lock对象。此外:

  • wait()方法,相当于cond_wait()
  • notify_all(),相当与cond_broadcast()
  • nofify(),与notify_all()功能类似,但只唤醒一个等待的线程,而不是全部
threading.Semaphore对象: semaphore,也就是计数锁(semaphore传统意义上是一种进程间同步工具,见Linux进程间通信)。创建对象的时候,可以传递一个整数作为计数上限 (sema = threading.Semaphore(5))。它与Lock类似,也有Lock的两个方法。
threading.Event对象: 与threading.Condition相类似,相当于没有潜在的Lock保护的condition variable。对象有True和False两个状态。可以多个线程使用wait()等待,直到某个线程调用该对象的set()方法,将对象设置为True。线程可以调用对象的clear()方法来重置对象为False状态。
 
 
练习
参照Linux多线程与同步中的condition variable的例子,使用Python实现。同时考虑使用面向过程和面向对象的编程方法。
更多的threading的内容请参考:

http://docs.python.org/library/threading.html

 

总结

threading.Thread

Lock, Condition, Semaphore, Event

#2楼 2012-10-23 00:50 ZZB  
按照你的例子,用condition写了个练习,不知道是不是这个意思?!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env python
#-*- coding:utf-8 -*-
 
import time, os
import threading
 
class BoothThread(threading.Thread):
    def __init__(self, tid, cond):
        self.tid = tid
        self.cond = cond
        threading.Thread.__init__(self)
 
    def run(self):
        self.cond.acquire()
        
        global num 
        num += 1
        if num <= 10:
            self.cond.wait()
            print self.tid, 'drink beer'
        elif num == 11:
            self.cond.notify_all()
 
        self.cond.release()
 
 
if __name__ == '__main__':
    num = 0
    cond = threading.Condition()
    for tid in xrange(100):
        bt = BoothThread(tid, cond)
        bt.start()
#3楼[楼主2012-10-23 09:36 Vamei  
@ZZB
需要获得和释放Condition中的Lock,不然还是会有race condition的可能性。
#4楼 2012-10-23 11:00 ZZB  
@Vamei
博主,我这里不是有self.cond.acquire()和self.cond.release()吗?这两个就是获得锁和释放锁,难道这样写的逻辑还不够?请指教~
#5楼[楼主2012-10-23 11:12 Vamei  
@ZZB
shit,我刚才看得不仔细。错了错了,抱歉啊。
 
#7楼 2013-02-26 11:16 冬火虫子  
我尝试用面向过程的方法做了一下练习,参考了ZZB写的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import os, time, threading
 
def doChore():
    time.sleep(2)
 
def drink(tid, cond):
    global num
    cond.acquire()
 
    num +=1
    if num <=10:
        cond.wait()
        print tid, "drink beer"
    elif num == 11:
        cond.notify_all()
 
    cond.release()
    doChore()
 
 
if __name__ == '__main__':
    num = 0
    cond = threading.Condition()
    for tid in range(100):
        new_thread = threading.Thread(target = drink,args = (tid,cond))
        new_thread.start()
#8楼 2013-02-26 11:20 冬火虫子  
请问:threading.Thread.__init__(self)在子类中的这句代码是做什么的呢?没有这句会有什么情况发生呢?
#9楼[楼主2013-02-26 13:53 Vamei  
@冬火虫子
由于继承了Thread类,所以初始化的时候执行该父类的初始化函数。
文档中要求调用该函数,我没有试过不使用这一句会发生什么。
#10楼 2013-07-21 00:05 海浪轻风  
面向过程的写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import threading
import time
import os
def doCore():
        time.sleep(0.5)
def booth(tid):
        global i
        global cond
        cont.acquire()
        i+=1
        if(i<=10):
                cont.wait()
                print('drink beer')
                doCore()
        elif(i==11):
                cont.notify_all()
        cont.release()
 
i=0
cont=threading.Condition()
for k in range(100):
        new_thread=threading.Thread(target=booth,args=(k,))
        new_thread.start()

面向对象版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import threading,os,time
def doChore():
        time.sleep(0.5)
class WorkThread(threading.Thread):
        def __init__(self,id,monitor):
                self.id=id
                self.monitor=monitor
                threading.Thread.__init__(self)
        def run(self):
                monitor['cont'].acquire()
                monitor['num']=monitor['num']+1
                if(monitor['num']<=10):
                        monitor['cont'].wait()
                        print('drink beer')
                        doChore()
                elif(monitor['num']==11):
                        monitor['cont'].notify_all()
                monitor['cont'].release()
                doChore()
 
 
 
monitor={'num':0,'cont':threading.Condition()}
for i in range(100):
        workThread=WorkThread(i,monitor)
        workThread.start()
#11楼 2013-11-05 11:15 wanyao  
@vamei
为什么面向对象的版本运行起来线程是有顺序的?不科学阿,求解释~
(0, 'now left:', 99)
(1, 'now left:', 98)
(2, 'now left:', 97)
(3, 'now left:', 96)
(4, 'now left:', 95)
(5, 'now left:', 94)
(6, 'now left:', 93)
(7, 'now left:', 92)
(8, 'now left:', 91)
(9, 'now left:', 90)
(0, 'now left:', 89)
(1, 'now left:', 88)
(2, 'now left:', 87)
(3, 'now left:', 86)
(4, 'now left:', 85)
(5, 'now left:', 84)
(6, 'now left:', 83)
(7, 'now left:', 82)
(8, 'now left:', 81)
(9, 'now left:', 80)
(0, 'now left:', 79)
(1, 'now left:', 78)
(2, 'now left:', 77)
(3, 'now left:', 76)
(4, 'now left:', 75)
#12楼 2014-01-16 12:30 icy_123  
PS C:\py> python thread.py
(0, ':now left:', 99)
Exception in thread Thread-1:
Traceback (most recent call last):
File "C:\Program Files (x86)\Python276\lib\threading.py", line 810, in __bootstrap_inner
self.run()
File "thread.py", line 23, in run
monitor['tick'].release()
AttributeError: 'int' object has no attribute 'release'
请问这个是因为windows的系统里的Int没有这个属性么
#13楼 2014-03-24 11:07 圣徒甜茶  
@icy_123
monitor['tick'] 是个整数对象,怎么release。。。
lock()才能release,代码里对应的monitor['lock']
 
posted @ 2014-05-02 13:00  cnshen  阅读(177)  评论(0编辑  收藏  举报