python——threading与原子操作

文章目录

    • threading
        • 实验一
        • 实验二
        • 实验三
        • threading.Lock()

前些日子在采用python threading模块去开了一些线程,每个线程都包含一些跟同一个远程对象的socket通信,然而却发现socket得到的结果出错了,而且A线程期望得到的结果让B线程接收了,而B线程期望得到的结果让A接收了。

threading

由于全局锁GIL的存在,python的多线程一直名不副实,换句话说,python并不能真正地并行执行threading模块。在同一个CPU上开启threading的多线程,实际上同一时刻时刻只能执行一个线程。而不同的线程实际上被分配了CPU时间片交叉执行。

这种方式对于计算密集型的程序毫无益处,不仅不能利用CPU多核的特点减少运行时间,反而会因为“线程”上下文的切换导致总体执行时间上升。但是对于IO密集型的程序反倒能带来很多可观的收益。nodejs就是这样的例子。

如果两个这样的python线程同时操作了一个共享变量,就可能会带来问题。

实验一

请看下面的例子:

wrong = 0
correct = 0
num = 0

count = 10000
while count > 0 :
	count -= 1
	num = 1
	if num == 2:
		wrong += 1
	else:
		correct += 1

我们执行了一个循环,每次都对num赋值1,然后立马读取num,如果不等于1,wrong计数加1,否则correct计数加1。显然,无论循环多少次,wrong的计数永远都是0。除非CPU出了问题。

实验二

现在,我们用两个python线程去做这样的事情,并分别对两个线程的结果的正确与否做单独计数:

num = 0

awrong = 0
acorrect = 0
bwrong = 0
bcorrect = 0

class A(threading.Thread):
    def run(self):
        global num
        global awrong
        global acorrect

        count = 1000
        while count > 0:
            count -= 1
            num = 1
            if num != 1:
                awrong+=1
            else:
                acorrect += 1

class B(threading.Thread):
    def run(self):
        global num
        global bwrong
        global bcorrect

        count = 1000
        while count > 0:
            count -= 1
            num = 2
            if num != 2:
                bwrong += 1
            else:
                bcorrect += 1

if __name__ == '__main__':
    a = A()
    b = B()
    a.start()
    b.start()

    a.join()
    b.join()

    print(acorrect)
    print(bcorrect)
    print(awrong)
    print(bwrong)

当count比较小的时候,我执行了很多次,wrong计数都为0。
在这里插入图片描述
有两种解释:

  • 概率太小,实验次数依然不够多
  • CPU为线程分配单次的时间片足够一个线程执行完这1000次循环,如果没有上下文的切换,自然就不会产生错误。

我们将count设为 10,000,000(一千万),结果就有所不同了:
在这里插入图片描述
在这一千万次读写中,两个线程各有50多次读预期错误,概率为0.000005%。

因为对num的读写不是一个原子操作。读预期错误发生在线程A对num进行了写,但是却在这时候用完了CPU的时间片,然后切换到了线程B,于此同时B线程在上一次切换的时候也发生了这样的事情。所以轮到B享用时间片的时候,线程B第一件事情就是要读取num,结果读到了一个错误的值。

可见读预期错误发生的条件很苛刻,它要求连续两次的时间片切换时机都发生在当前线程对num的写和读之间:

  • B首先发生了对num写,并在读之前发生了时间片切换
  • A在享用完时间片后,也是对num完成了写,但是未读

实验三

我们还可以在两个线程的写和读之间各插入一次0.01秒的休眠。由于一个CPU时间片对于0.01秒又显得太短,所以线程A完成写之后,一定会切换到线程B完成对num的写。此时,线程A醒来时,几乎不可能读到正确的值,因为

  • 如果A先B醒来,读到的就是B写过值
  • 如果A后B醒来,无论B醒来之后有没有写,A读到的还是B写过的值

而线程B如果想要读到正确的值,则必须:

  • 在A之前醒来
  • 或者在A后醒来,但A读完之后,还没有完成写,就发生了时间片切换

然而,我们只在写到读之间插入了休眠,而读到写几乎是一瞬间,发生时间片切换的概率很低。综上所述,加入了休眠之后,想要得到正确的结果的概率反而变得很低,至于有多低,取决于CPU为线程分配的时间片长度与休眠时间
的差距,差距越大,得到正确结果的概率越低。

下面是循环1000次,休眠0.01s的结果。
在这里插入图片描述
下面是循环1000次,休眠0.1秒的结果。
在这里插入图片描述

threading.Lock()

想要在写之后立马完成读,只需要在写之前加锁,等读完了再释放锁即可。

lock = threading.Lock()

while True:
	lock.acquire()
    num = 2
    if num != 2:
         bwrong += 1
    else:
         bcorrect += 1
    lock.release()

为了防止获取锁之后未释放锁之前程序因为发生错误或者异常导致没有锁没有被释放掉,而后续的代码由于获取不到锁,程序假死的情况发生,可以用上下文管理的方式来使用锁。

lock = threading.Lock()

while True:
	with lock:
		num = 2
		if num != 2:
			bwrong += 1
		else:
			bcorrect += 1
		

你可能感兴趣的:(python)