Python-多线程学习总结

我们在做软件开发的时候很多要用到多线程技术。例如如果做一个下载软件象flashget就要用到、象在线视频工具realplayer也要用到因为要同时下载media stream还要播放。其实例子是很多的。线程相对进程来说是“轻量级”的,操作系统用较少的资源创建和管理线程。程序中的线程在相同的内存空间中执行,并共享许多相同的资源。


在python中如何创建一个线程对象
如果你要创建一个线程对象,很简单,只要你的类继承threading.Thread,然后在__init__里首先调用threading.Thread的__init__方法即可

import threading

class mythread(threading.Thread):

    def __init__(self, threadname):

    threading.Thread.__init__(self, name = threadname)

....    

这才仅仅是个空线程,我可不是要他拉空车的,他可得给我干点实在活。很简单,重写类的run()方法即可,把你要在线程执行时做的事情都放到里面

import threading

import time

class mythread(threading.Thread):

    def __init__(...):

        ....

    def run(self):

        for i in range(10):

            print self.getName(), i

            time.sleep(1)

以上代码我们让这个线程在执行之后每隔1秒输出一次信息到屏幕,10次后结束,而继承threading.Thread的类也只重写__init__(), run()这两个方法即可,当然也可以加其他自定义方法
getName()是threading.Thread类的一个方法,用来获得这个线程对象的name。还有一个方法setName()当然就是来设置这个线程对象的name的了。


如果要创建一个线程,首先就要先创建一个线程对象

mythread1 = mythread('mythread 1')

一个线程对象被创建后,他就处于“born”(诞生状态)
如何让这个线程对象开始运行呢?只要调用线程对象的start()方法即可,start()方法会自动调用线程对象的run()方法

mythread1.start()

现在线程就处于“ready”状态或者也称为“runnable”状态。


奇怪吗?不是已经start了吗?为什么不称为“running”状态呢?其实是有原因的。因为我们的计算机一般是不具有真正并行处理能力的。我们所谓的多线程只是把时间分成片段,然后隔一个时间段就让一个线程执行一下,然后进入“sleeping ”状态,然后唤醒另一个在“sleeping”的线程,如此循环runnable->sleeping->runnable... ,只是因为计算机执行速度很快,而时间片段间隔很小,我们感受不到,以为是同时进行的。所以说一个线程在start了之后只是处在了可以运行的状态,他什么时候运行还是由系统来进行调度的。

那一个线程什么时候会“dead”呢?一般来说当线程对象的run方法执行结束或者在执行中抛出异常的话,那么这个线程就会结束了。系统会自动对“dead”状态线程进行清理。

如果一个线程t1在执行的过程中需要等待另一个线程t2执行结束后才能运行的话那就可以在t1在调用t2的join()方法

....

def t1(...):

...

t2.join()

...

这样t1在执行到t2.join()语句后就会等待t2结束后才会继续运行。

但是假如t2是个死循环的话那么等待就没有意义了,那怎么办呢?可以在调用t2的join()方法的时候给一个浮点数做超时参数,这样这个线程就不会等到花儿也谢了了。我等你10s,你不回来我还不允许我改嫁啊?:)

def t1(...):

...

t2.join(10)

...


如果一个进程的主线程运行完毕而子线程还在执行的话,那么进程就不会退出,直到所有子线程结束为止,如何让主线程结束的时候其他子线程也乖乖的跟老大撤退呢?那就要把那些不听话的人设置为听话的小弟,使用线程对象的setDaemon()方法,参数为bool型。True的话就代表你要听话,我老大(主线程)扯呼,你也要跟着撤,不能拖后腿。如果是False的话就不用那么听话了,老大允许你们将在外军命有所不受的。需要注意的是setDaemon()方法必须在线程对象没有调用start()方法之前调用,否则没效果。

t1 = mythread('t1')

print t1.getName(),t1.isDaemon()

t1.setDaemon(True)

print t1.getName(),t1.isDaemon()

t1.start()

print 'main thread exit'


当执行到 print 'main thread exit' 后,主线程就退出了,当然t1这个线程也跟着结束了。但是如果不使用t1线程对象的setDaemon()方法的话,即便主线程结束了,还要等待t1线程自己结束才能退出进程。isDaemon()是用来获得一个线程对象的Daemonflag状态的。

如何来获得与线程有关的信息呢?
获得当前正在运行的线程的引用

running = threading.currentThread()

获得当前所有活动对象(即run方法开始但是未终止的任何线程)的一个列表

