【python】 Multithreading

本文详细介绍了Python中多线程的使用方法,包括线程的创建、控制和结果收集,探讨了GIL对多线程效率的影响,并演示了如何使用锁避免线程间的数据竞争。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

如下是学习笔记,学习资源来自

莫烦PYTHON:https://morvanzhou.github.io/tutorials/python-basic/threading/

视频资源:https://www.bilibili.com/video/av16944429/?p=1

更多关于 python 的文章可以参考 【Python】



1 什么是 Multithreading

多线程是加速程序计算的有效方式,Python的多线程模块threading上手快速简单,从这节开始我们就教大家如何使用它。

2 添加线程 Thread

先介绍一下几个基本的操作

import threading
print(threading.active_count(),'\n') # 获取已激活的线程数
print(threading.enumerate(),'\n') # 查看所有线程信息
print(threading.current_thread()) # 查看现在正在运行的线程

output

5 

[<_MainThread(MainThread, started 16252)>, <Thread(Thread-4, started daemon 14528)>, <Heartbeat(Thread-5, started daemon 7312)>, <HistorySavingThread(IPythonHistorySavingThread, started 10140)>, <ParentPollerWindows(Thread-3, started daemon 13244)>] 

<_MainThread(MainThread, started 16252)>

注意所有线程信息的组成方式为 一个<_MainThread(...)>带多个<Thread(...)>。

下面来添加一个线程

import threading
def thread_job():
    print('this is an added Thread,number is %s'% threading.current_thread())
added_thread = threading.Thread(target=thread_job)
added_thread.start()

output

this is an added Thread,number is <Thread(Thread-7, started 5808)>

3 join 功能

import threading
import time
def thread_job():
    print("T1 start\n")
    for i in range(10):
        time.sleep(0.1) # 任务间隔0.1s
    print("T1 finish\n")
added_thread = threading.Thread(target=thread_job)
added_thread.start()
#added_thread.join()
time.sleep(0.1)
print("all done\n")

预期输出顺序

T1 start
T1 finish
all done

真实 output 的顺序(如果不加第二个 time.sleep(0.1),可能结果不一样,取决于线程的速度了)

T1 start
all done
T1 finish

线程任务还未完成便输出all done。如果要遵循顺序,可以在启动线程后对它调用join
added_thread.start() 之后 加入 added_thread.join() ,结果为

T1 start
T1 finish
all done

使用join对控制多个线程的执行顺序非常关键。举个例子,假设我们现在再加一个线程 added_thread2added_thread2 的任务量较小,会比 added_thread 更快完成:

import threading
import time 
def thread_job():
    print("T1 start\n")
    for i in range(10):
        time.sleep(0.1)
    print("T1 finish\n")
def thread_job2():
    print("T2 start\n")
    print("T2 finish\n")
added_thread = threading.Thread(target=thread_job)
added_thread2 = threading.Thread(target=thread_job2)
added_thread.start()
added_thread2.start()
print("all done\n")

输出顺序的”一种”结果是:

T2 start
T1 start
all done
T2 finish
T1 finish

现在 T1T2 都没有 join,注意这里说”一种”是因为 all done 的出现完全取决于两个线程的执行速度, 完全有可能 T2 finish 出现在 all done 之前。这种杂乱的执行方式是我们不能忍受的,因此要使用 join 加以控制。

我们试试在T1启动后,T2启动前加上 added_thread.join()

added_thread.start()
added_thread.join()
added_thread2.start()

output 的顺序为

T1 start
T1 finish
T2 start
all done
T2 finish

可以看到,T2 会等待 T1 结束后才开始运行。
推荐使用1221的 V 型排布:

added_thread.start()
added_thread2.start()
added_thread2.join()
added_thread.join()

output 的顺序为

T1 start
T2 start
T2 finish
T1 finish
all done

4 储存进程结果 Queue

代码实现功能,将数据列表中的数据传入,使用四个线程处理,将结果保存在 Queue 中,线程执行完后,从Queue 中获取存储的结果。

import threading
from queue import Queue
# 定义一个被多线程调用的函数
def job(list1,q):
    for i in range(len(list1)):
        list1[i] = list1[i]**2
    q.put(list1) #多线程调用的函数不能用return返回值
