Python:多线程threading模块

时间:2024-11-08 12:04:20

目录

  • Thread对象
  • Lock对象
  • local对象

Thread对象:

多任务可以由多进程完成,也可以由一个进程内的多线程完成。进程是由至少1个线程组成的。

threading模块在较低级的模块 _thread 基础上建立较高级的线程接口。参见: queue 模块。

例子:

import time, threading

def loop():
print('thread %s is running...' % threading.current_thread().name)
time.sleep(2)
print('Main thread is %s' % threading.main_thread().name)
print('thread %s ended.' % threading.current_thread().name) print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended' % threading.current_thread().name)

解释:

threading.current_thread()函数

返回当前线程的实例Thread对象。

threading.main_thread()函数

返回主Thread对象。一般是Python解释器开始时创建的线程。

class threading.Thread(group=Nonetarget=Nonename=Noneargs=()kwargs={}*daemon=None)

这是一个构造器,创造一个thread对象。

  • group用于扩展
  • target指向一个可调用对象,这里是一个函数对象。target会被底层的run()方法引用。
  • args是一个tuple参数,给target使用。
  • kwargs是一个dict, 给target使用。

Thread().is_alive()

返回bol值,判断当前线程是否是存活的。

Thread().name

返回线程对象的名字。

Lock锁对象

多线程和多进程最大的不同:

多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响。

多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

例子:

import time, threading

balance = 0
# lock = threading.Lock() def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n def run_thread(n):
for i in range(1000000):
# lock.acquire()
change_it(n)
# lock.release() t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

结果应当是0,但是未必是0。

原因:

多线程是并行的,即线程是交替运行的。

balance = balance + n这条语句在程序中是分两部分执行:

x = balance + n  #先计算,存入一个临时变量
balance = x #然后将临时变量赋值给balance

t1, t2是交替运行的,不是下面的运行方式:

初始值 balance = 0

t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0 t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2 # balance = 0 结果 balance = 0

而是,交替运行:

初始值 balance = 0

t1: x1 = balance + 5  # x1 = 0 + 5 = 5

t2: x2 = balance + 8  # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8 t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0 t2: x2 = balance - 8 # x2 = 0 - 8 = -8
t2: balance = x2 # balance = -8 结果 balance = -8

所以需要一个锁定机制,保证当某个线程操作进程内的一个共享变量,其他线程就不能操作。这就是Lock对象的作用。

锁对象

threading.Lock()方法

返回一个锁对象。即创建一把锁。

Lock().acquire() 上锁,Lock().release()  开锁。

就好像在家里(一个进程),多个家人(代表多个线程),你上卫生间,反锁门。其他人(其他线程)就用不了卫生间,只能等你出来再用。

先执行acquire()的线程会获得使用权,使用完成后用release()结束。其他线程在此期间会等待。

缺点:

1 。使用了锁,就相当于多线程变成单线程,效率下降。

2 。由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。

多核cup

如果你不幸拥有一个多核CPU,你肯定在想,多核应该可以同时执行多个线程。

如果写一个死循环的话,会出现什么情况呢?

打开Mac OS X的Activity Monitor,或者Windows的Task Manager,都可以监控某个进程的CPU使用率。

写一个死循环:

import threading, multiprocessing, os

print("%s" % os.getpid())

def loop():
x = 0
while True:
x = x ^ 1 for i in range(multiprocessing.cpu_count()):
t = threading.Thread(target=loop)
t.start()

Python:多线程threading模块

但实际上,只占用了一个核。如果全占应该是500%。

这是因为Python在解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。

小结

多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。

Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。


ThreadLocal

线程本地数据是特定线程的数据。管理线程本地数据,只需要创建一个 local (或者一个子类型)的实例并在实例中储存属性。

class threading.local

思路:1问题->2解决办法->3优化解决办法

1.问题

在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。

但是局部变量也有问题,就是在函数调用的时候,必须要层层传递,很麻烦的!

def process_student(name):
std = Student(name)
# std是局部变量,但是每个函数都要用它,因此必须传进去:
do_task_1(std)
do_task_2(std) def do_task_1(std):
do_subtask_1(std)
do_subtask_2(std) def do_task_2(std):
do_subtask_2(std)
do_subtask_2(std)

如果std作为全局变量?

std = Student(name)

def process_student():
my_std = std
do_task_1()
do_task_2() def do_task_1():
my_std = std def do_task_2():
my_std = std

也不行。因为要多线程工作,每个线程要处理不同的Student对象,std不能被所有的线程共享。

2.解决办法

使用全局dict来存放所有的Student对象,然后以thread自身作为key获得线程对应的Student对象。

这样可以!

import threading

class Student(object):
def __init__(self, name):
self.name = name # 创建全局dict:
g_dict = {} def process_student():
# 获取当前线程关联的student:
std = g_dict[threading.current_thread()]
print('Hello, %s (in %s)' % (std.name, threading.current_thread().name)) def process_thread(name):
# 通过线程对象作为key,来绑定student到g_dict内:
g_dict[threading.current_thread()] = Student(name)
process_student() t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

问题解决,但这样代码显得不优雅。

3.优化解决办法

使用模块threading自带的class threading.local

它代表线程本地数据的类。

import threading

class Student(object):
def __init__(self, name):
self.name = name # 创建全局ThreadLocal对象:
local_school = threading.local() def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std.name, threading.current_thread().name)) def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = Student(name)
process_student() t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

其实就可以把local_school看成是全局变量。但每个属性都是线程的局部变量,线程之间不会干扰,不用管理Lock的问题,ThreadLocal内部会处理。

ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

具体可以访问底层模块__threading__local

https://github.com/python/cpython/blob/master/Lib/_threading_local.py

问题解决了:

通过ThreadLocal,在一个线程内,参数可以*的在函数之间调用了。

⚠️本文学习为目的 。主要参考https://www.liaoxuefeng.com/wiki/1016959663602400/1017630786314240