threading.local()、多线程里全局变量锁

这个人的系列文章值得一读:http://blog.51cto.com/suhaozhi/category3.html/p2,不过这个系列总共15偏,Python并发入门,有很多文字描述错误,有些道理也是错的,特别是进程那块,竟然说和线程等同,筛选着看就行

你需要对多线程程序中的临界区加锁以避免竞争条件。

要在多线程程序中安全使用可变对象,你需要使用 threading 库中的 Lock 对象,就像下边这个例子这样:

import threading

class SharedCounter:
    '''
    A counter object that can be shared by multiple threads.
    '''
    def __init__(self, initial_value = 0):
        self._value = initial_value
        self._value_lock = threading.Lock()

    def incr(self,delta=1):
        '''
        Increment the counter with locking
        '''
        with self._value_lock:
             self._value += delta

    def decr(self,delta=1):
        '''
        Decrement the counter with locking
        '''
        with self._value_lock:
             self._value -= delta

Lock 对象和 with 语句块一起使用可以保证互斥执行,就是每次只有一个线程可以执行 with 语句包含的代码块。with 语句会在这个代码块执行前自动获取锁,在执行结束后自动释放锁。

 

多线程与互斥锁

一、锁的概念。

  锁,通常被用来实现共享数据的访问,为每一个共享的数据,创建一个Lock对象(一把锁),当需要访问这个共享的资源时,可以调用acquire方法来获取一个锁的对象,当共享资源访问结束后,在调用release方法去解锁;最好结合with使用,因为with可以自动释放锁,也会处理异常导致释放锁失败的问题。

二、python中的互斥锁。

  在介绍互斥锁之前,先来一起看一个例子。(每个线程对num实现一次-1的操作)

import threading
import  time

num = 200  #每个线程都共享这个变量。
tread_list = []

def add_num():
    global num  #每个线程都去获取这个全局变量。
    temp_num = num
    time.sleep(0.1) #执行sleep,相当于执行IO操作.
    num = temp_num - 1 #对公共的变量做一个-1的操作。
for i in range(200):      #同时开启200个线程
    t = threading.Thread(target=add_num)
    t.start()  # 子线程准备就绪,等待获取CPU时间片执行线程任务add_num
    tread_list.append(t)
for t in tread_list:
    t.join()  # 表示主线程会等待所有子线程执行结束后继续往下执行下面的代码语句,即执行:print "ending....num = %s" %(num)
print "ending....num = %s" %(num)

最后的结果就是:ending....num = 199

结果并不是我们想要的。来分析下为何会出现这种现象。

  200个线程现在想统一修改一个全局变量,由于python解释器的GIL(全局解释锁)锁的限制,每次只能有一个线程在cpu上运行,在执行到sleep时,就相当于一次I/O操作,这时就会切到其他的线程,在执行sleep之前,当前运行的这个线程,这个线程取到的全局变量的值是200(temp_num = 200),还没来得及做修改,就被切换到其他线程了,其他的线程也是一样的道理,取到temp_num = 200这个值后,还没来得及计算,执行到sleep触发一次IO操作后,又切到了其他的线程,第2个第3个直到最后一个线程都拿到了temp_num=200这个变量后,后面的计算操作才会开始运行!(不要忘记一个概念,线程在切换之前,是会保存当前执行状态的)当所有线程都拿到了emp_num=200这个变量后,每个线程都会自己执行一遍

num = temp_num - 1这也就导致了每个线程执行的都是200-1 所以,最后的结果就等于199.

 

下面执行的结果却是200,为什么?

因为主线程没有使用join,所以主线程没有等待子线程结束就先结束了,这个时候,所有的子线程还没有来得及做减一操作,所以num的值还是200。

此时的200个子线程就成了孤儿线程,会有系统的初始进程init进行托管,等子线程执行结束后,init把子线程的资源进行回收。

