Python学习笔记 线程

本文详细介绍了Python中线程的概念、创建方式、线程间同步机制以及常用的线程类和方法,包括Lock、RLock、Condition、Semaphore、Event和Timer等,帮助读者全面理解Python线程的使用。

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

目录

 -简述
    ~ 线程的概念
    ~ 引入线程的原因
    ~ 线程与进程的区别
    ~ Python 中的线程和 GIL

 -创建线程
    ~ 通过 Thread 类
    ~ 通过自定义的线程类

 -线程间同步(锁)
    ~ Lock
    ~ RLock

 -条件对象(Condition)
    ~ 常用方法及解析
    ~ 示例代码

 -信号量对象(Semaphore)
    ~ 常用方法及解析
    ~ 示例代码

 -事件对象(Event)
    ~ 常用方法及解析
    ~ 示例代码

 -定时器对象(Timer)
    ~ 常用方法及解析

 -Threading 模块的一些方法

简述

线程的概念

  线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。如果把进程比作一个生产车间,那么线程就是车间里的一条生产线。一条线程指的是进程中一个单一顺序的控制流,一个进程中必定有一个线程(称为主线程),且可以并发多个线程,每条线程并行执行不同的任务。

同样是为了并行,为什么有了进程之后还要引入线程?

  因为进程是资源的拥有者,创建、撤销和切换都存在较大的时空开销。而线程则只是执行单位,每一个线程所需要的资源都共享自它所在的进程,各种操作所需的开销就要小很多。有时候也把线程称为轻量级进程。

线程与进程的区别
  1. 线程共享创建它的进程的地址空间;进程具有自己的地址空间。
  2. 线程可以直接访问其所属进程的数据段;子进程具有其父进程数据段的副本。
  3. 线程可以直接与其所属进程中的其他线程通信;进程必须使用进程间通信与同级进程进行通信。
  4. 新线程很容易创建; 新进程需要复制父进程。
  5. 线程可以对同一进程的线程进行相当多的控制;进程只能控制子进程。
  6. 对主线程的更改(取消,优先级更改等)可能会影响该进程其他线程的行为;对父进程的更改不会影响子进程。
Python 中的线程和 GIL

  与其他语言不同,由于 GIL(全局解释锁) 的存在,Python 中的线程不能实现并行机制。也就是说,Python 中的多线程不能利用多核的优势,即使你在一个进程中开启了多个线程,但在运行过程中,同一时刻只会有一个线程被运行。

  不过呢,不能实现线程的并行机制并不是 Python 这门语言的锅,而且 Python 也完全可以实现这种机制,但由于时下最流行,最普遍的 Python 解释器 CPython 上有 GIL 这把锁,导致在 CPython 解释执行的代码在时同一时刻都只能运行一个线程。

  GIL 并不是 Python 语言的特性。Python 可以完全不依赖 GIL ,如果想摆脱 GIL 只需要换一个解释器即可(如 JPython)。但那样的话你可能会面临更大的问题——许多现有的库不再被支持。

  GIL 是加在 CPython 解释器 上的一把全局解释锁,它的作用是在解释器层面上确保线程安全,每个线程在执行时都会先获取GIL,这把锁会保证在同一时刻只有一个线程被运行,从而在解释器层面上保证线程安全(事实证明用这种方法保证线程安全会带来很大的弊端)。

  虽不能并行,但在处理 IO 密集型任务时,Python 的多线程还是能有效提高执行效率的(通过 IO 阻塞或执行至一定时间时切换线程,从而提高 CPU 的利用率。)。对于计算密集型任务,多线程很不理想,因为这类任务需要持续使用CPU,但线程在达到执行时间的阈值之后会自动释放 CUP,这就造成了无意义的线程切换,反而造成了资源浪费。一般不建议用 Python 实现计算密集型任务,如果真的有这方面的需求,可以考虑使用 multiprocessing 模块,通过进程实现并行,或是直接换一种语言来实现。

创建线程

  关于线程方面,Python 提供了 threading 模块,该模块实现了诸多方法和类来满足对线程的各种操作。

通过 Thread 类

  Thread 类是 threading 模块中创建线程对象的类,通过对该类的实例化和其方法的调用,可以执行线程的创建、启动等操作。下面介绍一下 Thread 类

   class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, daemon=None)
           功能:创建一个线程对象
           参数:
                 group:保留参数,用于以后扩展,现在没什么用
                 target:表示调用的对象(由 run() 方法调用),即线程要执行的任务
                 name:表示创建的线程的名字
                 args:表示调用对象的位置参数元组
                 kwargs:表示调用对象的关键字参数字典
                 daemon:守护线程标志,True表示该线程是守护线程,False 表示非守护。默认为继承创建该线程的线程守护模式
           返回值:一个线程对象