# 定义一个多线程函数 
def multithreading():
    q = Queue()
    threads = []
    data = [[1,2,3],[3,4,5],[4,4,4],[5,5,5]]
    for i in range(4):
        #Thread首字母要大写,被调用的job函数没有括号,只是一个索引,参数在后面
        t = threading.Thread(target=job,args=(data[i],q))
        t.start()
        threads.append(t) #把每个线程append到线程列表中
    for thread in threads:
        thread.join()
    results = []
    for _ in range(4):
        results.append(q.get()) #q.get()按顺序从q中拿出一个值
    print(results)
multithreading()

output

[[1, 4, 9], [9, 16, 25], [16, 16, 16], [25, 25, 25]]

5 GIL(Global Interpreter Lock) 不一定有效率

这次我们来看看为什么说 python 的多线程 threading 有时候并不是特别理想. 最主要的原因是就是, Python 的设计上, 有一个必要的环节, 就是 Global Interpreter Lock (GIL). 这个东西让 Python 还是一次性只能处理一个东西.

12.9 Python的全局锁问题 摘抄了一段对于 GIL 的解释:

  • 尽管Python完全支持多线程编程, 但是解释器的C语言实现部分在完全并行执行时并不是线程安全的。 实际上,解释器被一个全局解释器锁保护着,它确保任何时候都只有一个Python线程执行。 GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势 (比如一个使用了多个线程的计算密集型程序只会在一个单CPU上面运行)。

  • 在讨论普通的GIL之前,有一点要强调的是GIL只会影响到那些严重依赖CPU的程序(比如计算型的)。 如果你的程序大部分只会涉及到I/O,比如网络交互,那么使用多线程就很合适, 因为它们大部分时间都在等待。 实际上,你完全可以放心的创建几千个Python线程, 现代操作系统运行这么多线程没有任何压力,没啥可担心的。

现在我们来测试一下 GIL

我们创建一个 job, 分别用 threading 和 一般的方式执行这段程序. 并且创建一个 list 来存放我们要处理的数据. 在 Normal 的时候, 我们这个 list 扩展4倍, 在 threading 的时候, 我们建立4个线程, 并对运行时间进行对比.

import threading
from queue import Queue
import time

def job(l, q):
    res = sum(l)
    q.put(res)

def multithreading(l):
    q = Queue()
    threads = []
    for i in range(4):
        t = threading.Thread(target=job, args=(l, q))
        t.start()
        threads.append(t)
    [t.join() for t in threads]
    total = 0
    for _ in range(4):
        total += q.get()
    print(total)

def normal(l):
    total = sum(l)
    print(total)

l = list(range(1000000))
s_t = time.time()
normal(l*4)
print('normal: ',time.time()-s_t)
s_t = time.time()
multithreading(l)
print('multithreading: ', time.time()-s_t)

output

1999998000000
normal:  0.15650606155395508
1999998000000
multithreading:  0.07280611991882324

我们发现 threading 却没有快多少, 按理来说, 我们预期会要快 3-4倍, 因为有建立 4 个线程, 但是并没有. 这就是其中的 GIL 在作怪.

6 线程锁 Lock

1)不使用 Lock 的情况

import threading
import time
from random import uniform
# 函数一:全局变量A的值每次加1,循环10次,并打印
def job1():
    global A
    for i in range(10):
        time.sleep(uniform(0,1))
        A+=1
        print('job1',A)
# 函数二:全局变量A的值每次加10,循环10次,并打印
def job2():
    global A
    for i in range(10):
        time.sleep(uniform(0,1))
        A+=10
        print('job2',A)
# 主函数:定义两个线程,分别执行函数一和函数二
A=0
t1=threading.Thread(target=job1)
t2=threading.Thread(target=job2)
t1.start()
t2.start()
t2.join()
t1.join()

不加 time.sleep(uniform(0,1)) 打印的结果可能很乱,print 中逗号前后两句有先后关系!uniform(0,1) 的功能是随机产生0和1之间的数字!

结果如下

job1 1
job1 2
job2 12
job2 22
job2 32
job1 33
job2 43
job1 44
job1 45
job2 55
job1 56
job2 66
job1 67
job1 68
job1 69
job2 79
job2 89
job1 90
job2 100
job2 110