最后我们再来查看num的值,依然是199,这是因为子线程被遗弃后成为孤儿线程,但是依然长大成人,完成了自己的任务,然后被init回收。

    

 

  还拿刚刚写的那个减法程序举例,我们把sleep的时间缩短为0.001秒看看会出现什么效果?还是上一段代码,只不过把add_num函数的time.sleep(0.1)改为time.sleep(0.001)看看出现了什么效果。这个结果完全出乎意料,最终num变成了115或者其他的值,每个系统值会有不同。

  接着来分析下造成这种结果的原因。当sleep时间较短的时候,在线程切换的过程中,之前运行的线程的sleep就已经执行结束了,就会重新参与竞争cpu资源,在切的过程中,之前的线程sleep结束,就有了被切回去的可能,继续执行后面的num = temp_num - 1 所以就会导致这种情况。

 

注意!!这里面的sleep是用来模拟程序中的I/O操作!

  从第二个例子中,我们可以看到一个全局资源被抢占的现象,没有控制多个线程对一个全局资源的访问控制,造成全局资源的损坏(这里的损坏是指得到了我们不想要的结果)使我们无法预测程序最后执行的结果,如果想避免这种问题,就需要用到“互斥锁”。

  “互斥锁”最主要的作用就是,保证在操作共享数据时,共享数据的完整性。

  互斥锁实现的方式,就是为每个共享的资源创建一个Lock对象,当需要访问这个共享资源的时候,调用这个锁的acquire方法来获取锁的对象,资源访问结束后,在调用release方法去解锁。

  我们对上面的程序进行整改,为此我们需要添加一个互斥锁变量t_lock = threading.Lock(),然后在争夺资源的时候之前我们会先抢占这把锁t_lock.acquire(),对资源使用完成之后我们在释放这把锁t_lock.release().

# -*- coding:utf-8 -*-

import threading
import  time

num = 1000
tread_list = []

t_lock = threading.RLock()  #创建一个锁的对象。 或者写成:t_lock = threading.Lock()

def add_num():

    global num,temp_num

    if t_lock.acquire():   #加锁                                使用with: with t_lock:
        temp_num = num                                  temp_num=num
        time.sleep(0.001) #执行sleep,相当于执行IO操作.                 time.sleep(0.001)
        num = temp_num - 1                                num = temp_num -1 
        t_lock.release()  #公共资源访问和操作结束后,解锁。            

for i in range(200):
    t = threading.Thread(target=add_num)
    t.start()
    tread_list.append(t)
for t in tread_list:
    t.join()

print "ending....num = %s" %(num)

最后看下输出结果:ending....num = 800。之前的资源抢占现象得到了解决。

 

当一个线程去调用一个Lock对象的acquire()方法去得到一个锁时,这把锁就会进入一个“locked”锁定的状态,在锁定时,每次只有一个线程可以获得锁,如果有第二个线程试图去获得锁(去访问操作共享资源时),去操作共享的数据时,第二个线程就会进入阻塞状态,直到线程1对共享数据资源操作结束后,调用了这个lock对象的release方法后(此时的锁已经变为“unlocked”状态),线程二才可以去操作共享资源。

 

大概的加锁思路就是这样的:

import threading

R=threading.Lock()  #创建一个锁的对象

R.acquire() #加锁

'''

对公共数据的操作    #执行了对公共数据的操作后

'''

R.release() #解锁

最后补充~

写到这里,可能会有人觉得,互斥锁和join没什么区别!!事实并非如此!

互斥锁可以做到,只有对公共数据进行访问或者操作的时候是串行模式!

如果使用了join,那么两个线程中所有执行的操作都会变为串行模式!!

这两个还是有很大区别的!

 

 

 

 

 

linux会根据线程的优先级、线程的闲置情况、I/O操作、紧要程度等对线程进行切换,进程CPU时间片的分配。更详细的可以参考《Linux内核设计与实现》

 

refer to:

1、https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001431928972981094a382e5584413fa040b46d46cce48e000

2、https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p04_locking_critical_sections.html#id3

3、http://blog.51cto.com/suhaozhi/1924938

posted @ 2018-11-29 00:26  脚本小娃子  阅读(924)  评论(0编辑  收藏  举报