threadlist = threading.enumerate()

获得这个列表的长度

threadcount = threading.activeCount()

查看一个线程对象的状态调用这个线程对象的isAlive()方法,返回1代表处于“runnable”状态且没有“dead”

threadflag = threadingObj.isAlive()

 

多个执行线程经常要共享数据,如果仅仅读取共享数据还好,但是如果多个线程要修改共享数据的话就可能出现无法预料的结果。

假如两个线程对象t1和t2都要对数值num=0进行增1运算,那么t1和t2都各对num修改10次的话,那么num最终的结果应该为20。但是如果当t1取得num的值时(假如此时num为0),系统把t1调度为“sleeping”状态,而此时t2转换为“running”状态,此时t2获得的num的值也为0,然后他把num+1的值1赋给num。系统又把t2转化为“sleeping”状态,t1为“running”状态,由于t1已经得到num值为0,所以他也把num+1的值赋给了num为1。本来是2次增1运行,结果却是num只增了1次(用锁的原因, 系统没准儿在哪个线程执行的时候断开去跟别的线程好了)。类似这样的情况在多线程同时执行的时候是有可能发生的。所以为了防止这类情况的出现就要使用线程同步机制

最简单的同步机制就是“锁

锁对象用threading.RLock或threading.Lock类创建(其中Lock不允许在acquire之后再次acquire, 而RLock可以, 但是必须成对release)

mylock = threading.RLock()

    如何使用锁来同步线程呢?线程可以使用锁的acquire() (获得)方法,这样锁就进入“locked”状态。每次只有一个线程可以获得锁(所以可以把修改共享数据的代码放在获得锁acquire和释放锁release之间, 因为只有一个线程会获得锁并执行其中代码, 即只有一个线程在获得值并修改值)。如果当另一个线程试图获得这个锁的时候,就会被系统变为“blocked”状态,直到那个拥有锁的线程调用锁的release() (释放)方法,这样锁就会进入“unlocked”状态。“blocked”状态的线程就会收到一个通知,并有权利获得锁。如果多个线程处于“blocked”状态,所有线程都会先解除“blocked”状态,然后系统选择一个线程来获得锁,其他的线程继续沉默(“blocked”)。

import threading

mylock = threading.RLock()

class mythread(threading.Thread)

    ...

    def run(self ...):

        ...     #此处 不可以 放置修改共享数据的代码

        mylock.acquire()

        ...     #此处 可以 放置修改共享数据的代码

        mylock.release()

        ...     #此处 不可以 放置修改共享数据的代码


我们把修改共享数据的代码称为“临界区”,必须将所有“临界区”都封闭在同一锁对象的acquire()和release()方法调用之间。

锁只能提供最基本的同步级别。有时需要更复杂的线程同步,例如只在发生某些事件时才访问一个临界区(例如当某个数值改变时)。这就要使用“条件变量”。


条件变量用threading.Condition类创建

mycondition = threading.Condition()

    条件变量是如何工作的呢?首先一个线程成功获得一个条件变量后,调用此条件变量的wait()方法会导致这个线程释放这个锁,并进入“blocked”状态,直到另一个线程调用同一个条件变量的notify()方法来唤醒那个进入“blocked”状态的线程。如果调用这个条件变量的notifyAll()方法的话就会唤醒所有的在等待的线程。

    如果程序或者线程永远处于“blocked”状态的话,就会发生死锁。所以如果使用了锁、条件变量等同步机制的话,一定要注意仔细检查,防止死锁情况的发生。对于可能产生异常的临界区要使用异常处理机制中的finally子句来保证释放锁(避免出错跳出, 而未释放锁!!)。等待一个条件变量的线程必须用notify()方法显式的唤醒,否则就永远沉默。保证每一个wait()方法调用都有一个相对应的notify()调用,当然也可以调用notifyAll()方法以防万一。

我们也经常会采用生产者/消费者关系的两个线程来处理一个共享缓冲区的数据。例如一个生产者线程接受用户数据放入一个共享缓冲区里,等待一个消费者线程对数据取出处理。但是如果缓冲区的太小而生产者和消费者两个异步线程的速度不同时,容易出现一个线程等待另一个情况。为了尽可能的缩短共享资源并以相同速度工作的各线程的等待时间,我们可以使用一个“队列”来提供额外的缓冲区

创建一个“队列”对象

import Queue

myqueue = Queue.Queue(maxsize = 10)

Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。

