【python多线程 Threading】

本文详细介绍了Python多线程的基础操作,包括导入模块、获取线程数、查看线程信息、使用join控制线程执行顺序以及线程锁的应用。通过实例展示了join如何避免主线程提前结束,以及线程锁如何保证共享资源的安全。此外,还讨论了GIL对Python多线程效率的影响,并给出了实际测试结果。

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


多线程是加速程序计算的有效方式,类似于并行计算,在一个脚本中同一时间同时运行多个程序。

在这里插入图片描述

1.多线程基础操作:

1.1导入模块

import threading

1.2获取已激活的线程数

print(threading.active_count())

1.3查看所有线程信息

print(threading.enumerate())
# [<_MainThread(MainThread, started 140736011932608)>, <Thread(SockThread, started daemon 123145376751616)>]

输出的结果是一个<_MainThread(…)>带多个<Thread(…)>。

1.4查看现在正在运行的线程

print(threading.current_thread())
# <_MainThread(MainThread, started 140736011932608)>

1.5添加线程:

def thread_job(): #定义线程功能
    print('This is a thread of %s' % threading.current_thread())
 
def main():
    thread = threading.Thread(target=thread_job,)   # 定义线程 
    thread.start()  # 让线程开始工作
    
if __name__ == '__main__':
    main()

2.join功能:阻塞主线程的运行

2.1 不加 join() 的结果

我们让 T1 线程工作的耗时增加

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")
 
def main():
added_thread = threading.Thread(target=thread_job, name='T1') #定义线程,线程的名字叫T1
added_thread.start()
print("all done\n")
 
if __name__ == '__main__':
    main()

运行结果:

T1 start
all done
T1 finish

因为我们的main主线程和thread_job线程是一起工作的,因为主线程比thread_job(故意做的很久)快,所以先打印出all done。

2.2 加入 join() 的结果

上面例子线程任务还未完成便输出all done。如果要遵循顺序,可以在启动线程后对它调用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")
     
    def main():
    added_thread = threading.Thread(target=thread_job, name='T1') #定义线程,线程的名字叫T1
    added_thread.start()
    added_thread.join()
    print("all done\n")
     
    if __name__ == '__main__':
        main()

结果:

T1 start
T1 finish
all done

2.3使用join对控制多个线程的执行顺序非常关键。

举个例子,假设我们现在再加一个线程T2,T2的任务量较小,会比T1更快完成:

 def T1_job():
        print("T1 start\n")
        for i in range(10):
            time.sleep(0.1)
        print("T1 finish\n")
     
    def T2_job():
        print("T2 start\n")
        print("T2 finish\n")
    def main():
    thread_1 = threading.Thread(target=T1_job, name='T1') #定义线程,线程的名字叫T1

    thread_2 = threading.Thread(target=T2_job, name='T2') #定义线程,线程的名字叫T2
    thread_1.start() # 开启T1
    thread_2.start() # 开启T2
    print("all done\n")
     
    if __name__ == '__main__':
        main()

T2比T1任务简单,结果:

T1 start
T2 start
T2 finish
all done
T1 finish

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

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

thread_1.start()
thread_1.join() # notice the difference!
thread_2.start()
print("all done\n")

结果:

T1 start
T1 finish
T2 start
all done
T2 finish

可以看到,T2会等待T1结束后才开始运行。在试验:

thread_1.start()
thread_2.start()
thread_1.join() # notice the difference!
print("all done\n")
T1 start
T2 start
T2 finish
T1 finish
all done

T2在T1之后启动,并且因为T2任务量小会在T1之前完成;而T1也因为加了join,all done在它完成后才显示。
你也可以添加thread_2.join()进行尝试,但为了规避不必要的麻烦,推荐如下这种1221的V型排布:

 hread_1.start() # start T1
 thread_2.start() # start T2
 thread_2.join() # join for T2
 thread_1.join() # join for T1
 print("all done\n")
  
 """
 T1 start
 T2 start
 T2 finish
 T1 finish
 all done
 """

3.存储进程结果:

多线程功能是没有反馈值的,所以要把线程的结果放在一个队列中,再在主线程中拿出来。

3.1导入线程,队列的标准模块

import threading
import time
from queue import Queue

3.2定义一个被多线程调用的函数

函数的参数是一个列表l和一个队列q,函数的功能是,对列表的每个元素进行平方计算,将结果保存在队列中

def job(l,q):
    for i in range (len(l)):
        l[i] = l[i]**2
    q.put(l)   #多线程调用的函数不能用return返回值

3.3定义一个多线程函数

