设为首页收藏本站

安徽论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 63167|回复: 0

Python线程安全,你真正了解了么?

[复制链接]

63

主题

511

回帖

965

积分

高级会员

Rank: 4

积分
965
发表于 2022-3-26 10:27:01 | 显示全部楼层 |阅读模式
网站内容均来自网络,本站只提供信息平台,如有侵权请联系删除,谢谢!
文章目录



网上经常看到一些关于线程安全的错误观点诸如: Python list、set 等非线程安全,而消息队列Queue线程安全,这是非常危险的大错特错的认识!!!
在Python中,线程安全是针对操作的原子性的,与对象无关
** 本文将从3个方向试图阐述这个问题**
1.什么是线程安全?



  • 首先,线程安全不是针对对象的,所以不能说Queue是线程安全的,而另一个对象(如list)是非线程安全的
  • 我们以一个demo解释下什么是线程安全
    1. import threadingnumber = 0def add():    global number    for i in range(1000000):        number += 1thread_1 = threading.Thread(target=add)thread_2 = threading.Thread(target=add)thread_1.start()thread_2.start()thread_1.join()thread_2.join()print(number)
    复制代码
    按照推论应该是累加操作,结果应该是2000000才对,但是实际运行下来多次可以观察到每次结果不同,而且都小于 200000, 这就是线程非安全此处体现为全局变量数据污染
2.GIL锁



  • cPython 解释器中存在大大的全局解释器锁,保证同一时间只有一个线程在运行
  • 由此产生个问题: 既然同一时刻只能有一个线程执行,那么为什么还会出现线程非安全呢
    因为线程执行过程中的遇到io操作会,或者连续执行一定数量指令后,会让出cpu资源,让其他线程使用,即线程调度,以上面的代码执行流程为例: 当线程1 执行全局变量+1 的时候线程2此时是阻塞等待状态,cpu调度到线程2的时候才执行线程2中的全局变量+1,不像Go中执行多线程赋值全局变量操作时候是真正并行的,存在执行时间先后的问题导致的全局变量混乱即线程非安全,这么想来,python GIL锁的存在导致程序线程之间一来一回交替执行永远不会出错才对,哈哈,那为什么还会混乱呢?这是因为我们默认number += 1是个原子操作了,即操作的原子性
3.原子操作



  • 原子操作(atomic operation),指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会切换到其他线程,它有点类似数据库中的 事务。
  • 我们列举一些常见的原子操作
    1. 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()
    复制代码
  • 常见的非原子操作
    1. i += 1L.append(L[-1])L[i] = L[j]D[x] = D[x] + 1
    复制代码
  • 如何确定原子操作?
    1. from dis import disdef func():    L = []    L.append(1+1)dis(func)
    复制代码
    当一行代码被分成多条字节码指令的时候,就代表在线程线程切换时,有可能只执行了一条字节码指令,此时若这行代码里有被多个线程共享的变量或资源时,并且拆分的多条指令里有对于这个共享变量的写操作,就会发生数据的冲突,导致数据的不准确。
  • 还是以上面的demo为例:
    当线程1执行 到number+=1 时,是分为两个字节码进行的操作,即 1. number+1 2. number赋值,那我们可以想象, 当所有线程同时运行,我们假设线程1先执行,此时线程1执行了一定数量的指令后,假设执行到 number=6后的 number+1(7) 这个原子操作处,此时线程2开始运行拿到的 number=6 那么 线程2接着执行累加到number=10,此时 交替到线程1 执行 number=7,多次循环运行下就会造成这个全局变量脏数据即线程非安全
  • 如何解决呢?
    即强制同步操作,全局变量非原子操作的地方实行串行同步操作,即,强制原子化,把我们会产生线程安全问题的关键代码加锁,此时锁内的代码即为一个原子操作,再加上GIL锁的影响此时线程是绝对安全的。
  • 我们对上述代码作些修改
    1. import threadingmutex = threading.Lock()number = 0def add():    global number    for i in range(1000000):            with mutex:                number += 1thread_1 = threading.Thread(target=add)thread_2 = threading.Thread(target=add)thread_1.start()thread_2.start()thread_1.join()thread_2.join()print(number)
    复制代码
    此时线程绝对安全,最终结果为2000000
    Python list、set 等非线程安全,而消息队列Queue线程安全,因为list的append操作也有原子操作,此时不管怎么追加元素都不会造成数据污染,如果是 非原子操作的append如L.append(a+b) 此时也是安全的,因为 append内的原子操作不是针对共享变量的的,或者说列表的写操作安全,内容的安全性在其他地方加锁保证,总而言之一句话,哪里有冲突解决哪里
    结论: 线程非安全是针对操作的不是针对某一类对象而言,真正引起线程非安全的是 写操作,而非读操作,确定写操作安全性才能确保线程安全。最后一句,哪里有冲突解决哪里!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
免责声明
1. 本论坛所提供的信息均来自网络,本网站只提供平台服务,所有账号发表的言论与本网站无关。
2. 其他单位或个人在使用、转载或引用本文时,必须事先获得该帖子作者和本人的同意。
3. 本帖部分内容转载自其他媒体,但并不代表本人赞同其观点和对其真实性负责。
4. 如有侵权,请立即联系,本网站将及时删除相关内容。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表