Python Version:3.10.12

如果要处理的任务是相互独立存在,那么使用多线程并发的处理数据会带来一定的效率提升。如果多线程任务之间相互有联系,那么使用多线程就要考虑一下线程安全问题了。

典型场景,多线程任务同时修改一个全局变量counter的值。

import threading

# 共享资源
counter = 0

# 线程函数
def worker(s):
    global counter
    for i in range(1000000):
        counter += 1
    print("{0} Counter value: {1}".format(s, counter))


# 创建两个线程并启动它们
t1 = threading.Thread(target=worker, args=('t1',))
t2 = threading.Thread(target=worker, args=('t2',))
t1.start()
t2.start()
t1.join()
t2.join()

理想的运行结果应该t1线程把couter的值累加到1000000,然后t2线程在此基础之上继续将总值累加到2000000。但是实际运行结果与理想状态有差异。造成差异的原因就是t1和t2会同时对couter进行修改,导致counter值产生混乱,因此运算结果不准确。

t1 Counter value: 1824819
t2 Counter value: 2000000


线程锁的作用就是在线程运行过程中对couter变量进行锁定,同一时间只有一个线程可以修改变量。

import threading

# 创建一个Lock对象
lock = threading.Lock()

# 共享资源
counter = 0

# 线程函数
def worker(s):
    global counter
    print('{}线程启动'.format(s))
    lock.acquire()  # 加锁,禁止其他线程执行相关逻辑

    print('{}线程进入运算逻辑'.format(s))
    for i in range(1000000):
        counter += 1

    lock.release() # 释放锁
    print("{0} Counter value: {1}".format(s, counter))
    print('{0}线程退出'.format(s))


# 创建两个线程并启动它们
t1 = threading.Thread(target=worker, args=('t1',))
t2 = threading.Thread(target=worker, args=('t2',))
t1.start()
t2.start()
t1.join()  # 先让两个线程启动在hold住主线程
t2.join()

运行结果与设想一致了。由此可见线程锁是锁住线程中的某一段代码的运行。

t1线程启动  
t2线程启动  # 同时启动了2个线程
t1线程进入运算逻辑  # t1拿到了锁,优先进行运算逻辑 
t1 Counter value: 1000000
t1线程退出
t2线程进入运算逻辑 # t2线程虽然很早就启动了,但要等t1释放锁以后才能进入运算逻辑
t2 Counter value: 2000000
t2线程退出


通过join方式实现类似的效果。

import threading

# 创建一个Lock对象
lock = threading.Lock()

# 共享资源
counter = 0

# 线程函数
def worker(s):
    global counter
    print('{}线程启动'.format(s))
    print('{}线程进入运算逻辑'.format(s))
    for i in range(1000000):
        counter += 1
    print("{0} Counter value: {1}".format(s, counter))
    print('{0}线程退出'.format(s))


# 创建两个线程并启动它们
t1 = threading.Thread(target=worker, args=('t1',))
t2 = threading.Thread(target=worker, args=('t2',))
t1.start()
t1.join() #join 紧跟在start()后面
t2.start()
t2.join()

从运行结果可以确认,如前文所述。join的通过控制主线程的执行流程,限制新线程的启动。

t1线程启动 # 第一个线程启动并行
t1线程进入运算逻辑
t1 Counter value: 1000000
t1线程退出
t2线程启动 # 第二个线程要等待上一个线程运行完毕,才开始启动并行
t2线程进入运算逻辑
t2 Counter value: 2000000
t2线程退出