属性

           name:一个字符串,表示线程名,无语意,可重名
           ident:线程标识符,线程未开始时为 None,开始后是一个非零整数,可通过 get_ident() 方法获取。线程标识符可被复用
           native_id:一个非负整数(未启动时为 None),表示线程 ID,可在系统范围内唯一标识线程,线程终止后会被回收
               说明:与进程ID相似,线程ID仅在创建线程到终止线程之间有效(确保系统范围内唯一)。
           daemon:一个布尔值,表示守护线程标志。其值继承与创建线程
               说明:必须在 start() 调用之前设置,否则会异常。主线程不是守护线程。当没有存活的非守护线程时,整个Python程序才会退出。

常用方法


      start()
          功能:开始线程活动,即开始执行 run() 方法
          参数:无
          返回值:None
          说明:单个线程只能调用一次,否则引发异常
      
      join(timeout=None)
          功能:阻塞等待,直到线程结束或超时
          参数:timeout是一个浮点数,表示阻塞的秒数,为默认值时表示阻塞至线程结束
          返回值:None
          说明 :判断是否超时在该方法后调用 is_alive() 才能判断
      
      is_alive()
          功能:判断线程是否存活
          参数:无
          返回值:存活为 True,否则返回 False
          说明:在 run() 方法执行期间返回 True
      
      还有 getName()、setName()、getDaemon()和setDaemon(),这些是旧的获取和设置线程名和守护状态的方法
通过自定义的线程类

  自定义的线程类,其实就是定义一个 Thread 的子类。与自定义进程类一样,它也需要注意以下几点

  1. 该子类必须实现 run() 方法
  2. 子类只能覆盖 Thread 类中的 run() 和 __init __() 方法,其他的任何方法都不应该覆盖
  3. 如果重写了__init__方法,在执行其他任何操作之前,先调用父类的__init__方法

  下面来举个栗子

import threading
import time

# 自定义的线程类
# class MyThread(threading.Thread):
#     def __init__(self, name, func):
#         super().__init__()
#         self.name = name
#         self.func = func
#
#     def run(self) -> None:
#         # self.func()
#
#         print("这里是测试2")
#         time.sleep(5)


def func():
    print('这里是另一个线程')
    time.sleep(2)
    print('over')


if __name__ == "__main__":
    my_Thread = threading.Thread(target=func, name='sonThread')
    # my_Thread = MyThread(name='text', func=func)
    my_Thread.setDaemon(True)
    # 设置成守护线程
    my_Thread.start()
    # 启动线程
    print(my_Thread.isDaemon())
    # 返回线程是否是守护线程
    time.sleep(5)
    print('parent over')

PS:线程启动后会自动调用它的 run() 方法。在自定义线程类时,我们可以直接把线程要执行的任务写在 run() 方法中,也可以传入一个可调用对象,然后在 run() 中调用它。

线程间同步(锁)

  线程中的 Lock 和 RLock 是与进程中的相差无几的,区别仅在于所在模块不同,所以这里不做多余的解释了。详情请参考上一篇博文:Python学习笔记 进程

条件对象(Condition)

  条件对象总是与某种类型的锁相关联,看构造方法:class threading.Condition(lock=None),它可以通过手动传入一个锁,或者让它自动创建一个锁。条件对象将锁用于同步某些共享状态的权限,它允许一个或多个线程等待,直到被另一个线程唤醒。

