Python并发编程之五:threading 模块

一、简介

  • multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,中文文档链接:https://docs.python.org/zh-cn/3/library/threading.html
  • 这个模块在较低级的模块 _thread 基础上建立较高级的线程接口。
  • 该模块的设计基于 Java的线程模型。 但是,在Java里面,锁和条件变量是每个对象的基础特性,而在Python里面,这些被独立成了单独的对象。 Python 的 Thread 类只是 Java 的 Thread 类的一个子集;目前还没有优先级,没有线程组,线程还不能被销毁、停止、暂停、恢复或中断。 Java 的 Thread 类的静态方法在实现时会映射为模块级函数。

二、threading 模块级别函数

函数解释
threading.active_count()返回当前活动的Thread对象的数量,与enumerate()函数返回的列表元素个数相同
threading.current_thread()返回当前Thread对象,对应调用者的控制线程(thread of control)。如果调用者的控制线程不是通过threading模块创建,返回一个功能受限的哑线程对象(dummy thread object)
threading.get_ident()返回一个非零整数,代表当前线程的"线程标识符"。这个值意在作为魔术cookie使用,例如作为索引从特定于线程的字典对象获取数据。当一个线程退出,新的线程创建,线程标识符可能被回收使用
threading.enumerate()返回当前活动Thread对象的列表。该列表包含守护线程、current_thread()函数创建的哑线程,以及主线程,不包含已终止的线程和未启动的线程。
threading.main_thread()返回主线程对象。通常来说,主线程就是启动python解释器的线程。
threading.settrace(func)为启动自threading模块的所有线程设置一个trace函数。在每个线程的run()方法调用前,传递func参数给sys.settrace()
threading.setprofile(func)为启动自threading模块的所有线程设置一个profile函数。在每个线程的run()方法调用前,传递func参数给sys.setprofile()
threading.stack_size([size])返回创建新线程使用的线程堆栈大小。

三、Thread 类的使用

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

调用这个构造函数时,必需带有关键字参数。参数如下:

  • group 应该为 None;为了日后扩展 ThreadGroup 类实现而保留。

  • target 是用于 run() 方法调用的可调用对象。默认是 None,表示不需要调用任何方法。

  • name 是线程名称。默认情况下,由 “Thread-N” 格式构成一个唯一的名称,其中 N 是小的十进制数。

  • args 是用于调用目标函数的参数元组。默认是 ()。

  • kwargs 是用于调用目标函数的关键字参数字典。默认是 {}。

  • 如果不是 None,daemon 参数将显式地设置该线程是否为守护模式。 如果是 None (默认值),线程将继承当前线程的守护模式属性。

1、开启线程的方式

Thread类代表在单独的控制线程中运行的活动,有两种方式指定:传递可调用对象到构造器的target参数,或重写子类的run()方法。除了__int__()方法和run()方法,Thread子类不应该重写除此之外的其他方法。
1、传递可调用对象到构造器的target参数的方式开启线程:

from threading import Thread
import time

def task(name):
    print(f'{name} is running')
    time.sleep(3)
    print(f'{name} is end')

if __name__ == '__main__':
    t = Thread(target=task, args=('子线程',))
    t.start()
    print('主线程 is running')

2、重写子类的run()方法的方式开启线程:

from threading import Thread
import time

class Task(Thread):

    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        print(f'{self.name} is running')
        time.sleep(3)
        print(f'{self.name} is end')

if __name__ == '__main__':
    task = Task('子线程')
    task.start()
    print('主线程 is running')

2、线程与进程关系

from threading import Thread
from multiprocessing import Process
import os

def work(name):
    print(f'{name} say hello', os.getpid(), os.getppid())

if __name__ == '__main__':

    for i in range(20):
        # 在主进程下开启子进程
        t = Process(target=work, args=('进程',))
        t.start()

        #在主进程下开启子线程
        t=Thread(target=work, args=('线程',))
        t.start()

结论:

1、线程开启速度比进程快。
2、在主进程下开启多个线程,每个线程都跟主进程的pid一样,开多个进程,每个进程都有不同的pid。
3、同一进程内的线程之间共享进程内的数据。

3、相关其他方法

方法解释
isAlive()返回线程是否活动的。
getName()返回线程名。
setName()设置线程名。

4、守护线程

1、运行完毕
  • 对主进程来说,运行完毕指的是主进程代码运行完毕。
  • 对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕。
2、遵循规则
守护xxx会等待主xxx运行完毕后被销毁
  • 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束。
  • 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
3、代码测试
from threading import Thread
import time

def work(name):
    print(f'{name} is running')
    time.sleep(3)
    print(f'{name} is end')

if __name__ == '__main__':

    t = Thread(target=work, args=('子线程',))
    t.setDaemon(True) #必须在t.start()之前设置
    t.start()

    print('主线程')
    print(t.is_alive())

运行结果:

子线程 is running
主线程
True

迷惑性强的例子:

from threading import Thread
import time
def foo():
    print(123)
    time.sleep(1)
    print("end123")

def bar():
    print(456)
    time.sleep(3)
    print("end456")

if __name__ == '__main__':

    t1=Thread(target=foo)
    t2=Thread(target=bar)

    t1.setDaemon(True)
    t1.start()
    t2.start()
    print("主线程")

运行结果:

123
456
主线程
end123
end456

解释:由于 t1 和 t2 都属于主线程下的子线程,就算设置了 t1 为守护线程,也要等到非守护线程 t2 运行完毕才算主线程运行完毕!

5、互斥锁(同步锁)

多线程的同步锁与多进程的同步锁是一个道理,就是多个线程抢占同一个数据(资源)时,我们要保证数据的安全,合理的顺序。

6、死锁现象与递归锁