在多线程函数中定义一个Queue,用来保存返回值,代替return,定义一个多线程列表,初始化一个多维数据列表,用来处理:

def multithreading():
    q =Queue()    #q中存放返回值,代替return的返回值
    threads = []
    data = [[1,2,3],[3,4,5],[4,4,4],[5,5,5]]

在多线程函数中定义四个线程,启动线程,将每个线程添加到多线程的列表中

for i in range(4):   #定义四个线程
    t = threading.Thread(target=job,args=(data[i],q)) #Thread首字母要大写,被调用的job函数没有括号,只是一个索引,参数在后面
    t.start()#开始线程
    threads.append(t) #把每个线程append到线程列表中

分别join四个线程到主线程

for thread in threads:
    thread.join()

定义一个空的列表results,将四个线运行后保存在队列中的结果返回给空列表results

results = []
for _ in range(4):
    results.append(q.get())  #q.get()按顺序从q中拿出一个值
print(results)

完整的代码


```python
   import threading
    import time
     
    from queue import Queue
     
    def job(l,q):
        for i in range (len(l)):
            l[i] = l[i]**2
        q.put(l)
     
    def multithreading():
        q =Queue()
        threads = []
        data = [[1,2,3],[3,4,5],[4,4,4],[5,5,5]]
        for i in range(4):
            t = threading.Thread(target=job,args=(data[i],q)) #Thread首字母要大写,被调用的job函数没有括号,只是一个索引,每次传进去data的一行
            t.start()
            threads.append(t)
        for thread in threads:
            thread.join()
        results = []
        for _ in range(4):
            results.append(q.get())
        print(results)
     
    if __name___=='__main__':
        multithreading()

最后运行结果为:

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

注意上面四个线程是并行计算的。

4. GIL 不一定有效率:

python 的多线程 threading 有时候并不是特别理想. 最主要的原因是就是, Python 的设计上, 有一个必要的环节, 就是 Global Interpreter Lock (GIL). 这个东西让 Python 在同一时间运行一个东西,会不停切换.

3.1 测试 GIL

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

 import threading
    from queue import Queue
    import copy
    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=(copy.copy(l), q), name='T%i' % i)
            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)
     
    if __name__ == '__main__':
        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)

如果你成功运行整套程序, 你大概会有这样的输出. 我们的运算结果没错, 所以程序 threading 和 Normal 运行了一样多次的运算. 但是我们发现 threading 却没有快多少, 按理来说, 我们预期会要快3-4倍, 因为有建立4个线程, 但是并没有. 这就是其中的 GIL 在作怪.

1999998000000
normal:  0.10034608840942383
1999998000000
multithreading:  0.08421492576599121

其实省下的是每个线程的读写时间。如果不同线程功能有差别,则效率提高很多。如果线程功能一样需要进行multiprocessing多核运算会省时间。

5.线程锁 Lock:

锁住第一个线程处理完再运行第二个线程。

5.1不用Lock

import threading  #导入线程标准模块
#函数一:全局变量A的值每次加1,循环10次,并打印
 
def job1():
    global A
    for i in range(10):
        A+=1
        print('job1',A)
 #函数二:全局变量A的值每次加10,循环10次,并打印
     
    def job2():
        global A
        for i in range(10):
            A+=10
            print('job2',A)
 #主函数:定义两个线程,分别执行函数一和函数二
     
    if __name__== '__main__':
        A=0
        t1=threading.Thread(target=job1)
        t2=threading.Thread(target=job2)
        t1.start()
        t2.start()
        t1.join()
        t2.join()
 #运行结果(在spyder编译器下运行的打印结果):
     
    job1job2 11
    job2 21
    job2 31
    job2 41
    job2 51
    job2 61
    job2 71
    job2 81
    job2 91
    job2 101
     1
    job1 102
    job1 103
    job1 104
    job1 105
    job1 106
    job1 107
    job1 108
    job1 109
    job1 110

可以看出,打印的结果非常混乱

5.2使用 Lock 的情况

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

 #函数一和函数二加锁
     
    def job1():
        global A,lock
        lock.acquire()
        for i in range(10):
            A+=1
            print('job1',A)
        lock.release()
     
    def job2():
        global A,lock
        lock.acquire()
        for i in range(10):
            A+=10
            print('job2',A)
        lock.release()
   #主函数中定义一个Lock
     
    if __name__== '__main__':
        lock=threading.Lock()
        A=0
        t1=threading.Thread(target=job1)
        t2=threading.Thread(target=job2)
        t1.start()
        t2.start()
        t1.join()
        t2.join()
  #运行结果
     
    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,最后打印输出的结果是不同的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

【网络星空】

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值