[python] 线程锁
参考:http://blog.csdn.net/kobeyan/article/details/44039831
1. 锁的概念
在python中,存在GIL,也就是全局解释器锁,能够保证同一时刻只有一个线程在运行,在这个方面可以认为是线程安全的,但是在线程运行的时候,是共享内存的,共享相同的数据信息,在python的多线程的情况下就不那么安全了。
多线程的主要目的为了提高性能与速度,用在无关的方向是最好的,例如在使用爬虫的时候,可以使用多线程来进行爬取数据,因为在这些线程之间没有需要共同操作的数据,从而在这个时候利用是最好的。
如果需要操作同一份数据,那么必须自己保证数据的安全性。
如果需要利用多cpu的特性,那么应该使用的是多进程编程,而不是多线程编程,多进程编程为multiprocessing。
2. 给线程加锁的原因
第一步:线程A修改了num的值为7
第二步:线程C不知道num的值已经发生了改变,直接调用了num的值7
第三步:线程B对num值加1,此时num值变为8
第四步:线程B使用了num值8
第五步:线程A使用了num值8
因为num只有一个,而三个操作都针对一个num进行,所以上面的操作过程是完全有可能的,而原来线程A、B、C想要使用的num值应该分别为:7、9、8,这里却变成了:8、8、7。试想一下,如果这三个线程的操作对整个程序的执行是至关重要的,会造成什么样的后果?
因此,出于程序稳定运行的考虑,对于线程需要调用内存中的共享数据时,我们就需要为线程加锁。
1 #!usr/bin/env python 2 from threading import Thread 3 from time import sleep, ctime 4 var = 0 5 class IncreThread(Thread): 6 def run(self): 7 global var 8 print 'before,var is ',var 9 sleep(1) 10 var += 1 11 print 'after,var is ',var 12 13 def use_incre_thread(): 14 threads = [] 15 for i in range(50): 16 t = IncreThread() 17 threads.append(t) 18 19 for i in range(50): 20 threads[i].start() 21 for t in threads: 22 t.join() 23 print 'After 10 times,var is ',var 24 25 if __name__ == '__main__': 26 print 'start at:', ctime() 27 use_incre_thread() 28 print 'end at:', ctime()
执行结果:
第一次:
1 start at: Wed Dec 14 21:20:37 2016 2 before,var is 0 3 before,var is 0 4 before,var is 0 5 before,var is 0 6 before,var is 0 7 before,var is 0 8 before,var is 0 9 before,var is 0 10 before,var is 0 11 before,var is 0 12 after,var is 1 13 after,var is after,var is after,var is after,var is after,var is after,var is after,var is 5 77 5 14 55 15 5 16 after,var is after,var is 77 17 After 10 times,var is 7 18 end at: Wed Dec 14 21:20:38 2016
第二次:
1 start at: Wed Dec 14 21:21:07 2016 2 before,var is 0 3 before,var is 0 4 before,var is 0 5 before,var is 0 6 before,var is 0 7 before,var is 0 8 before,var is 0 9 before,var is 0 10 before,var is 0 11 before,var is 0 12 after,var is 1 13 after,var is 2 14 after,var is after,var is after,var is after,var is 6666 15 after,var is after,var is after,var is after,var is 10101010 16 After 10 times,var is 10 17 end at: Wed Dec 14 21:21:08 2016
上述运算过程中,总体消耗时间都是1秒,但是运算结果为7和10,输出也较为混乱。
接下来对线程进行加锁,例子:
1 #!usr/bin/env python 2 from threading import Thread, Lock 3 from time import sleep, ctime 4 var = 0 5 lock = Lock() #创建(设置)锁 6 class IncreThread(Thread): 7 def run(self): 8 global var 9 lock.acquire() #获取锁 10 print 'before,var is ',var 11 sleep(1) 12 var += 1 13 print 'after,var is ',var 14 lock.release() #释放锁 15 16 def use_incre_thread(): 17 threads = [] 18 for i in range(10): 19 t = IncreThread() 20 threads.append(t) 21 for i in range(10): 22 threads[i].start() 23 for t in threads: 24 t.join() 25 print 'After 10 times,var is ',var 26 27 if __name__ == '__main__': 28 print 'start at:', ctime() 29 use_incre_thread() 30 print 'end at:', ctime()
执行结果:
第一次:
1 start at: Wed Dec 14 21:24:24 2016 2 before,var is 0 3 after,var is 1 4 before,var is 1 5 after,var is 2 6 before,var is 2 7 after,var is 3 8 before,var is 3 9 after,var is 4 10 before,var is 4 11 after,var is 5 12 before,var is 5 13 after,var is 6 14 before,var is 6 15 after,var is 7 16 before,var is 7 17 after,var is 8 18 before,var is 8 19 after,var is 9 20 before,var is 9 21 after,var is 10 22 After 10 times,var is 10 23 end at: Wed Dec 14 21:24:34 2016
第二次:
1 start at: Wed Dec 14 21:26:08 2016 2 before,var is 0 3 after,var is 1 4 before,var is 1 5 after,var is 2 6 before,var is 2 7 after,var is 3 8 before,var is 3 9 after,var is 4 10 before,var is 4 11 after,var is 5 12 before,var is 5 13 after,var is 6 14 before,var is 6 15 after,var is 7 16 before,var is 7 17 after,var is 8 18 before,var is 8 19 after,var is 9 20 before,var is 9 21 after,var is 10 22 After 10 times,var is 10 23 end at: Wed Dec 14 21:26:18 2016
在加锁后,两次执行结果一致(10,大家也可以多尝试几次),但消耗时间为10秒(主要是因为锁,保证了同一时刻只有一个线程在运行,也就是只有一个线程释放锁之后,下一个线程才能执行),总体上按照一下的方式进行执行:
创建(设置)锁Lock();
获取锁;
切换到一个线程去运行;
运行:
指定数量的字节码指令,或者
线程主动让出控制(可以调用times.sleep())
把线程设置成睡眠状态;
解锁;
重复以上步骤。
注:分析一下上面的程序:在某一线程修改var的值时,即给该线程加锁,该线程加锁后,只要是该线程需要调用的代码以及涉及的内存空间,都会立即被锁上,比如这里的"var+=1",其它线程虽然也在并发同时执行,但是不能执行"var+=1"这行代码的,即不能够去访问或修改var这一个共享内存空间的数据,只能等待该线程解锁后才能执行;当该线程解锁后,另一个线程马上加锁再来修改var的值,同时也不允许其它线程占用,如此类推,直到所有线程执行完毕。
另一个加锁实例:
1 #coding: utf-8 2 import threading 3 import time 4 5 counter = 0 6 counter_lock = threading.Lock() #只是定义一个锁,并不是给资源加锁,你可以定义多个锁,像下两行代码,当你需要占用这个资源时,任何一个锁都可以锁这个资源 7 counter_lock2 = threading.Lock() 8 counter_lock3 = threading.Lock() 9 10 #可以使用上边三个锁的任何一个来锁定资源 11 12 class MyThread(threading.Thread):#使用类定义thread,继承threading.Thread 13 def __init__(self,name): 14 threading.Thread.__init__(self) 15 self.name = "Thread-" + str(name) 16 def run(self): #run函数必须实现 17 global counter,counter_lock #多线程是共享资源的,使用全局变量 18 time.sleep(1); 19 if counter_lock.acquire(): #当需要独占counter资源时,必须先锁定,这个锁可以是任意的一个锁,可以使用上边定义的3个锁中的任意一个 20 counter += 1 21 print "I am %s, set counter:%s" % (self.name,counter) 22 counter_lock.release() #使用完counter资源必须要将这个锁打开,让其他线程使用 23 24 if __name__ == "__main__": 25 for i in xrange(1,101): 26 my_thread = MyThread(i) 27 my_thread.start()
再来看两个加锁例子:
example 1
1 import threading 2 import time 3 4 number = 0 5 6 lock = threading.RLock() 7 8 def run(num): 9 lock.acquire() 10 global number 11 number += 1 12 lock.release() 13 print number 14 time.sleep(1) 15 16 if __name__ == "__main__": 17 print "start at:",time.ctime() 18 for i in range(20): 19 t = threading.Thread(target=run, args=(i,)) 20 t.start() 21 print "end at:", time.ctime()
输出结果:
1 start at: Fri Dec 16 16:33:02 2016 2 1 3 2 4 3 5 4 6 5 7 6 8 7 9 8 10 9 11 10 12 11 13 12 14 13 15 14 16 15 17 16 18 17 19 18 20 19 21 end at: 20Fri Dec 16 16:33:02 2016
example 2
1 start at: Fri Dec 16 16:40:07 2016 2 1 3 end at: Fri Dec 16 16:40:07 2016 #希望各位学者解释这一步的原因 4 2 5 3 6 4 7 5 8 6 9 7 10 8 11 9 12 10 13 11 14 12 15 13 16 14 17 15 18 16 19 17 20 18 21 19 22 20
1 /mnt/hgfs/Python/day6$ time python thread_clock6.py | grep 'real' 2 3 real 0m20.073s 4 user 0m0.024s 5 sys 0m0.008s
由执行时间可以更好的说明上面的执行过程,但为什么会这样呢?下面来分析一下:由(2)的分析可知,虽然20个线程都是在同时并发执行run这一个函数,这里与(2)不同在于,(1)只加锁了涉及修改number的程序代码,而这里是加锁了整个函数!所以在20个线程同时开始并发执行这个函数时,由于每一个线程的执行都要加锁,并且加锁的是整个执行的函数,因此其它线程就无法调用该函数中的程序代码,只能等待一个线程执行完毕后再调用该函数的程序代码,如此一来,一个线程的执行需要sleep(1)一次,则20个线程的执行就需要sleep(1) 20次,并且该过程是串行的,因此我们才看到如上面所说的程序执行过程,也可以清晰的知道为什么程序的执行需要20s了。