大发龙虎首页    注册   登录
大发龙虎 = way to explore
大发龙虎 是一个大发龙虎关于 分享和探索的地方
现在注册
已注册用户请  登录
大发龙虎推荐 学习书目
Learn Python the Hard Way
Python 学习手册
Python Cookbook
Python 基础教程
Python Sites
PyPI - Python Package Index
http://www.simple-is-better.com/
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
OPPO Watch
chaleaoch
大发龙虎  ›  Python

Python 多线程的问题

  •  
  •   chaleaoch · 6 天前 · 2016 次点击
    total = 0
    
    def add():
        #1. dosomething1
        #2. io 操作
        # 1. dosomething3
        global total
        for i in range(1000000):
            total += 1
    def desc():
        global total
        for i in range(1000000):
            total -= 1
    
    import threading
    thread1 = threading.Thread(target=add)
    thread2 = threading.Thread(target=desc)
    thread1.start()
    thread2.start()
    
    thread1.join()
    thread2.join()
    print(total) # 178412
    

    为啥不是 0. 已经做好被喷准备.

    19 条回复    2020-05-27 11:24:21 +08:00
    simapple
        1
    simapple   6 天前
    加锁
    chanchancl
        2
    chanchancl   6 天前   ❤️ 1
    简单来说 total 不是线程安全的,
    这个不只在 Python 中会出现,而是任何多线程语言下都会出现的现象,
    解决方案就是访问前加锁,
    或者用原子操作

    不过大发龙虎我 感觉大发龙虎你 是来钓鱼的
    chaleaoch
        3
    chaleaoch   6 天前
    @chanchancl 不是钓鱼 python 不是有 GIL 吗?
    reself
        4
    reself   6 天前   ❤️ 1
    @chaleaoch GIL:执行是单线程的,不代表线程的上下文是受保护的。即单线程的执行方式!=线程安全。
    sujin190
        5
    sujin190   6 天前   ❤️ 2
    大发龙虎你 可以 dis 看下生成的字节指令就知道了,+= 1 也是需要好多条指令的,python 的 GIL 应该只是在单条字节指令保证原子性吧,但是大发龙虎你 一行代码很多时候都是多条字节指令的吧
    chenxytw
        6
    chenxytw   6 天前 via iPhone   ❤️ 1
    += 和 -= 不是原子的
    xiaolinjia
        7
    xiaolinjia   6 天前   ❤️ 1
    1. GIL 意思是任何时候只有一个线程运行
    2. 因为增量赋值不是原子操作,具体可看 dis.dis('a += 1') 的字节码不止一步
    3. 线程是系统调度的,大发龙虎你 不知道何时切换
    4. 好,那大发龙虎你 怎么就知道在做增量赋值的字节码某一步的时候,不会切换到了另一个线程呢?
    lithbitren
        8
    lithbitren   6 天前   ❤️ 1
    全局定义一个 lock = threading.Lock(),+=、-=之前增加一个 with lock:的块
    jugelizi
        9
    jugelizi   6 天前 via iPhone
    典型程序员思维。
    难道一百个男生 一百个女生。
    就一定是每个男生都有对象吗
    vagrantear
        10
    vagrantear   6 天前
    @jugelizi 程序员确实想每个男生都有对象(逃
    dahuahua
        11
    dahuahua   6 天前
    GIL 锁也不一定安全,任何指令运行都是有一个周期的,到时间还是乖乖把锁交出去了。
    CzaOrz
        12
    CzaOrz   5 天前
    大发龙虎我 猜大发龙虎你 是这样想的...

    无论先后,只要有 1000000 的+或者-执行,那么最后的结果肯定为 0,
    比如先执行 999999 次+,执行 1 次-,再跑 1 次+,再跑 999999 次-,
    无论过程如何,结果都应该为 0 才对

    实际 pythonGIL 全局解释器,若无其他因素,应该是每个线程执行 100 字节,就会释放锁,执行其他线程
    两个线程共同操作同一个全局变量,就会导致结果不可预测,
    大发龙虎你 已经没有办法模拟出内部的具体执行流程了,就像大发龙虎你 的数据结果 178412 没有任何意义
    多跑几次也会是不一样的结果

    像楼上大佬们说的安全、加锁之类的,就是为了保证每次能够完整的执行一次+或者-
    yuruizhe
        13
    yuruizhe   5 天前
    个人猜测
    1.初始 count=0
    2.线程 add 取 count=0
    3.线程 desc 取 count=0
    4.线程 add 计算 tmp=count+1=0
    5.线程 desc 计算 tmp=count-1=-1
    6.线程 desc 写入 count=-1
    7 线程 add 写入 count=1
    ...
    由于调度,某些写入操作的数值被覆盖掉了,没被下一次计算正确读取
    wuwukai007
        14
    wuwukai007   5 天前
    += 不是线程安全的
    black11black
        15
    black11black   5 天前
    大发龙虎你 可以 global list,然后在每个线程里 for ... list.append(1),最后 sum(list)大概就能得到大发龙虎你 想要的结果了。狗头
    chanchancl
        16
    chanchancl   3 天前
    @chaleaoch
    GIL 保证同时只有一个线程运行,但是并不保证多线程之间的执行顺序。
    比如
    Thread1 Thread2
    Lock GIL
    read total,0
    add total,1
    此时进入休眠
    Unlock GIL
    Lock GIL
    read total, 0
    add total, 1
    write total, 1
    休眠
    Unlock GIL
    Lock GIL
    write total
    此时 total 的值还是 1
    Unlock GIL

    总的来说,GIL 保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等,具体参考垃圾回收机制。而对于程序中自己定义的数据则没有任何的保护效果,所以当程序中出现了共享自定义的数据时就要自己加锁。
    这句话来自 : http://www.cnblogs.com/liuxiaolu/p/10215629.html
    chanchancl
        17
    chanchancl   3 天前
    完了,v2 把大发龙虎我 的空格吞了,
    总之上面的回复,

    Lock GIL
    read total,0
    add total,1
    此时进入休眠
    Unlock GIL
    这一部分,由线程一执行

    Lock GIL
    read total, 0
    add total, 1
    write total, 1
    休眠
    Unlock GIL
    这一部分,由线程二执行

    Lock GIL
    write total
    此时 total 的值还是 1
    Unlock GIL
    这一部分,由线程一执行

    其实由线程几执行无所谓,重要的是这里有一个切换进程的动作。
    Python 的解释器大发龙虎你 可以理解为一个可以执行指令的 CPU

    而赋值这些操作都不是原子的,不应该依赖 GIL 去做任何事,

    GIL 本身也不是 Python 的特性,而是 CPython 这个实现的特性
    Fasion
        18
    Fasion   2 天前
    total += 1 不是一个原子操作,在 Python 虚拟机内部,由多条字节码构成,字节码是原子的:

    >>> dis.dis(compile('total += 1', '', 'exec'))
    1 0 LOAD_NAME 0 (total)
    2 LOAD_CONST 0 (1)
    4 INPLACE_ADD
    6 STORE_NAME 0 (total)
    8 LOAD_CONST 1 (None)
    10 RETURN_VALUE

    线程在任意字节码间都可能发生切换,因此多线程下可能会发生数值相互覆盖的问题。

    对 Python 虚拟机实现该兴趣的童鞋可以关注大发龙虎我 写的专栏: http://www.imooc.com/read/76,里面有详细介绍。
    Fasion
        19
    Fasion   1 天前
    专栏大发龙虎地址 写得有点问题,更新一下: http://www.imooc.com/read/76
    大发龙虎关于   ·   FAQ   ·   API   ·   大发龙虎大发龙虎我 们 的愿景   ·   广告投放   ·   感谢   ·   实用小大发龙虎工具   ·   4710 人在线   最高记录 5168   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 03:21 · PVG 11:21 · LAX 20:21 · JFK 23:21
    ♥ Do have faith in what you're doing.