[python] 线程锁

参考:http://blog.csdn.net/kobeyan/article/details/44039831

  

1. 锁的概念

  在python中,存在GIL,也就是全局解释器锁,能够保证同一时刻只有一个线程在运行,在这个方面可以认为是线程安全的,但是在线程运行的时候,是共享内存的,共享相同的数据信息,在python的多线程的情况下就不那么安全了。

  多线程的主要目的为了提高性能与速度,用在无关的方向是最好的,例如在使用爬虫的时候,可以使用多线程来进行爬取数据,因为在这些线程之间没有需要共同操作的数据,从而在这个时候利用是最好的。

  如果需要操作同一份数据,那么必须自己保证数据的安全性。

  如果需要利用多cpu的特性,那么应该使用的是多进程编程,而不是多线程编程,多进程编程为multiprocessing。

2. 给线程加锁的原因

    我们知道,不同进程之间的内存空间数据是不能够共享的,试想一下,如果可以随意共享,谈何安全?但是一个进程中的多个线程是可以共享这个进程的内存空间中的数据的,比如多个线程可以同时调用某一内存空间中的某些数据(只是调用,没有做修改)。
    试想一下,在某一进程中,内存空间中存有一个变量对象的值为num=8,假如某一时刻有多个线程需要同时使用这个对象,出于这些线程要实现不同功能的需要,线程A需要将num减1后再使用,线程B需要将num加1后再使用,而线程C则是需要使用num原来的值8。由于这三个线程都是共享存储num值的内存空间的,并且这三个线程是可以同时并发执行的,当三个线程同时对num操作时,因为num只有一个,所以肯定会存在不同的操作顺序,想象一下下面这样操作过程:
第一步:线程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了。

原文地址:https://www.cnblogs.com/xiaofeiIDO/p/6181084.html