• Python线程(thread)


    Python实用教程_spiritx的博客-CSDN博客

    threading线程模块

    在Python3中,通过threading模块提供线程的功能。原来的thread模块已废弃。但是threading模块中有个Thread类(大写的T,类名),是模块中最主要的线程类,一定要分清楚了,千万不要搞混了。

    threading模块提供了一些比较实用的方法或者属性,例如:

    方法与属性描述
    current_thread()返回当前线程
    active_count()返回当前活跃的线程数,1个主线程+n个子线程
    get_ident()返回当前线程
    enumerate()返回当前活动 Thread 对象列表
    main_thread()返回主 Thread 对象
    settrace(func)为所有线程设置一个 trace 函数
    setprofile(func)为所有线程设置一个 profile 函数
    stack_size([size])返回新创建线程栈大小;或为后续创建的线程设定栈大小为 size
    TIMEOUT_MAXLock.acquire(), RLock.acquire(), Condition.wait() 允许的最大超时时间

    threading模块包含下面的类:

    • Thread:基本线程类
    • Lock:互斥锁
    • RLock:可重入锁,使单一进程再次获得已持有的锁(递归锁)
    • Condition:条件锁,使得一个线程等待另一个线程满足特定条件,比如改变状态或某个值。
    • Semaphore:信号锁。为线程间共享的有限资源提供一个”计数器”,如果没有可用资源则会被阻塞。
    • Event:事件锁,任意数量的线程等待某个事件的发生,在该事件发生后所有线程被激活
    • Timer:一种计时器
    • Barrier:Python3.2新增的“阻碍”类,必须达到指定数量的线程后才可以继续执行。

    创建线程

    有两种方式来创建线程:一种是继承Thread类,并重写它的run()方法;另一种是在实例化threading.Thread对象的时候,将线程要执行的任务函数作为参数传入线程。

    Thread子类

    1. import threading
    2. import time
    3. import random
    4. class MyThread(threading.Thread):
    5. def __init__(self, thread_name):
    6. # 注意:一定要显式的调用父类的初始化函数。
    7. super(MyThread, self).__init__(name=thread_name)
    8. def run(self):
    9. print(f"{self.name} run......")
    10. time.sleep(random.randint(0,3))
    11. print(f"{self.name} Over!")
    12. for i in range(3):
    13. MyThread("thread-" + str(i)).start()

    直接注入任务函数

    1. import threading
    2. import time
    3. def show(arg):
    4. time.sleep(1)
    5. print('thread '+str(arg)+" running....")
    6. if __name__ == '__main__':
    7. for i in range(10):
    8. t = threading.Thread(target=show, args=(i,))
    9. t.start()

    Thread对象

    它的定义如下:

    threading.Thread(self, group=None, target=None, name=None,
         args=(), kwargs=None, *, daemon=None)
    • 参数group是预留的,用于将来扩展;
    • 参数target是一个可调用对象,在线程启动后执行;
    • 参数name是线程的名字。默认值为“Thread-N“,N是一个数字。
    • 参数args和kwargs分别表示调用target时的参数列表和关键字参数。

    Thread类定义了以下常用方法与属性:

    方法与属性说明
    start()

    启动线程,等待CPU调度。

    它在一个线程里最多只能被调用一次。 它安排对象的 run() 方法在一个独立的控制线程中被调用。

    如果同一个线程对象中调用这个方法的次数大于一次,会抛出 RuntimeError 。

    run()

    线程被cpu调度后自动执行的方法

    代表线程活动的方法。

    你可以在子类型里重载这个方法。 标准的 run() 方法会对作为 target 参数传递给该对象构造器的可调用对象(如果存在)发起调用,并附带从 args 和 kwargs 参数分别获取的位置和关键字参数。

    使用列表或元组作为传给 Thread 的 args 参数可以达成同样的效果。

    name只用于识别的字符串。它没有语义。多个线程可以赋予相同的名称。 初始名称由构造函数设置。
    getName()、setName()和name用于获取和设置线程的名称。3.10 版后已移除.

    setDaemon()

    设置为后台线程或前台线程(默认是False,前台线程)。如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止。如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程执行完成后,程序才停止。3.10 版后已移除.
    isDaemon()是否为守护线程,3.10 版后已移除.
    daemon

    一个布尔值,表示这个线程是否是一个守护线程(True)或不是(False)。 这个值必须在调用 start() 之前设置,否则会引发 RuntimeError 。它的初始值继承自创建线程;主线程不是一个守护线程,因此所有在主线程中创建的线程默认为 daemon = False。

    当没有存活的非守护线程时,整个Python程序才会退出。

    ident获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None。
    native_id此线程的线程 ID (TID),由 OS (内核) 分配。 这是一个非负整数,或者如果线程还未启动则为 None。 请参阅 get_native_id() 函数。 这个值可被用来在全系统范围内唯一地标识这个特定线程 (直到线程终结,在那之后该值可能会被 OS 回收再利用)。
    is_alive()判断线程是否是激活的(alive)。从调用start()方法启动线程,到run()方法执行完毕或遇到未处理异常而中断这段时间内,线程是激活的。
    join([timeout])

    调用该方法将会使主调线程堵塞,直到被调用线程运行结束或超时。参数timeout是一个数值类型,表示超时时间,如果未提供该参数,那么主调线程将一直堵塞到被调线程结束。

    这会阻塞调用这个方法的线程,直到被调用 join() 的线程终结 -- 不管是正常终结还是抛出未处理异常 -- 或者直到发生超时,超时选项是可选的。

    当 timeout 参数存在而且不是 None 时,它应该是一个用于指定操作超时的以秒为单位的浮点数或者分数。因为 join() 总是返回 None ,所以你一定要在 join() 后调用 is_alive() 才能判断是否发生超时 -- 如果线程仍然存活,则 join() 超时。

    当 timeout 参数不存在或者是 None ,这个操作会阻塞直到线程终结。

    一个线程可以被合并多次。

    如果尝试加入当前线程会导致死锁, join() 会引起 RuntimeError 异常。如果尝试 join() 一个尚未开始的线程,也会抛出相同的异常。

    在多线程执行过程中,有一个特点要注意,那就是每个线程各执行各的任务,不等待其它的线程,自顾自的完成自己的任务,比如下面的例子:

    1. import time
    2. import threading
    3. def doWaiting():
    4. print('start waiting:', time.strftime('%H:%M:%S'))
    5. time.sleep(3)
    6. print('stop waiting', time.strftime('%H:%M:%S'))
    7. t = threading.Thread(target=doWaiting)
    8. t.start()
    9. # 确保线程t已经启动
    10. time.sleep(1)
    11. print('start job')
    12. print('end job’)
    13. ‘’'
    14. start waiting: 10:50:35
    15. start job
    16. end job
    17. stop waiting 10:50:38
    18. ''

    Python默认会等待最后一个线程执行完毕后才退出。上面例子中,主线程没有等待子线程t执行完毕,而是啥都不管,继续往下执行它自己的代码,执行完毕后也没有结束整个程序,而是等待子线程t执行完毕,整个程序才结束。

    有时候我们希望主线程等等子线程,不要“埋头往前跑”。那要怎么办?使用join()方法!如下所示:

    1. import time
    2. import threading
    3. def doWaiting():
    4. print('start waiting:', time.strftime('%H:%M:%S'))
    5. time.sleep(3)
    6. print('stop waiting', time.strftime('%H:%M:%S'))
    7. t = threading.Thread(target=doWaiting)
    8. t.start()
    9. # 确保线程t已经启动
    10. time.sleep(1)
    11. print('start join')
    12. # 将一直堵塞,直到t运行结束。
    13. t.join()
    14. print('end join’)
    15. ‘’'
    16. start waiting: 10:54:03
    17. start join
    18. stop waiting 10:54:06
    19. end join
    20. ''

    我们还可以使用setDaemon(True)把所有的子线程都变成主线程的守护线程,当主线程结束后,守护子线程也会随之结束,整个程序也跟着退出。

    1. import threading
    2. import time
    3. import random
    4. class MyThread(threading.Thread):
    5. def __init__(self, thread_name):
    6. # 注意:一定要显式的调用父类的初始化函数。
    7. super(MyThread, self).__init__(name=thread_name)
    8. self.daemon = True
    9. def run(self):
    10. print(f"{self.name} run......")
    11. time.sleep(2)
    12. print(f"{self.name} Over!")
    13. for i in range(3):
    14. MyThread("thread-" + str(i)).start()
    15. time.sleep(1)
    16. print("Game Over!”)
    17. ‘’'
    18. thread-0 run......
    19. thread-1 run......
    20. thread-2 run......
    21. Game Over!
    22. ‘''

    线程池

    因为新建线程系统需要分配资源、终止线程系统需要回收资源,所以如果可以重用线程,则可以减去新建/终止的开销以提升性能。同时,使用线程池的语法比自己新建线程执行线程更加简洁。

    简单使用

    Python为我们提供了ThreadPoolExecutor来实现线程池,此线程池默认子线程守护。它的适应场景为突发性大量请求或需要大量线程完成任务,但实际任务处理时间较短。

    class concurrent.futures.ThreadPoolExecutor(max_workers=Nonethread_name_prefix=''initializer=Noneinitargs=())

    1.ThreadPoolExecutor构造实例的时候,传入max_workers参数来设置线程池中最多能同时运行的线程数目。

    2.使用submit函数来提交线程需要执行的任务(函数名和参数)到线程池中,并返回该任务的句柄(类似于文件、画图),注意submit()不是阻塞的,而是立即返回。

    3.通过submit函数返回的任务句柄,能够使用done()方法判断该任务是否结束。下面的例子可以看出,由于任务有2s的延时,在task1提交后立刻判断,task1还未完成,而在延时4s之后判断,task1就完成了。

    4.使用cancel()方法可以取消提交的任务,如果任务已经在线程池中运行了,就取消不了。这个例子中,线程池的大小设置为2,任务已经在运行了,所以取消失败。如果改变线程池的大小为1,那么先提交的是task1,task2还在排队等候,这是时候就可以成功取消。

    5.使用result()方法可以获取任务的返回值,注意:这个方法是阻塞的。

    1. from concurrent.futures import ThreadPoolExecutor
    2. import time
    3. # 参数times用来模拟下载的时间
    4. def down_video(times):
    5. time.sleep(times)
    6. print("down video {}s finished".format(times))
    7. return times
    8. executor = ThreadPoolExecutor(max_workers=2)
    9. # 通过submit函数提交执行的函数到线程池中,submit函数立即返回,不阻塞
    10. task1 = executor.submit(down_video, (3))
    11. task2 = executor.submit(down_video, (2))
    12. # done方法用于判定某个任务是否完成
    13. print("任务1是否已经完成:",task1.done())
    14. # cancel方法用于取消某个任务,该任务没有放入线程池中才能取消成功
    15. print("取消任务2:",task2.cancel())
    16. time.sleep(4)
    17. print("任务1是否已经完成:",task1.done())
    18. # result方法可以获取task的执行结果
    19. print(task1.result())

    submit(fn, *args, **kwargs):将 fn 函数提交给线程池。*args 代表传给 fn 函数的参数,*kwargs 代表以关键字参数的形式为 fn 函数传入参数。

    shutdown(wait=True):关闭线程池。

    1. def func(name, addr):
    2. tn = threading.currentThread().name
    3. logs.info("{} 姓名:{}, 住址:{}".format(tn, name, addr))
    4. time.sleep(3)
    5. return name
    6. def threadPool_with(action, *args):
    7. all_task = []
    8. with ThreadPoolExecutor(max_workers=2) as pool:
    9. for i in range(len(args)):
    10. all_task.append(pool.submit(action, name=args[i].get("name"), addr=args[i].get("addr")))
    11. # 主线程等待所有子线程完成
    12. # wait(fs: 表示需要执行的序列, timeout: 等待的最大时间,如果超过这个时间即使线程未执行完成也将返回, return_when:表示wait返回结果的条件,默认为 ALL_COMPLETED 全部执行完成再返回,可选 FIRST_COMPLETED 第一个子线程完成)
    13. wait(all_task, return_when=FIRST_COMPLETED)
    14. logs.info("fist_结束")
    15. # 会阻塞线程,相当于return_when=ALL_COMPLETED 可与wait调整顺序
    16. result = [i.result() for i in all_task]
    17. logs.info(f"result:{result}")
    18. logs.info("全部结束")
    19. if __name__ == "__main__":
    20. """run"""
    21. threadPool_with(func, {"name": "zhangsan", "addr": "beijing"}, {"name": "lisi", "addr": "beijing"}, {"name": "wangwu", "addr": "beijing"})

    as_completed()

    有时候我们是得知某个任务结束了,就去获取结果,而不是一直判断每个任务有没有结束。这是就可以使用as_completed方法一次取出所有任务的结果。

    1. from concurrent.futures import ThreadPoolExecutor, as_completed
    2. import time
    3. # 参数times用来模拟网络请求的时间
    4. def get_html(times):
    5. time.sleep(times)
    6. print("get page {}s finished".format(times))
    7. return times
    8. executor = ThreadPoolExecutor(max_workers=2)
    9. urls = [3, 2, 4] # 并不是真的url
    10. all_task = [executor.submit(get_html, (url)) for url in urls]
    11. for future in as_completed(all_task):
    12. data = future.result()
    13. print("in main: get page {}s success".format(data))
    14. # 执行结果
    15. # get page 2s finished
    16. # in main: get page 2s success
    17. # get page 3s finished
    18. # in main: get page 3s success
    19. # get page 4s finished
    20. # in main: get page 4s success

    as_completed()方法是一个生成器,在没有任务完成的时候,会阻塞,在有某个任务完成的时候,会yield这个任务,就能执行for循环下面的语句,然后继续阻塞住,循环到所有的任务结束。从结果也可以看出,先完成的任务会先通知主线程


    Executor.map()

    Executor.map(func, *iterables, timeout=None, chunksize=1)

    该函数类似于全局函数 map(func, *iterables),只是该函数将会启动多个线程,以异步方式立即对 iterables 执行 map 处理。

    1. from concurrent.futures import ThreadPoolExecutor
    2. import time
    3. # 参数times用来模拟网络请求的时间
    4. def get_html(times):
    5. time.sleep(times)
    6. print("get page {}s finished".format(times))
    7. return times
    8. executor = ThreadPoolExecutor(max_workers=2)
    9. urls = [3, 2, 4] # 并不是真的url
    10. for data in executor.map(get_html, urls):
    11. print("in main: get page {}s success".format(data))
    12. # 执行结果
    13. # get page 2s finished
    14. # get page 3s finished
    15. # in main: get page 3s success
    16. # in main: get page 2s success
    17. # get page 4s finished
    18. # in main: get page 4s success

    使用map方法,无需提前使用submit方法,map方法与python标准库中的map含义相同,都是将序列中的每个元素都执行同一个函数。上面的代码就是对urls的每个元素都执行get_html函数,并分配各线程池。可以看到执行结果与上面的as_completed方法的结果不同,输出顺序和urls列表的顺序相同,就算2s的任务先执行完成,也会先打印出3s的任务先完成,再打印2s的任务完成。

    wait()

    wait方法可以让主线程阻塞,直到满足设定的要求。

    1. from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED, FIRST_COMPLETED
    2. import time
    3. # 参数times用来模拟网络请求的时间
    4. def get_html(times):
    5. time.sleep(times)
    6. print("get page {}s finished".format(times))
    7. return times
    8. executor = ThreadPoolExecutor(max_workers=2)
    9. urls = [3, 2, 4] # 并不是真的url
    10. all_task = [executor.submit(get_html, (url)) for url in urls]
    11. wait(all_task, return_when=ALL_COMPLETED)
    12. print("main")
    13. # 执行结果
    14. # get page 2s finished
    15. # get page 3s finished
    16. # get page 4s finished
    17. # main

    wait方法接收3个参数,等待的任务序列、超时时间以及等待条件。等待条件return_when默认为ALL_COMPLETED,表明要等待所有的任务都结束。可以看到运行结果中,确实是所有任务都完成了,主线程才打印出main。等待条件还可以设置为FIRST_COMPLETED,表示第一个任务完成就停止等待。

    线程锁

    由于线程之间的任务执行是CPU进行随机调度的,并且每个线程可能只执行了n条指令之后就被切换到别的线程了。当多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,这被称为“线程不安全”。为了保证数据安全,我们设计了线程锁,即同一时刻只允许一个线程操作该数据。线程锁用于锁定资源,可以同时使用多个锁,当你需要独占某一资源时,任何一个锁都可以锁这个资源,就好比你用不同的锁都可以把相同的一个箱子锁住是一个道理。

    我们先看一下没有锁的情况下,脏数据是如何产生的。

    1. import threading
    2. import time
    3. import random
    4. import math
    5. number = 0
    6. random.seed(13)
    7. def plus():
    8. time.sleep(random.random())
    9. print(f"子线程{threading.current_thread().name} start...")
    10. global number # global声明此处的number是外面的全局变量number
    11. for _ in range(100000): # 进行一个大数级别的循环加一运算
    12. localv = number
    13. math.factorial(100) #大计算量的任务,让CPU在多个线程间切换
    14. number = localv + 1
    15. print(f"子线程{threading.current_thread().name}运算结束后,{number=}")
    16. threadp = [0,1,2]
    17. for i in range(3): # 用2个子线程,就可以观察到脏数据
    18. threadp[i] = threading.Thread(target=plus)
    19. threadp[i].start()
    20. time.sleep(3)
    21. for i in range(3):
    22. threadp[i].join()
    23. print("主线程执行完毕后,number = ", number)
    24. ’’’
    25. 子线程Thread-1 (plus) start...
    26. 子线程Thread-1 (plus)运算结束后,number=100000
    27. 子线程Thread-3 (plus) start...
    28. 子线程Thread-2 (plus) start...
    29. 子线程Thread-3 (plus)运算结束后,number=200000
    30. 子线程Thread-2 (plus)运算结束后,number=210542
    31. 主线程执行完毕后,number = 210542
    32. ‘’‘

    结果不等于300000,并且每次重新运行,结果都不一样,可以很明显地看出脏数据的情况。这是因为三个线程在运行过程中,CPU随机调度,你算一会我算一会,在没有对number进行保护的情况下,就发生了数据错误。

    上面为了防止脏数据而使用join()的方法,其实是让多线程变成了单线程,属于因噎废食的做法,正确的做法是使用线程锁。Python在threading模块中定义了几种线程锁类,分别是:

    • Lock 互斥锁
    • RLock 可重入锁
    • Semaphore 信号
    • Event 事件
    • Condition 条件
    • Barrier “阻碍”

    互斥锁Lock

    互斥锁是一种独占锁,同一时刻只有一个线程可以访问共享的数据。使用很简单,初始化锁对象,然后将锁当做参数传递给任务函数,在任务中加锁,使用后释放锁。

    1. import threading
    2. import time
    3. import random
    4. import math
    5. number = 0
    6. random.seed(13)
    7. lk = threading.Lock()
    8. def plus(lk):
    9. time.sleep(random.random())
    10. print(f"子线程{threading.current_thread().name} start...")
    11. global number # global声明此处的number是外面的全局变量number
    12. for _ in range(100000): # 进行一个大数级别的循环加一运算
    13. lk.acquire() # 开始加锁
    14. localv = number
    15. math.factorial(100) #大计算量的任务,让CPU在多个线程间切换
    16. number = localv + 1
    17. lk.release() # 释放锁,让别的线程也可以访问number
    18. print(f"子线程{threading.current_thread().name}运算结束后,{number=}")
    19. threadp = [0,1,2]
    20. for i in range(3): # 用2个子线程,就可以观察到脏数据
    21. threadp[i] = threading.Thread(target=plus, args=(lk,))
    22. threadp[i].start()
    23. time.sleep(3)
    24. for i in range(3):
    25. threadp[i].join()
    26. print("主线程执行完毕后,number = ", number)
    27. ‘’'
    28. 子线程Thread-1 (plus) start...
    29. 子线程Thread-1 (plus)运算结束后,number=100000
    30. 子线程Thread-3 (plus) start...
    31. 子线程Thread-2 (plus) start...
    32. 子线程Thread-2 (plus)运算结束后,number=278418
    33. 子线程Thread-3 (plus)运算结束后,number=300000
    34. 主线程执行完毕后,number = 300000
    35. ‘''

    上次的任务每次都能稳定的输出300000。

    RLock的使用方法和Lock一模一样,只不过它支持重入锁。该锁对象内部维护着一个Lock和一个counter对象。counter对象记录了acquire的次数,使得资源可以被多次require。最后,当所有RLock被release后,其他线程才能获取资源。在同一个线程中,RLock.acquire()可以被多次调用,利用该特性,可以解决部分死锁问题。

    信号Semaphore

    类名:BoundedSemaphore。这种锁允许一定数量的线程同时更改数据,它不是互斥锁。比如地铁安检,排队人很多,工作人员只允许一定数量的人进入安检区,其它的人继续排队。

    1. import time
    2. import threading
    3. def run(n, se):
    4. se.acquire()
    5. print("run the thread: %s" % n)
    6. time.sleep(1)
    7. se.release()
    8. # 设置允许5个线程同时运行
    9. semaphore = threading.BoundedSemaphore(5)
    10. for i in range(20):
    11. t = threading.Thread(target=run, args=(i,semaphore))
    12. t.start()

    运行后,可以看到5个一批的线程被放行。

    事件Event

    事件线程锁的运行机制:全局定义了一个Flag,如果Flag的值为False,那么当程序执行wait()方法时就会阻塞,如果Flag值为True,线程不再阻塞。这种锁,类似交通红绿灯(默认是红灯),它属于在红灯的时候一次性阻挡所有线程,在绿灯的时候,一次性放行所有排队中的线程。

    事件主要提供了四个方法set()、wait()、clear()和is_set()。

    调用clear()方法会将事件的Flag设置为False。

    调用set()方法会将Flag设置为True。

    调用wait()方法将等待“红绿灯”信号。

    is_set():判断当前是否"绿灯放行"状态

    下面是一个模拟红绿灯,然后汽车通行的例子:

    1. #利用Event类模拟红绿灯
    2. import threading
    3. import time
    4. event = threading.Event()
    5. def lighter():
    6. green_time = 5 # 绿灯时间
    7. red_time = 5 # 红灯时间
    8. event.set() # 初始设为绿灯
    9. while True:
    10. print("\33[32;0m 绿灯亮...\033[0m")
    11. time.sleep(green_time)
    12. event.clear()
    13. print("\33[31;0m 红灯亮...\033[0m")
    14. time.sleep(red_time)
    15. event.set()
    16. def run(name):
    17. while True:
    18. if event.is_set(): # 判断当前是否"放行"状态
    19. print("一辆[%s] 呼啸开过..." % name)
    20. time.sleep(1)
    21. else:
    22. print("一辆[%s]开来,看到红灯,无奈的停下了..." % name)
    23. event.wait()
    24. print("[%s] 看到绿灯亮了,瞬间飞起....." % name)
    25. if __name__ == '__main__':
    26. light = threading.Thread(target=lighter,)
    27. light.start()
    28. for name in ['奔驰', '宝马', '奥迪']:
    29. car = threading.Thread(target=run, args=(name,))
    30. car.start()

    条件Condition

    Condition称作条件锁,依然是通过acquire()/release()加锁解锁。

    wait([timeout])方法将使线程进入Condition的等待池等待通知,并释放锁。使用前线程必须已获得锁定,否则将抛出异常。

    notify()方法将从等待池挑选一个线程并通知,收到通知的线程将自动调用acquire()尝试获得锁定(进入锁定池),其他线程仍然在等待池中。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。

    notifyAll()方法将通知等待池中所有的线程,这些线程都将进入锁定池尝试获得锁定。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。

    下面的例子,有助于你理解Condition的使用方法:

    1. import threading
    2. import time
    3. num = 0
    4. con = threading.Condition()
    5. class Foo(threading.Thread):
    6. def __init__(self, name, action):
    7. super(Foo, self).__init__()
    8. self.name = name
    9. self.action = action
    10. def run(self):
    11. global num
    12. con.acquire()
    13. print("%s开始执行..." % self.name)
    14. while True:
    15. if self.action == "add":
    16. num += 1
    17. elif self.action == 'reduce':
    18. num -= 1
    19. else:
    20. exit(1)
    21. print("num当前为:", num)
    22. time.sleep(1)
    23. if num == 5 or num == 0:
    24. print("暂停执行%s!" % self.name)
    25. con.notify()
    26. con.wait()
    27. print("%s开始执行..." % self.name)
    28. con.release()
    29. if __name__ == '__main__':
    30. a = Foo("线程A", 'add')
    31. b = Foo("线程B", 'reduce')
    32. a.start()
    33. b.start()

    定时器Timer

    定时器Timer类是threading模块中的一个小工具,用于指定n秒后执行某操作。一个简单但很实用的东西。

    1. from threading import Timer
    2. def hello():
    3. print("hello, world")
    4. # 表示1秒后执行hello函数
    5. t = Timer(1, hello)
    6. t.start()

    通过with语句使用线程锁

    所有的线程锁都有一个加锁和释放锁的动作,非常类似文件的打开和关闭。在加锁后,如果线程执行过程中出现异常或者错误,没有正常的释放锁,那么其他的线程会造到致命性的影响。通过with上下文管理器,可以确保锁被正常释放。其格式如下:

    with some_lock:
        # 执行任务...
    

    这相当于:

    some_lock.acquire()
    try:
        # 执行任务..
    finally:
        some_lock.release()

    全局解释器锁(GIL)

    既然介绍了多线程和线程锁,那就不得不提及Python的GIL问题。

    1. import threading
    2. import time
    3. import random
    4. number = 0
    5. def plus():
    6. global number # global声明此处的number是外面的全局变量number
    7. time.sleep(random.random())
    8. for _ in range(100): # 进行一个大数级别的循环加一运算
    9. number = number + 1
    10. time.sleep(random.random())
    11. print(f"子线程{threading.current_thread().name}运算结束后,{number=}")
    12. threadp = [0,1,2]
    13. for i in range(3): # 用2个子线程,就可以观察到脏数据
    14. threadp[i] = threading.Thread(target=plus)
    15. threadp[i].start()
    16. time.sleep(3)
    17. for i in range(3):
    18. threadp[i].join()
    19. print("主线程执行完毕后,number = ", number)

    在大多数环境中,单核CPU情况下,本质上某一时刻只能有一个线程被执行,多核CPU时则 可以支持多个线程同时执行。但是在Python中,无论CPU有多少核,同时只能执行一个线程。这是由于GIL的存在导致的。

    GIL的全称是Global Interpreter Lock(全局解释器锁),是Python设计之初为了数据安全所做的决定。Python中的某个线程想要执行,必须先拿到GIL。可以把GIL看作是执行任务的“通行证”,并且在一个Python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。GIL只在CPython解释器中才有,因为CPython调用的是c语言的原生线程,不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。在PyPy和JPython中没有GIL。

    Python多线程的工作流程:

    1. 拿到公共数据
    2. 申请GIL
    3. Python解释器调用操作系统原生线程
    4. cpu执行运算
    5. 当该线程执行一段时间消耗完,无论任务是否已经执行完毕,都会释放GIL
    6. 下一个被CPU调度的线程重复上面的过程

    Python针对不同类型的任务,多线程执行效率是不同的:

    对于CPU密集型任务(各种循环处理、计算等等),由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换是需要消耗资源的),所以Python下的多线程对CPU密集型任务并不友好。

    IO密集型任务(文件处理、网络通信等涉及数据读写的操作),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以Python的多线程对IO密集型任务比较友好。

    为什么不能去掉GIL?

    首先,在早期的Python解释器依赖较多的全局状态,传承下来,使得想要移除当今的GIL变得更加困难。其次,对于程序员而言,仅仅是理解GIL的实现就需要对操作系统设计、多线程编程、C语言、解释器设计和CPython解释器的实现有着非常彻底的理解,更不用说对它进行修改删除了。总之,整体技术难度大,会对当前内部框架产生根本性的影响,牵一发而动全身。

    在1999年,针对Python1.5,一个叫做“freethreading”的补丁已经尝试移除GIL,用细粒度的锁来代替。然而,GIL的移除给单线程程序的执行速度带来了一定的负面影响。当用单线程执行时,速度大约降低了40%。虽然使用两个线程时在速度上得到了提高,但这个提高并没有随着核数的增加而线性增长。因此这个补丁没有被采纳。

    虽然,在Python的不同解释器实现中,如PyPy就移除了GIL,其执行速度更快(不单单是去除GIL的原因)。但是,我们通常使用的CPython解释器版本占有着统治地位的使用量,所以,你懂的。

    在实际使用中的建议:

    Python中想要充分利用多核CPU,就用多进程。因为每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行。在Python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。同时建议在IO密集型任务中使用多线程,在计算密集型任务中使用多进程。另外,深入研究Python的协程机制,你会有惊喜的。

  • 相关阅读:
    HCIA --- ACL(访问控制列表)
    13个Redis面试题,你都知道哪些?
    MFC 注册表
    【DPDK】使用 Open vSwitch * 采用 DPDK 帧间 VM NFV 应用程序
    idea启动Tomcat时控制台出现乱码的解决(亲测有效)
    Http协议和Https协议
    JUC源码笔记
    MyBatis获取参数值的两种方式
    Python之路—200行Python代码搞了个打飞机游戏!!
    【Java 进阶篇】Cookie 使用详解
  • 原文地址:https://blog.csdn.net/spiritx/article/details/132783171