进程也有死锁与递归锁,进程的死锁和递归锁与线程的死锁递归锁同理。

死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁。

1、死锁实例:

from threading import Thread
from threading import Lock
import time

lock_A = Lock()
lock_B = Lock()


class MyThread(Thread):

    def run(self):
        self.f1()
        self.f2()

    def f1(self):
        lock_A.acquire()
        print(f'{self.name}拿到A锁')

        lock_B.acquire()
        print(f'{self.name}拿到B锁')
        lock_B.release()

        lock_A.release()

    def f2(self):
        lock_B.acquire()
        print(f'{self.name}拿到B锁')
        time.sleep(0.1)

        lock_A.acquire()
        print(f'{self.name}拿到A锁')
        lock_A.release()

        lock_B.release()

if __name__ == '__main__':
    for i in range(3):
        t = MyThread()
        t.start()

    print('主....')

运行结果:

Thread-1拿到A锁
Thread-1拿到B锁
Thread-1拿到B锁
Thread-2拿到A锁
主....
# 程序卡死

解释:线程1(Thread-1)执行 f1 函数:拿到A锁→拿到B锁→释放A锁→释放B锁,然后执行 f2 函数:拿到B锁(与此同时,线程2(Thread-2)执行 f1 函数:拿到A锁)→ 程序卡死,原因:线程1(Thread-1)继续执行必须拿到A锁后才能释放B锁,而线程2(Thread-2)执行必须拿到B锁后才能释放A锁,两个线程互不相让,这个时候就导致了阻塞。

解决死锁问题就要用到递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

2、用递归锁解决死锁问题:
首先导入 RLock :

from threading import RLock

将前面代码中的:

lock_A = Lock()
lock_B = Lock()

改为:

lock_A = lock_B = RLock()

这样就可以解决死锁问题!!!

7、信号量

1、线程的信号量同进程的一样。

2、Semaphore管理一个内置的计数器,每当调用acquire()时内置计数器-1;调用release() 时内置计数器+1;计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

3、实例:

from threading import Thread
from threading import Semaphore
from threading import current_thread
import time
import random

sem = Semaphore(5)

def task():
    sem.acquire()
    print(f'{current_thread().getName()} is running!')
    time.sleep(random.randint(1,3))
    sem.release()


if __name__ == '__main__':
    for i in range(20):
        t = Thread(target=task)
        t.start()

解释:同时只有5个线程可以获得 semaphore,即可以限制最大连接数为 5。

8、线程队列

1、线程队列不需要通过 threading 模块里面导入,直接 import queue 就可以,这是python自带的,用法基本和进程 multiprocess 模块中的 Queue 是一样的。

2、实例:

  1. 先进先出:
import queue

q = queue.Queue()

q.put('线程1')
q.put('线程2')
q.put('线程3')

print(q.get())
print(q.get())
print(q.get())

'''
结果:
线程1
线程2
线程3
'''
  1. 先进后出:
import queue

q = queue.LifoQueue()

q.put('线程1')
q.put('线程2')
q.put('线程3')

print(q.get())
print(q.get())
print(q.get())

'''
结果:
线程3
线程2
线程1
'''
  1. 优先级进出:
import queue

q = queue.PriorityQueue()

q.put((1, '线程1'))
q.put((-1, '线程2'))
q.put((0, '线程3'))

print(q.get()[1])
print(q.get()[1])
print(q.get()[1])

'''
结果:
线程2
线程3
线程1
'''

解释:put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高。

9、线程池

1、简介

1、从Python3.2开始,标准库为我们提供了 concurrent.futures 模块,它提供了 ThreadPoolExecutor (线程池)和ProcessPoolExecutor (进程池)两个类。

2、相比 threading 等模块,该模块通过 submit 返回的是一个 future 对象,它是一个未来可期的对象,通过它可以获悉线程的状态主线程(或进程)中可以获取某一个线程(进程)执行的状态或者某一个任务执行的状态及返回值:

  • 主线程可以获取某一个线程(或者任务的)的状态,以及返回值。
  • 当一个线程完成的时候,主线程能够立即知道。
  • 让多线程和多进程的编码接口一致。
2、示例

1、submit(fn, *args, **kwargs) 异步提交任务:

import time
import threading
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def func(n):
    time.sleep(2)
    print('%s打印的:'%(threading.get_ident()), n)
    time.sleep(2)
    return n*n
tpool = ThreadPoolExecutor(max_workers=5) #默认一般起线程的数据不超过CPU个数*5
# tpool = ProcessPoolExecutor(max_workers=5) #进程池的使用只需要将上面的ThreadPoolExecutor改为ProcessPoolExecutor就行了,其他都不用改
# 异步执行
t_lst = []
for i in range(1, 6):
    t = tpool.submit(func, i) # 提交执行函数,返回一个结果对象,i作为任务函数的参数 def submit(self, fn, *args, **kwargs):  可以传任意形式的参数
    t_lst.append(t)

tpool.shutdown() # 相当于进程池的pool.close()+pool.join()操作

print('主线程')
for ti in t_lst:
    print('>>>>', ti.result())

2、map(func, *iterables, timeout=None, chunksize=1) 取代for循环submit的操作:

import time
import threading
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def func(n):
    time.sleep(2)
    print('%s打印的:'%(threading.get_ident()), n)
    time.sleep(2)
    return n*n

if __name__ == '__main__':

    tpool = ThreadPoolExecutor(max_workers=5) #默认一般起线程的数据不超过CPU个数*5
    # tpool = ProcessPoolExecutor(max_workers=5) #进程池的使用只需要将上面的ThreadPoolExecutor改为ProcessPoolExecutor就行了,其他都不用改

    s = tpool.map(func, range(1, 6))

    tpool.shutdown() # 相当于进程池的pool.close()+pool.join()操作
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值