将一个值放入队列中

myqueue.put(10)

调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元(此处空是由于达到maxsize上限)。如果block为0,put方法将引发Full异常。

将一个值从队列中取出

myqueue.get()

调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为1。如果队列为空且block为1,get()就使调用线程暂停,一直阻塞,直到从queue中获取到一个值。如果block为0,队列将引发Empty异常。

我们用一个例子来展示如何使用Queue

# queue_example.py

from Queue import Queue

import threading

import random

import time



# Producer thread

class Producer(threading.Thread):

    def __init__(self, threadname, queue):

        threading.Thread.__init__(self, name = threadname)

        self.sharedata = queue

    def run(self):

        for i in range(20):

            print self.getName(),'adding',i,'to queue'

            self.sharedata.put(i)

            time.sleep(random.randrange(10)/10.0)

        print self.getName(),'Finished'



# Consumer thread

class Consumer(threading.Thread):

    def __init__(self, threadname, queue):

        threading.Thread.__init__(self, name = threadname)

        self.sharedata = queue

    def run(self):

        for i in range(20):

            print self.getName(),'got a value:',self.sharedata.get()

            time.sleep(random.randrange(10)/10.0)

        print self.getName(),'Finished'



# Main thread

def main():

    queue = Queue()
producer = Producer('Producer', queue) consumer
= Consumer('Consumer', queue)    print 'Starting threads ...' producer.start() consumer.start() producer.join() consumer.join() print 'All threads have terminated.' if __name__ == '__main__': main()

PS:main()中可以把以下两行交换(由于get()时默认没有get到就阻塞, 直到get成功), 而如果Producer中for i in range(20)的20改为19则consumer就会一直处于阻塞状态

    producer = Producer('Producer', queue)

    consumer = Consumer('Consumer', queue)

一般使用Queue的步骤如下:

1.创建一个 Queue.Queue() 的实例,然后使用数据对它进行填充。    

2.将经过填充数据的实例传递给线程类,后者是通过继承 threading.Thread 的方式创建的。    

3.生成守护线程池。    

4.每次从队列中取出一个项目,并使用该线程中的数据和 run 方法以执行相应的工作。    

5.在完成每个项目之后,使用 queue.task_done() 函数向任务已经完成的队列发送一个信号。    

6.在主程序中对队列执行 join 操作(保持阻塞状态,直到处理了队列中的所有项目为止。在将一个项目添加到该队列时,未完成的任务的总数就会增加。当使用者线程调用 task_done() 以表示检索了该项目、并完成了所有的工作时,那么未完成的任务的总数就会减少。当未完成的任务的总数减少到零时,join() 就会结束阻塞状态),实际上意味着等到队列全部task_done()或者queue.empty()(即队列为空),再退出主程序。

7.也可不用queue在主程序中join, 而用每个线程在主程序中join, 等每个线程run(while True, 如果队列为空(queue.Empty())则break, 否则get(), 继续执行)完后退出主程序(既可以用queue.task_done()与queue.join()配合, 又可以用每个线程.join(), 不过有了queue谁还用每个线程去join呢~~)

 

关于join()

1 join方法的作用是阻塞主进程无法执行join以后的语句,专注执行多线程,必须等待多线程执行完毕之后才能执行主线程的语句

2 多线程多join的情况下,依次执行各线程的join方法,前一个结束之后,才能执行后一个

3 无参数,则等待到该线程结束,才开始执行下一个线程的join。

4 设置参数后,则等待该线程N秒之后不管该线程是否结束,就开始执行后面的主进程。

 

再贴个例子体会一下

#!/home/oracle/dbapython/bin/python

#encoding=utf-8

import threading

import time

from Queue import Queue



class Producer(threading.Thread):

    def run(self):

        global queue

        count = 0

        while True:

            for i in range(100):

                if queue.qsize() > 1000:

                     pass

                else:

                     count = count +1

                     msg = '生成产品'+str(count)

                     queue.put(msg)

                     print msg

            time.sleep(1)



class Consumer(threading.Thread):

    def run(self):

        global queue

        while True:

            for i in range(3):

                if queue.qsize() < 50:

                    pass

                else:

                    msg = self.name + '消费了 '+queue.get()

                    print msg

            time.sleep(1)



queue = Queue()



def test():

    for i in range(100):

        msg='初始产品'+str(i)

        print msg

        queue.put(msg)

    for i in range(2):

        p = Producer()

        p.start()

    for i in range(5):

        c = Consumer()

        c.start()