常用方法及解析
		acquire
            说明:同于 lock.acquire(),这里的 lock 是指传入的参数,可以是 Lock 或 RLock

        release()
            说明:同于 lock.release(),这里的 lock 是指传入的参数,可以是 Lock 或 RLock

        wait(timeout=None)
            功能:等待,直到被唤醒或超时
            参数:timeout 是一个浮点数,表示等待的时间,默认为直到被唤醒
            返回值:超时返回 False,否则(被唤醒)返回 True
            说明:这个方法释放底层锁,然后阻塞,直到在另外一个线程中调用同一个条件变量的 notify() 或 notify_all() 唤醒它,或者直到超时。一旦被唤醒或者超时,它重新获得锁并返回

        wait_for(predicate, timeout=None)
            功能:等待,直到条件计算为真或超时
            参数:predicate 应该是一个返回可表示布尔值的可调用对象或判断式,timeout 表示最大等待时间
            返回值:超时返回 False,否则返回 predicate 的返回值
            说明:该方法会重复的调用 wait() 方法,直到满足判断是或发生超时,相当于如下代码
                while not predicate():
                    cv.wait()

        notify(n=1)
            功能:唤醒最多 n 个等待这个条件的线程,如果没有线程等待,则相当于空操作
            参数:n 表示唤醒线程的最大个数,默认为 1
            返回值:None
            说明:该方法必须在获取到底层锁的线程调用,否则会抛出 RuntimeError 异常;
                 另外,该方法不会释放锁,不会使本线程阻塞等待

        notify_all()
            功能:唤醒所有正在等待这个条件的线程
            说明:与 notify() 类似
示例代码
import threading
import time
num, n = 0, 0


def cat():
    global num, n
    con.acquire()
    while True:
        if n == 2:
            # 跳出循环并唤醒另一个线程,促使另一个线程也顺利结束,而不是无休止的运行
            con.notify()
            break
        num += 1
        print("双十一库存的香蕉数:%s" % str(num))
        time.sleep(1)
        if num >= 3:
            print("差不多了,可以让顾客下单了")
            con.notify()
            # notify() 方法唤醒一个等待的线程,但它不会使本线程阻塞等待,不会释放锁
            con.wait()
    con.release()
    # 释放锁


def shopaholic():
    con.acquire()
    global num, n
    while True:
        if n == 3:
            print('还想下单?土你都要吃不起啦')
            break
            # 此条件用于跳出循环,从而使线程执行完毕
        num -= 1
        print("双十一香蕉库存数量:{}".format(num))
        time.sleep(2)
        if num == 0 and n != 3:
            print("库存没了,店家赶紧去补充啊!")
            con.notify()
            # 唤醒另一个线程,执行完该句后,被唤醒的线程仍被阻塞,直到该线程获得锁
            con.wait()
            # 所以本线程需要释放锁,用 wait() 或 release() 方法
            n += 1
    con.release()
    # wait() 方法释放锁,然后阻塞至被唤醒并获得锁,而 release() 方法只会释放锁,并继续往下执行


if __name__ == "__main__":
    lock = threading.Lock()
    con = threading.Condition(lock=lock)
    thread1 = threading.Thread(target=cat)
    thread2 = threading.Thread(target=shopaholic)
    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()

信号量对象(Semaphore)

  Semaphore 是一种同步锁,与 Lock 的区别是它可以允许多个线程访问同一资源。信号量对象的构造函数:class threading.Semaphore(value=1)

  Semaphore 是实现信号量对象的类,信号量对象内部管理着一个原子性的计数器,计数器的值表示 release() 方法调用的次数减去 acquire() 的调用次数再加上一个初始值(即 value 的值)。如果需要, acquire() 方法将会阻塞直到可以返回而不会使得计数器变成负数。在没有显式给出 value 的值时,它默认为1。

  简单来说,信号量对象总共允许 value 个线程同时访问资源,而内部计数器表示的是当前还可以加入的线程数,即计数器的值 = value - 当前的线程数。计数器为 0 时,则其他线程将阻塞等待,直到其值大于零才会有新的线程加入,这一点与进程池相似,即 出去一个进来一个。

常用方法及解析
		acquire(blocking=True, timeout=None)
            功能:阻塞或非阻塞的获取一个信号量,成功后计数器的值会减 1
            参数:blocking 表示是否阻塞,默认值为 True
                 timeout 是一个浮点数,表示阻塞的秒数,若 blocking 为 False,则该参数会被忽略
            返回值:获取成功返回 True,否则返回 False

        release()
            功能:释放一个信号量,使内部计数器加 1
            参数:无
            返回值:None
示例代码
import threading
import time


def waiter(num):
    semaphore.acquire()
    print('I am the waiter on the {}th.'.format(num))
    time.sleep(2) # 因为线程执行时间一样,会同时结束,故看起来两个两个的输出
    # time.sleep(num) # 若执行时间不同,就能看到一进一出的效果了
    semaphore.release()


if __name__ == "__main__":
    semaphore = threading.Semaphore(value=2)
    lis = []
    for i in range(10):
        th = threading.Thread(target=waiter, args=(i,))
        th.start()
        lis.append(th)
    for threads in lis:
        threads.join()

