当前位置: 首页 > 科技观察

说说Python中的线程安全

时间:2023-03-21 15:46:16 科技观察

在并发编程中,如果多个线程访问同一个资源,我们需要保证访问时不会发生冲突,数据修改时不会出错。这就是我们常说的线程安全。在什么情况下访问数据是安全的?什么情况下访问数据是不安全的?你怎么知道你的代码是否是线程安全的?您如何访问数据以确保数据安全?本文将一一为您解答问题。1、什么是线程不安全?要弄清楚什么是线程安全,首先要了解什么是线程不安全。例如下面这段代码启动两个线程,将全局变量number递增10万次,每次递增1。fromthreadingimportThread,Locknumber=0deftarget():globalnumberfor_inrange(1000000):number+=1thread_01=Thread(targettarget=target)thread_02=Thread(targettarget=target)thread_01.start()thread_02.start()thread_01.join()thread_02.join()print(number)正常我们预期的输出结果,一个线程递增100万,两个线程递增200万,输出必须是2,000,000。但是事实并不是你想的那样,不管你运行多少次,每次输出的结果都不一样,而且这些输出结果有一个特点,就是都小于200万。下面是执行3次的结果145978213798911432921这种现象就是线程不安全。根本原因是我们的操作数+=1不是原子操作,导致线程不安全。2.什么是原子操作?原子操作是指不会被线程调度机制打断的操作。这个操作一旦开始,就会一直运行到结束,中间不会切换到其他线程。它有点类似于数据库中的事务。在Python的官方文档中列出了一些常见的原子操作L.append(x)L1.extend(L2)x=L[i]x=L.pop()L1[i:j]=L2L.sort()x=yx.field=yD[x]=yD1.update(D2)D.keys()以及后面的不是原子操作ii=i+1L.append(L[-1])L[i]=L[j]D[x]=D[x]+1和上面一样,我用的是自增操作number+=1,其实相当于number=number+1,可以看到这个可以拆分成多个步骤(首先读取、添加,然后分配)不是原子操作。这样,当多个线程同时读取时,有可能读取到同一个数字值,读取了两次,却只加了一次,最终自增的数量小于预期。当我们还不能确定我们的代码是否是原子的时,我们可以尝试通过dis模块中的dis函数来检查。当我们执行这段代码时,我们可以看到代码行号+=1,由两个字节组成的代码实现。BINARY_ADD:将两个值相加STORE_GLOBAL:将相加的值重新赋值每条字节码指令都是一个整体,不可分割,其达到的效果就是我们所说的原子操作。当一行代码被分成多条字节码指令时,意味着线程切换时可能只有一条字节码指令被执行。这时候,如果这行代码中有多个线程共享的变量或资源时,而拆分指令中有对这个共享变量的写操作,就会发生数据冲突,导致数据不准确。为了对比,我们试试上面列出的其中一个原子操作,看看是不是真的是官网说的原子操作。这里我以字典的更新操作为例。代码和执行过程如截图所示。从截图可以看出,虽然info.update(new)也分为几个操作:LOAD_GLOBAL:加载全局变量LOAD_ATTR:加载属性,获取更新方法LOAD_FAST:加载新变量CALL_FUNCTION:调用函数POP_TOP:执行更新操作但是我们要知道,真正会导致数据冲突的不是读操作,而是写操作。上面这么多字节码指令,只有一个写操作(POP_TOP),所以字典的更新方法是一个原子操作。3、实现人工原子操作在多线程下,我们不能保证我们的代码是原子的,所以如何让我们的代码“原子化”是一件很重要的事情。方法也很简单,就是当你访问多个线程共享的资源时,加锁就可以达到类似原子操作的效果。如果一个代码没有被执行,那么它必须被执行才能接受线程调度。因此,我们使用加锁的方法对示例1进行一些修改,使其成为“原子的”。fromthreadingimportThread,Locknumber=0lock=Lock()deftarget():globalnumberfor_inrange(1000000):withlock:number+=1thread_01=Thread(targettarget=target)thread_02=Thread(targettarget=target)thread_01.start()thread_02.start()thread_01。join()thread_02.join()print(number)此时无论执行多少次,输出都是2000000。4.Queue为什么是线程安全的?Python的线程模块中主要有三种消息通信机制:EventConditionQueue是使用最多的Queue,我们都知道它是线程安全的。当我们写入和提取它时,它不会被中断而导致错误,这就是为什么我们在使用队列时不需要额外的锁。他是怎么做到的呢?根本原因在于Queue实现了锁原语,所以他可以像第3节那样实现人为的原子操作。即原语的执行必须是连续的,在执行过程中不允许中断。