print线程不安全
logging线程安全
threading.local类

线程安全

  • 线程执行一段代码,不会产生不确定的结果,那这段代码就是线程安全的

print线程不安全

# 看如下例子
import threading

def work():
    current = threading.current_thread()
    print(f"{current.name} {current.ident} is working~~~~~") 
    # 字符串是不可变的类型,它可以作为一个整体不可分割输出。end=''不让print输出换行
    # print(f"{current.name} {current.ident} is working~~~~~\n", end=''

for i in range(100):
    t = threading.Thread(target=work, name=f'work-{i}')
    t.start()
--------输出部分如下结果---------------
......
work-63 123145520099328 is working~~~~~
work-64 123145536888832 is working~~~~~work-65 123145520099328 is working~~~~~

work-66 123145520099328 is working~~~~~
work-67 123145520099328 is working~~~~~work-68 123145536888832 is working~~~~~
work-69 123145553678336 is working~~~~~

work-70 123145520099328 is working~~~~~
......

看代码,应该是一行行打印,但是很多字符串打在了一起,为什么?
说明,print函数被打断了,被线程切换打断了。print函数分两步,第一步打印字符串,第二步打印换行符,就在这之间,发生了线程的切换。这说明print函数是线程不安全的
可以避免不打印换行符来“解决”打印问题

logging线程安全

标准库里面的logging模块,日志处理模块,线程安全的,生成环境代码都使用logging

import threading
import logging

fmt_str = "%(asctime)s %(thread)s %(threadName)s %(message)s"
logging.basicConfig(level=logging.INFO, format=fmt_str)

def work():
    current = threading.current_thread()
    logging.info(f"{current} is working~~~~~")

for i in range(100):
    t = threading.Thread(target=work, name=f'work-{i}')
    t.start()

可以看出结果为一行行打印,不会出现print的情况,因此,logging是线程安全的
更多关于logging模块的使用可参看日志处理

threading.local类

例1: 局部变量

import threading
import time
import logging

def work():
    x = 0
    for i in range(10):
        time.sleep(0.01)
        x += 1
    logging.warning(f"{threading.current_thread()},{x}")


for i in range(5):
    t = threading.Thread(target=work)
    t.start()
------------输出结果-------------------
WARNING:root:<Thread(Thread-1, started 123145586499584)>,10
WARNING:root:<Thread(Thread-4, started 123145636868096)>,10
WARNING:root:<Thread(Thread-5, started 123145653657600)>,10
WARNING:root:<Thread(Thread-2, started 123145603289088)>,10
WARNING:root:<Thread(Thread-3, started 123145620078592)>,10

x是局部变量,每一个线程的x是独立的,互不干扰的

例2: 全局变量

import threading
import time
import logging

class A:
    pass

globle_data = A()

def work():
    globle_data.x = 0
    for i in range(10):
        time.sleep(0.01)
        globle_data.x += 1
    logging.warning(f"{threading.current_thread()},{globle_data.x}")

for i in range(5):
    t = threading.Thread(target=work)
    t.start()
-----------输出结果--------------
WARNING:root:<Thread(Thread-2, started 123145374384128)>,46
WARNING:root:<Thread(Thread-4, started 123145407963136)>,47
WARNING:root:<Thread(Thread-5, started 123145424752640)>,48
WARNING:root:<Thread(Thread-1, started 123145357594624)>,49
WARNING:root:<Thread(Thread-3, started 123145391173632)>,50

使用了全局对象,但是线程之间互相干扰,导致了不期望的结果

例3: threading.local

import threading
import time
import logging

global_data = threading.local()

def work():
    global_data.x = 0
    for i in range(10):
        time.sleep(0.01)
        global_data.x += 1
    logging.warning(f"{threading.current_thread()},{global_data.x}")

for i in range(5):
    t = threading.Thread(target=work)
    t.start()

结果显示和使用局部变量的效果一样

threading.local的本质

import threading
import time
import logging

X = 'abc'
global_data = threading.local()
global_data.x = 100

print(global_data, type(global_data), global_data.x)

def work():
    print(X)
    print(global_data)    # 另起一个线程,这里ok
    print(global_data.x)  # 另起一个线程,这里会报错,说明global_data.x不能跨线程
    print('in func work~~~~')

work()
print('~~~~~~~~~~~~~~~')
# 启动一个线程
# threading.Thread(target=work).start()  # AttributeError: '_thread._local' object has no attribute 'x'

可查看threading.local源码,发现threading.local类构建了一个大字典,存放所有线程相关的字典,定义如下:
{ id(Thread) -> (ref(Thread), thread-local dict) }(每一线程实例的id为字典的key,元组为字典的value分别为线程对象引用和每个线程自己的字典)
本质运行时,threading.local实例处在不同的线程中,就从大字典中找到当前线程相关键值对中的字典,覆盖threading.local实例的 __dict__,这样就可以在不同的线程中,安全地使用线程独有的数据,做到了线程间数据隔离,如同本地变量一样安全

参考

  • magedu