事件对象(Event)

  Event 是线程间通信的最简单机制之一:一个线程发出事件信号,而其他线程等待该信号,当收到信号时就执行相应的操作。

  Event 是实现事件对象的类,事件对象内部管理着一个标志,调用 set() 方法可将其设为 True,当标志为 True 时,等待该标志的线程就会运行,当标志为 False 时,等待标志的线程就会阻塞等待,直到标志变为 True。

  利用事件对象,我们可以在一个线程控制另一个线程的运行。(这让我想起了关键词 yield,利用这个关键词我们可以控制函数的分段运行)

常用方法及解析
       		is_set()
                功能:获取标志状态
                参数:无
                返回值:当且仅当内部标志为 True 时 返回 True,否则返回False

            set()
                功能:将内部标志设置为 True
                参数:无
                返回值:None
                说明:调用该方法后,所有等待这个事件的线程都将被唤醒,此时调用 wait() 方法的线程不会被阻塞

            clear()
                功能:将内部标志设置为 False
                参数:无
                返回值:None
                说明:调用该方法后,调用 wait() 方法的线程将被阻塞

            wait(timeout=None)
                功能:阻塞线程直到内部标志为 True或超时
                参数:timeout 是一个浮点数,表示阻塞的时间,默认无限制
                返回值:None
                说明:如果内部标志为 True,则该方法立刻返回,相当于空操作
示例代码
import threading
import time


def client():
    print('肚子饿了,咱去找个地儿吃饭去')
    print('来到了饭店门前')
    event.set()
    time.sleep(5)
    print('吃饱了,推门而去')
    event.set()


def waiter():
    event.wait()
    print('欢迎光临,客官您里边儿请')
    event.clear()
    event.wait()
    print('谢谢惠顾,客官您慢走')


if __name__ == "__main__":
    event = threading.Event()
    th1 = threading.Thread(target=client,)
    th2 = threading.Thread(target=waiter,)
    lis = [th1, th2]
    for threads in lis:
        threads.start()
    for threads in lis:
        threads.join()

定时器对象(Timer)

  了解一下就可以了

  Timer 类表示一个操作应该在等待一定时间间隔之后运行,相当于一个定时器。Timer 类是 Thread 类的子类,所以它可以像一个自定义线程一样工作。

常用方法及解析
			class threading.Timer(interval, function, args=[], kwargs={})
			        功能:创建一个定时器,在经历 interval 秒的间隔时间后,将用 参数 args h
			             和 关键字参数 kwargs 调用 function
			        参数:interval 表示计时时间,单位为秒
			             function 表示一个可调用对象
			             args 是可调用对象的位置参数列表,默认为空
			             kwargs 表示可调用对象的关键字参数字典
			        返回值:一个定时器对象
        
			start()  :启动定时器
			
			cancel() :停止定时器并取消执行定时器将要执行的操作,仅在定时器仍处于等在状态是有效

threading 模块的一些函数

  threading 模块提供了很多实用的方法,下面来介绍一下

		active_count()
            功能:返回当前存活的 Thread 对象的数量
            参数:无
            返回值:一个整型数,表示存活的线程数
            说明:返回数等于 enumerate() 函数返回的列表长度
        
        enumerate()
            功能:以列表形式返回当前所有存活的 Thread 对象
            参数:无
            返回值:一个列表,其中元素是当前存活的 Thread 对象
            说明:该列表包括守护线程,由 current_thread() 创建的虚拟线程对象和主线程,但不包括终止的线程和尚未启动的线程。
            
        current_thread()
            功能:返回与调用者的控制线程相对应的 Thread 对象,即获取表示当前线程的 Thread 对象。
            参数:无
            返回值:一个线程对象,表示调用该函数的线程
            说明:如果调用者的控制线程不是通过 Thread 创建的,则返回功能受限的虚拟线程对象
        
        main_thread()
            功能:返回主线程对象
            参数:无
            返回值:一个线程对像
            说明:正常情况下,主线程是启动 Python 解释器的线程
        
        get_native_id()
            功能:获取当前线程的 ID
            参数:无
            返回值:一个非负整数
            说明:ID 可以在系统内唯一标示该线程,直到线程终止,ID 被 OS 回收。这是 3.8 版本的新功能
            
另,除了以上函数外,该模块还提供了excepthook()、get_ident()、settrace()等函数,详情请见官网
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值