可以看出,打印的结果非常混乱,job1 和 job2 在交替占用资源A

2)使用 Lock 的情况
lock 在不同线程使用同一共享内存时,能够确保线程之间互不影响,使用lock的方法是, 在每个线程执行运算修改共享内存之前,执行 lock.acquire() 将共享内存上锁, 确保当前线程执行时,内存不会被其他线程访问,执行运算完毕后,使用 lock.release() 将锁打开, 保证其他的线程可以使用该共享内存。

import threading
import time
from random import uniform
# 函数一:全局变量A的值每次加1,循环10次,并打印
def job1():
    global A,lock
    lock.acquire()
    for i in range(10):
        time.sleep(uniform(0,1))
        A+=1
        print('job1',A)
    lock.release()
# 函数二:全局变量A的值每次加10,循环10次,并打印
def job2():
    global A,lock
    lock.acquire()
    for i in range(10):
        time.sleep(uniform(0,1))
        A+=10
        print('job2',A)
    lock.release()
# 主函数:定义两个线程,分别执行函数一和函数二
lock=threading.Lock()
A=0
t1=threading.Thread(target=job1)
t2=threading.Thread(target=job2)
t1.start()
t2.start()
t2.join()
t1.join()

output

job1 1
job1 2
job1 3
job1 4
job1 5
job1 6
job1 7
job1 8
job1 9
job1 10
job2 20
job2 30
job2 40
job2 50
job2 60
job2 70
job2 80
job2 90
job2 100
job2 110

从打印结果来看,使用 lock 后,一个一个线程执行完。使用 lock 和不使用 lock ,最后打印输出的结果是不同的。

补充

多进程 demo

来自 骚操作!一行 Python 代码实现并行

将遍历传入的文件夹中的图片文件生成缩略图,并将这些缩略图保存到特定文件夹中

import os 
import PIL 

from multiprocessing import Pool 
from PIL import Image

SIZE = (75,75)
SAVE_DIRECTORY = 'thumbs'

def get_image_paths(folder):
    return (os.path.join(folder, f) 
            for f in os.listdir(folder) 
            if 'jpeg' in f)

def create_thumbnail(filename): 
    im = Image.open(filename)
    im.thumbnail(SIZE, Image.ANTIALIAS)
    base, fname = os.path.split(filename) 
    save_path = os.path.join(base, SAVE_DIRECTORY, fname)
    im.save(save_path)

if __name__ == '__main__':
    folder = os.path.abspath(
        '11_18_2013_R000_IQM_Big_Sur_Mon__e10d1958e7b766c3e840')
    os.mkdir(os.path.join(folder, SAVE_DIRECTORY))

    images = get_image_paths(folder)

    pool = Pool()
    pool.map(creat_thumbnail, images)
    pool.close()
    pool.join()

多线程 demo

来自 骚操作!一行 Python 代码实现并行

dummy 是 multiprocessing 模块的完整克隆,唯一的不同在于 multiprocessing 作用于进程,而 dummy 模块作用于线程(因此也包括了 Python 所有常见的多线程限制)。

所以替换使用这两个库异常容易。你可以针对 IO 密集型任务和 CPU 密集型任务来选择不同的库。

import urllib2 
from multiprocessing.dummy import Pool as ThreadPool 

urls = [
    'http://www.python.org', 
    'http://www.python.org/about/',
    'http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html',
    'http://www.python.org/doc/',
    'http://www.python.org/download/',
    'http://www.python.org/getit/',
    'http://www.python.org/community/',
    'https://wiki.python.org/moin/',
    'http://planet.python.org/',
    'https://wiki.python.org/moin/LocalUserGroups',
    'http://www.python.org/psf/',
    'http://docs.python.org/devguide/',
    'http://www.python.org/community/awards/'
    # etc.. 
    ]

# Make the Pool of workers
pool = ThreadPool(4) 
# Open the urls in their own threads
# and return the results
results = pool.map(urllib2.urlopen, urls)
#close the pool and wait for the work to finish 
pool.close() 
pool.join()

更多关于 python 的文章可以参考 【Python】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值