soarli

Python多线程学习笔记
前言之前做过Python多线程的应用,但是一直没有系统学习,加之最近有了相关需求且发现其在Linux上的执行效率是...
扫描右侧二维码阅读全文
03
2022/04

Python多线程学习笔记

前言

之前做过Python多线程的应用,但是一直没有系统学习,加之最近有了相关需求且发现其在Linux上的执行效率是真的高便决定简单学习并做个记录。

基本用法

import threading

def thread_job():
    print('This is an added Thread, number is %s' %threading.current_thread())


def main():
    # 输出激活状态的线程数
    # 1
    print(threading.active_count())

    # 输出当前激活状态线程的详细信息
    # [<_MainThread(MainThread, started 14940)>]
    print(threading.enumerate())

    # 输出正在运行的线程是哪一个
    # <_MainThread(MainThread, started 14940)>
    print(threading.current_thread())

    # 新建一个线程
    # This is an added Thread, number is <Thread(Thread-1, started 9948)>
    added_thread = threading.Thread(target=thread_job)
    added_thread.start()
    
    

if __name__ == '__main__':
    main()

join方法

默认情况下,多线程任务中子线程的运行并不会阻塞主线程,使用join方法即可对主线程进行阻塞。

import threading
from time import sleep

def thread_job():
    print('T1 start\n')
    for i in range(10):
        sleep(0.1)
    print('T1 finish\n')

def main():
    added_thread = threading.Thread(target=thread_job, name="T1")
    added_thread.start()
    print('all done\n')
    
if __name__ == '__main__':
    main()

输出顺序:

T1 start
all done


T1 finish

阻塞状态:

import threading
from time import sleep

def thread_job():
    print('T1 start\n')
    for i in range(10):
        sleep(0.1)
    print('T1 finish\n')

def main():
    added_thread = threading.Thread(target=thread_job, name="T1")
    added_thread.start()
    added_thread.join() # 随后的语句将被阻塞
    print('all done\n')
    
if __name__ == '__main__':
    main()

输出顺序:

T1 start

T1 finish

all done

双线程模型:

import threading
from time import sleep

def thread_job():
    print('T1 start\n')
    for i in range(10):
        sleep(0.1)
    print('T1 finish\n')

# 一个执行较快的函数
def T2_job():
    print('T2 start\n')
    print('T2 finish\n')

def main():
    added_thread = threading.Thread(target=thread_job, name="T1")
    thread_2 = threading.Thread(target=T2_job, name="T2")
    added_thread.start()
    thread_2.start()
    added_thread.join() # 随后的语句将被阻塞
    print('all done\n')
    
if __name__ == '__main__':
    main()

输出结果:

T1 start

T2 start

T2 finish

T1 finish

all done

即便是运行较快的线程,只要不阻塞,其优先级也会低于主线程:

import threading
from time import sleep

def thread_job():
    print('T1 start\n')
    for i in range(10):
        sleep(0.1)
    print('T1 finish\n')

# 一个执行较快的函数
def T2_job():
    print('T2 start\n')
    print('T2 finish\n')

def main():
    added_thread = threading.Thread(target=thread_job, name="T1")
    thread_2 = threading.Thread(target=T2_job, name="T2")
    added_thread.start()
    added_thread.join() # 随后的语句将被阻塞
    thread_2.start()
    # thread_2.join() # 随后的语句将被阻塞
    print('all done\n')
    
if __name__ == '__main__':
    main()

输出结果:

T1 start

T1 finish

T2 start
all done


T2 finish

Queue功能

由于多线程没有返回值,我们可以将其运算结果放在一个队列中,随后每个线程的队列到主线程中后即可取出数据。

以计算列表中每个元素值的平方为例:

import threading
from queue import Queue

# 二次方的实现
def job(l,q):
    for i in range(len(l)):
        l[i] = l[i] ** 2
    q.put(l)

# 多线程模块
def multithreading():
    # 定义队列(放返回值以取代return)
    q = Queue()
    # 所有的线程放入本列表
    threads = []
    data = [[1,2,3],[3,4,5],[4,4,4],[5,5,5]]
    # 定义4个线程
    for i in range(4):
        # 设定线程并对每个线程传进去一个子列表和全局队列
        t = threading.Thread(target=job, args=(data[i], q))
        # 启动线程
        t.start()
        # 加入到所有线程中
        threads.append(t)
    # 阻塞每一个线程以实现异步(需要并发而不是依次执行)
    for thread in threads:
        thread.join()
    # 定义空列表(用来存每一个线程返回过来的值)
    results = []
    for _ in range(4):
        # 按顺序从q中取出值存到results
        results.append(q.get())
    print(results)
    
if __name__ == '__main__':
    multithreading()

全局解释器锁GIL未必提高效率

Python的多线程,只有用于I/O密集型程序时效率才会有明显的提高。

原因如下:

Python代码的执行是由Python虚拟机进行控制。它在主循环中同时只能有一个控制线程在执行,意思就是Python解释器中可以运行多个线程,但是在执行的只有一个线程,其他的处于等待状态。

这些线程执行是有全局解释器锁(GIL)控制,它来保证同时只有一个线程在运行。在多线程运行环境中,Python虚拟机执行方式如下:

  1. 设置GIL
  2. 切换进线程
  3. 执行下面操作之一

​ 1.运行指定数量的字节码指令

​ 2.线程主动让出控制权

  1. 切换出线程(线程处于睡眠状态)
  2. 解锁GIL
  3. 进入1步骤

注意:Python运行计算密集型的多线程程序时,更倾向于让线程在整个时间片内始终占据GIL,而I/O秘籍型的多线程程序在I/O被调用前会释放GIL,以允许其他线程在I/O执行的时候运行。

下面这段示例运行时多线程未必胜得过单线程:

import threading
import time
import copy
from queue import Queue

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)

Lock线程锁

第一个线程处理完再开始第二个线程(针对shared memory场景):

import threading

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()


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()

参考资料:

https://zhuanlan.zhihu.com/p/91601448

https://www.cnblogs.com/guyuyun/p/11185832.html

https://www.bilibili.com/video/BV1jW411Y7Wj

https://docs.python.org/zh-cn/3/library/threading.html

最后修改:2022 年 04 月 04 日 05 : 33 AM

发表评论