1、共享全局变量资源竞争

一个线程写入,一个线程读取,没问题,如果两个线程都写入呢?显然是会存在问题的,会使得写入的值达不到想要的结果;
Python中的dis模块中的dis方法可以查看一句Python代码的cpu运行轨迹,也就是cpu指令,
如果只是读取数据时,如读取一个函数,此时数据是安全的,因为没有涉及任何修改,
当改数据时,可能会涉及数据不安全,如当多个线程同时修改一个数据时,Python中的一句代码对应了多条cpu指令,假设有4条指令,当执行完第二条时,cpu时间片轮转了,此时数据可能发生错误。
所以任何 += -= *- 都是数据不安全的

只有load读数据,安全

  1. import dis
  2. def add_num():
  3. num = 1
  4. num += 1
  5. dis.dis(add_num)

运行结果:

 42           0 LOAD_CONST               1 (1)
              2 STORE_FAST               0 (num)

 43           4 LOAD_FAST                0 (num)
              6 LOAD_CONST               1 (1)
              8 INPLACE_ADD
             10 STORE_FAST               0 (num)
             12 LOAD_CONST               0 (None)
             14 RETURN_VALUE

一个简单的赋值语句,对应的都是两个操作指令,一个是读取常量,一个是快速存储;如果再涉及到运算,有还会多一个运算的指令,而在多线程中,这些指令极有可能在某一条就中断了(cpu时间片轮转了),下次接着回来继续运行时,可能另一个线程中也完成了写的操作,这样就会导致两边写的数据最终结果是不一样的

import threading

num = 0


def demo1():
    global num
    for i in range(1000000):
        num += 1
    print(num)


def demo2():
    global num
    for i in range(1000000):
        num += 1
    print(num)


def main():
    t1 = threading.Thread(target=demo1, name='domo1')
    t2 = threading.Thread(target=demo2, name='domo2')
    t1.start()
    t2.start()


if __name__ == '__main__':
    main()

运行结果:

1256870
1322058

如果是一个线程写完,另一个线程再写入,那么,结果应该是1000000,2000000,而现在的结果显然不是,原因就是线程在写入的过程中被中断了,去执行另一个线程,那么就有可能在存值的过程中被中断,这样导致的就是最终的结果少加了;

2、互斥锁(mutex)

当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制;

某个线程要更改共享数据时,先将其锁定,此时资源的状态为”锁定”,其他线程不能改变,只到该线程释放资源,将资源的状态变成”非锁定”,其他的线程才能再次锁定该资源互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

创建锁
mutex = threading.Lock()

加锁
mutex.acquire()

解锁
mutex.release()
import threading

num = 0


def demo1():
    global num
    mutex.acquire()
    for i in range(2000000):
        num += 1
    mutex.release()
    print(num)


def demo2():
    global num
    mutex.acquire()
    for i in range(2000000):
        num += 1
    mutex.release()
    print(2, num)


def main():
    t1 = threading.Thread(target=demo1, name='domo1')
    t2 = threading.Thread(target=demo2, name='domo2')
    t1.start()
    t2.start()


if __name__ == '__main__':
    mutex = threading.Lock()
    main()

注意:

  • 加锁和解锁必须向对应,加多少个锁,解多少个锁,并且同一Lock()只能acquire一次,下一次acquire必须release后才能,不然会造成死锁;
  • 使用RLock()方法,可以实现连着加锁,和连着解锁;
  • acquire()可以设置加锁时间timeout;
  • 在实现线程安全的控制中,比较常用的是 RLock;

3、死锁

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        # 对mutexA上锁
        mutexA.acquire()

        # mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
        print(self.name+'----do1---up----')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
        mutexB.acquire()
        print(self.name+'----do1---down----')
        mutexB.release()

        # 对mutexA解锁
        mutexA.release()

class MyThread2(threading.Thread):
    def run(self):
        # 对mutexB上锁
        mutexB.acquire()

        # mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
        print(self.name+'----do2---up----')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
        mutexA.acquire()
        print(self.name+'----do2---down----')
        mutexA.release()

        # 对mutexB解锁
        mutexB.release()

mutexA = threading.Lock()
mutexB = threading.Lock()

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

3.1 避免死锁

  • 程序设计时要尽量避免
  • 添加超时时间等

4、Condition

互斥锁Lock和RLock只能提供简单的加锁和释放锁等功能,它们的主要作用是在多线程访问共享数据时,保护共享数据,保证数据和关键代码的完整性。在此基础上,Python提供了Condition类,Condition类不仅自身依赖于Lock和RLock,即具有它们的阻塞特性,此外还提供了一些有利于线程通信,以及解决复杂线程同步问题的方法,它也被称作条件变量

相关方法:

  • acquire():加锁
  • release():解锁
  • wait(timeout):将线程挂起,直到收到一个notify通知或者等待时间超出timeout才会被唤醒,自动解锁;wait()必须在先Lock的前提下调用,否则会引起RuntimeError错误。
  • notify(n=1):唤醒在Condition的waiting池中的n(参数n可设置,默认为1)个正在等待的线程并通知它,收通知的线程将自动调用acquire()方法尝试加锁;如果waiting池中有多个线程,随机选择n个唤醒;必须在已获得Lock的前提下调用,否则将引发错误。
  • notify_all():唤醒waiting池中的等待的所有线程并通知它们。
  • 可以用上下文管理器(with)的方法来使用cond,使用with,则会自动加解锁,无需acquire和release。

5、Queue线程

在线程中,访问一些全局变量,加锁是一个经常的过程。如果你是想把一些数据存储到某个队列中,那么Python内置了一个线程安全的模块叫做queue模块。Python中的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列Queue,LIFO(后入先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。

初始化Queue(maxsize):创建一个先进先出的队列,maxsize:队列的长度,不定义理论上无限大(由自身计算机性能决定)。
qsize():返回队列的大小。
empty():判断队列是否为空,返回布尔值。
full():判断队列是否满了,返回布尔值。
get():从队列中取一个数据,队列空则阻塞,可设置阻塞时间timeout。
put():将一个数据放到队列中,队列满则阻塞,可设置阻塞时间timeout。
get_nowait():从队列中取一个数据,队列空直接抛出异常。
put_nowait():将一个数据放到队列中,队列满直接抛出异常。

线程同步

实现
天猫精灵:小爱同学

小爱同学:在

天猫精灵:现在几点了?

小爱同学:你猜猜现在几点了
注意小爱和天猫的启动顺序,先wait()的先启动;如果先启动notify的线程,会导致notify没有唤醒的对象,就进入了wait的挂起状态,然后就两个线程都是挂起,程序阻塞了;

import threading
from threading import Condition


class Xiaoai(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        cond.acquire()
        cond.wait()
        print("{}:在!".format(self.name))
        cond.notify()

        cond.wait()
        print("{}:你猜猜现在几点了。".format(self.name))

        cond.release()


class Tianmao(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        # cond.acquire()
        with cond:
            print("{}:小爱同学。".format(self.name))
            cond.notify()
            cond.wait()
            print("{}:现在几点了?".format(self.name))
            cond.notify()
            # cond.release()


if __name__ == '__main__':
    cond = Condition()
    xiaoai = Xiaoai("小爱")
    tianmao = Tianmao("天猫")

    xiaoai.start()
    tianmao.start()