【引自selfboot的博客】我们知道,在多线程环境下,每个线程都可以使用其所属进程的全局变量。如果一个线程修改了一个全局变量,它会影响所有其他线程。为了防止多个线程同时修改变量,引入了线程同步机制,通过互斥锁、条件变量或读写锁来控制对全局变量的访问。仅仅使用全局变量不能满足多线程环境的需要。在很多情况下,线程还需要有自己的私有数据,这些数据对其他线程是不可见的。因此,线程中也可以使用局部变量。局部变量只能被线程自身访问,不能被同进程下的其他线程访问。有时候使用局部变量不方便,所以python也提供了ThreadLocal变量,它本身就是全局变量,但是每个线程都可以用它来保存自己的私有数据,其他线程也是不可见的。下图展示了线程中这些变量的存在:线程变量全局VS局部变量先用一个小程序来看下多线程环境下全局变量的同步。importthreadingglobal_num=0defthread_cal():globalglobal_numforiinxrange(1000):global_num+=1#Get10threads,runthemandwaitthemallfinished.threads=[]foriinrange(10):threads.append(threading.Thread(target=thread_cal))threads[i].start()foriinrange(10):threads[i].join()#Valueofglobalvariablecanbeconfused.printglobal_num这里我们创建了10个线程,每个线程对全局变量global_num加11000次(循环加11000次是为了延长单个线程的执行时间,使得线程在执行过程中被中断和切换),当10个线程执行完后,全局变量的值是多少?答案是不确定的。简单来说,global_num+=1不是原子操作,所以执行过程可能会被其他线程打断,导致其他线程读到一个脏值。以两个线程执行+1为例,其中一种可能的执行顺序如下(本例中最大结果为1):这个问题在多线程全局变量同步中使用全局变量时比较常见,而解决办法也很简单,可以使用互斥锁、条件变量或者读写锁。让我们考虑使用互斥量来解决上面代码的问题。只需要在执行+1操作之前加锁,操作完成后释放锁,这样就可以保证操作的原子性。l=threading.Lock()...l.acquire()global_num+=1l.release()在线程中使用局部变量时没有这个问题,因为每个线程的局部变量是不能被其他线程访问的。接下来我们用10个线程对各自的局部变量加11000次,打印每个线程结束时执行的操作总数(每个线程为1000):defshow(num):printthreading.current_thread()。getName(),numdefthread_cal():local_num=0for_inxrange(1000):local_num+=1show(local_num)threads=[]foriinrange(10):threads.append(threading.Thread(target=thread_cal))threads[i].start()可见这里每个线程都有自己的local_num,各个线程之间互不干扰。在Thread-local对象上面的程序中,我们需要将local_num局部变量传递给show函数,并没有错。但是考虑到在实际生产环境中,我们可能会调用很多函数,而每个函数又需要很多局部变量。这时候使用传参的方式会很不友好。为了解决这个问题,一个直观的方法是创建一个全局字典来保存进程ID和进程局部变量的映射关系,运行的线程可以根据自己的ID获取自己拥有的数据。这样,就可以避免在函数调用中传递参数,如下例所示:threading.current_thread()global_data[cur_thread]=0for_inxrange(1000):global_data[cur_thread]+=1show()#Neednolocalvariable.Looksgood....保存一个全局字典,然后以线程标识为key,本地数据对应线程的值作为值,这种做法并不完美。首先,每个函数在需要线程局部数据时,都需要先获取自己的线程ID,比较繁琐。更糟糕的是,线程之间没有真正的数据隔离,因为每个线程都可以读取全局字典,每个线程都可以更改字典的内容。为了更好的解决这个问题,python线程库实现了ThreadLocal变量(很多语言都有类似的实现,比如Java)。ThreadLocal真正实现了线程间的数据隔离,使用时不需要手动获取自己的线程ID,如下例所示:global_data=threading.local()defshow():printthreading.current_thread().getName(),global_data.numdefthread_cal():global_data.num=0for_inxrange(1000):global_data.num+=1show()threads=[]...print"Mainthread:",global_data.__dict__#{}上面例子中的每个线程都可以通过global_data.num得到自己唯一的数据,每个线程读取的global_data是不同的,这样真正做到了线程间的隔离。Python通过本地类实现ThreadLocal变量。代码量小(只有100多行),但理解难度大,涉及大量Python黑魔法。我将在下一篇文章中详细分析。所以ThreadLocal被黑了?不!Python的WSGI工具库werkzeug中有更好的ThreadLocal实现,它甚至支持协程之间的私有数据。实现比较复杂,有机会再分析。深入理解Python中的ThreadLocal变量(中)