if __name__ == '__main__':

    test()

结果如下:

初始产品0

初始产品1

初始产品2

初始产品3

初始产品4

初始产品5

初始产品6

初始产品7

初始产品8

...

初始产品92

初始产品93

初始产品94

初始产品95

初始产品96

初始产品97

初始产品98

初始产品99

生成产品1

生成产品2

生成产品3

生成产品4

 生成产品1

生成产品2

生成产品3

生成产品4

生成产品5

生成产品5

Thread-3消费了 初始产品0

生成产品6

生成产品7

生成产品6

生成产品8

Thread-3消费了 初始产品1

Thread-5消费了 初始产品2

Thread-4消费了 初始产品3

Thread-6消费了 初始产品4

生成产品7Thread-7消费了 初始产品5



生成产品9

Thread-3消费了 初始产品6

Thread-5消费了 初始产品7

Thread-4消费了 初始产品8

Thread-6消费了 初始产品9

Thread-7消费了 初始产品10

生成产品8

生成产品10

Thread-5消费了 初始产品11

Thread-4消费了 初始产品12

Thread-6消费了 初始产品13

Thread-7消费了 初始产品14

生成产品9生成产品11



生成产品10

生成产品12

生成产品11

生成产品13生成产品12

生成产品13

...

再来一个例子:

#coding:utf-8  

''''' 

今天我们来学习一下python里的多线程问题,并用一个多线程爬虫程序来实验。 

@author FC_LAMP 

有几点要说明一下: 

1) 线程之间共享状态、内存、资源,并且它们相互间易于通信。 

'''  

import threading,urllib2  

import datetime,time  

import Queue  

  

hosts = ['http://www.baidu.com','http://news.163.com/','http://weibo.com/u/2043979614','http://fc-lamp.blog.163.com']  

  

class ThreadClass(threading.Thread):  

    def __init__(self,queue):  

        threading.Thread.__init__(self)  

        self.queue = queue  

          

    def run(self):  

        ''''' 

          run 方法用于要执行的功能 

        '''  

        #getName()用于获取线程名称  

        while True:  

            #从队列中获取一个任务  

            host = self.queue.get()  

            #抓取工作  

            url = urllib2.urlopen(host)  

            print url.read(500)  

            #标记队列工作已完成  

            self.queue.task_done()  

  

def main():  

    #创建队列实例  

    queue = Queue.Queue()  

    #生成一个线程池  

    for i in range(len(hosts)):  

        t = ThreadClass(queue)  

        #主程序退出时,子线程也立即退出  

        t.setDaemon(True)  

        #启动线程  

        t.start()  

          

    #向队列中填充数数  

    for host in hosts:  

        queue.put(host)  

  

    #只到所有任务完成后,才退出主程序  

    queue.join()  

  

if __name__=='__main__':  

      

    st = time.time()  

    main()  

    print '%f'%(time.time()-st)  

看个够:

    #coding:utf-8  

    import socket  

    import sys  

    import time  

    import Queue  

    import threading  

      

    host = 'localhost'  

    port = 8000  

      

    #创建socket对象  

    s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  

      

    #绑定一个特定地址,端口  

    try:  

        s.bind((host,port))  

    except  Exception as e :  

        print 'Bind Failed:%s'%(str(e))  

        sys.exit()  

    print 'Socket bind complete!!'  

      

    #监听连接  

    s.listen(10) #最大连接数10  

      

    #创建连接队列  

    queue = Queue.Queue()  

      

    #创建线程  

    class TaskThread(threading.Thread):  

        def __init__(self):  

            threading.Thread.__init__(self)  

              

        def run(self):  

            while 1:  

                t = queue.get()  

                t.send('welecome.....')  

                #接收数据  

                client_data = t.recv(1024)  

                t.sendall(client_data)  

                #释放资源  

                #t.close()              

              

      

    #接受连接  

    while 1:  

      

        #将连接放入队列  

        conn,addr = s.accept()  

        print 'Connected from %s:%s'%(addr[0],str(addr[1]))  

        queue.put(conn)  

      

        #生成线程池  

        th = TaskThread()  

        th.setDaemon(True)  

        th.start()  

      

        queue.join()  

    s.close()  

后续:

  像threading.Thread中的方法, 比如getName(), setName(), isAlive(), setDaemon(), isDaemon()等, threading.Condition中的方法, Queue中的方法使用, 请自行Google

你可能感兴趣的:(python)