Python中的线程threading对象

python基本知识 专栏收录该内容
105 篇文章 5 订阅

Python中的线程threading对象

Python的线程开发使用标准库threading
进程靠线程执行代码,至少有一个主线程,其它线程是工作线程。
主线程是第一个启动的线程。
父线程:如果线程A中启动了一个线程B,A就是B的父线程。
子线程:B就是A的子线程。

Thread类

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

# 签名 
def __init__(self, group=None, target=None, name=None,args=(), kwargs=None, *, daemon=None)
    pass
参数名含义
target线程调用的对象,就是目标函数
name为线程起一个名字(线程的名字)
args为目标函数传递实参,元组
kwargs为目标函数传递关键字参数,字典

线程并启动与退出

  • 通过threading.Thread创建一个线程对象,target是目标函数,可以使用name为线程指定名称。
  • 启动线程需要调用线程的start()方法才能启动
  • 线程之所以执行函数,是因为线程中就是要执行代码的,而简单的封装就是函数,所以还是函数调用
  • 线程执行完,线程就退出了
  • Python没有提供线程退出的方法,线程在下面情况时退出
    1. 线程函数内语句执行完毕
    2. 线程函数中抛出未处理的异常
  • Python的线程没有优先级、没有线程组的概念,也不能被销毁、停止、挂起,那也就没有恢复、中断了。
import threading

def worker():
    print("hello word")
    print("my name is xdd")

t = threading.Thread(target=worker,name="xdd") #创建一个线程对象
t.start() #启动线程

def add(x,y):
    print("{} + {} = {}".format(x,y,x+y),threading.current_thread().ident)

tt = threading.Thread(target=add,name="xddadd",args=(3,),kwargs={"y":15})
tt.start()

threading_002

threading的属性和方法

名称含义
threading.current_thread()返回当前线程对象
threading.current_thread().ident返回当前线程的id
threading.main_thread()返回主线程(main线程)对象
threading.active_count()返回当前处于active状态的线程个数。(活着的,还未运行结束的线程个数)
threading.enumerate()返回所有活着的线程列表,不包括已经终止的线程和未开始的线程
threading.getident()返回当前线程的ID,非0整数
  • active_count,enumerate方法返回的值包含主线程。
import threading

def shothread():
    print("- "*30)
    print(threading.enumerate())
    print(threading.active_count())
    print(threading.get_ident())
    print(threading.current_thread(),threading.current_thread().ident)
    print(threading.main_thread())

shothread()
t = threading.Thread(target=shothread)
t.start()

threading_003

Thread线程的实例对象的属性和方法

名称含义
Thread.name线程的名字,一个标识符,线程的名称可以重名。getName(),setName()获取、设置这个名词
Thread.ident线程ID,是个非0的整数。线程启动后才会有ID,否则为None。线程退出,此时Id依旧可以访问。此ID会被系统重复使用
Thread.is_alive()返回线程是否或者
  • 注意:线程的name这是一个名称,可以重复;ID必须唯一,但可以在线程退出后再利用。
import threading
import time

def worker():
    for i in range(5):
        time.sleep(1)
        print("i am working")
    print("finished")

t = threading.Thread(target=worker,name="worker1")
print(t.name,t.ident,t.is_alive())
t.start()

while True:
    time.sleep(0.8)
    tbool = t.is_alive()
    if tbool:
        print("main print id = {},name = {},是否活着{}".format(t.ident,t.name,tbool))
    else:
        print(t.ident,t.is_alive())
        t.start() #不能重新启动,线程只能启动一次

threading_004

Thread线程对象的start,run和join方法

名称含义
Thread.start()启动线程。每一个线程必须且只能执行该方法一次(实质是调用操作系统接口构建一个线程)
Thread.run()运行线程函数
Thread.join(timeout=None)柱塞当前程序,直到Thread程序运行完成。
如果设置了timeout,那么最多只阻塞timeout秒。没有设置,默认是永久阻塞。
  • start()方法会调用run()方法,而run()方法来运行函数
    1. 使用start方法启动线程,会正真的启动一个新的线程。
    2. 使用run方法,会执行执行线程中的函数,不会出现新的线程
  • 因此,启动线程请使用start方法,且对于这个线程来说,start方法只能调用一次。(设置_started属性实现)
import threading
import time

def worker():
    print("当前所有线程:{}".format(threading.enumerate()))
    for i in range(5):
        time.sleep(1)
        print("id={},name={}线程正在运行".format(threading.get_ident(),threading.current_thread().name))
    print("finished")

class Mythread(threading.Thread):
    def start(self) -> None:
        print("start~~~~~~~~~~~")
        super().start()

    def run(self):
        print("run~~~~~~~~~~")
        super().run()

t = Mythread(target=worker,name="worker1")
print("- "*30,"开始测试run")
t.run()
# 由于run函数执行完后会 删除del self._target, self._args, self._kwargs线程对象的这些属性,所以动态添加
t._target = worker
t._args = ()
t._kwargs = {}
print("- "*30,"开始测试start")
t.start()

threading_005

  • join方法演示
import threading
import time
import sys

def print(st):
    sys.stdout.write(st+"\n")

def worker(num):
    for i in range(num):
        print("{}正在运行。{}".format(threading.current_thread().ident,threading.enumerate()))
        time.sleep(1)

t2 = threading.Thread(target=worker,args=(5,))
t2.start()

t2.join() #可以看到要等t2执行完成后才调用下面打印方法
print("main Thread Exits")

threading_008

多线程

顾名思义,多个线程,一个进程中如果有多个线程运行,就是多线程,实现一种并发。
当使用start方法启动线程后,进程内有多个活动的线程并行的工作,就是多线程。

  • 一个进程中至少有一个线程,并作为程序的入口,这个线程就是主线程
  • 一个进程至少有一个主线程。其他线程称为工作线程
import threading
import time
import sys

def print(st):
    sys.stdout.write(st+"\n")

def worker():
    print("当前所有线程:{}".format(threading.enumerate()))
    for i in range(5):
        time.sleep(1)
        print("id={},name={}线程正在运行".format(threading.get_ident(),threading.current_thread().name))
    print("{}线程finished".format(threading.current_thread().name))

class Mythread(threading.Thread):
    def start(self) -> None:
        print("start~~~~~~~~~~~")
        super().start()

    def run(self):
        print("run~~~~~~~~~~")
        super().run()

t1 = Mythread(target=worker,name="t1")
t2 = Mythread(target=worker,name="t2")

t1.start()
t2.start()

threading_006
可以看到t1和t2交替执行

线程安全

上面多线程示例代码中如果不加如下代码:

def print(st):
    sys.stdout.write(st+"\n")

多运行几次,有机会出现如下打印结果:
threading_007
在IPython中演示,会更加明显

  • 开图,打印结果应该是一行行,但是很多字符串乱打印在了一起。说明,print函数被打断了,被线程切换打断了。
    1. print函数分两步打印,第一步打印字符串,第二步打印换行符,就在这之间,发生了线程的切换。所以会产生上面的打印结果,说明print函数是线程不安全
  • 线程安全:线程执行一段代码,不会产生不确定的结果,那这段代码就是线程安全的
  • 多线程编程时,防止print函数的打印错乱解决办法
    1. 不让print打印换行符,即end=""就可以了,在打印一个字符时,末尾加上换行符\n
    2. 使用sys.stdout.write()方法代替打印
    3. 使用loggin模块打印.import logging,logging.warning()
  • loggin标准库里面的模块,日志处理模块,线程安全的,生成环境代码都使用logging

daemon线程和non-daemon线程

注意:这里的daemon不是Linux中的守护进程
Python中,构造线程的时候,可以设置daemon属性,这个属性必须在start方法前面设置好。
在Thread类中设置daemon的源码如下:

# 源码Thread类中的__init__方法中
if daemon is not None:
    self._daemonic = daemon #用户设定的daemon值
else:
    self._daemonic = current_thread().daemon
  • 主线程main中的daemon = False。所以主线程是non-daemon线程
  • 如果子线程是daemon线程,即daemon = True。当主线程执行完成,发现子线程是daemon线程,并且还未执行完成。不会等待子线程而直接结束主线程。
  • 如果子线程是non-daemon线程,即daemon = False。当主线程执行完成,发现子线程是non-daemon线程,会等待子线程。直到子线程执行完成后才退出。
名称含义
Thread.daemon属性表示线程是否是daemon线程,这个值必须在start()之前设置,否则会引发RuntimeError异常
Thread.isDaemon()是否是daemon线程
Thread.setDaemon(TrueFalse)
import threading
import time
import sys

def print(st):
    sys.stdout.write(st+"\n")

def worker(name,timeout):
    print("{}的daemon = {}".format(name,threading.current_thread().daemon))
    time.sleep(timeout)
    print("{} working".format(name))

t1 = threading.Thread(target=worker,args=("t1",5),daemon=True)
t1.start()

t2 = threading.Thread(target=worker,args=("t2",2),daemon=True)
t2.setDaemon(False)
t2.start()

print("main Thread Exits")

上例说明,如果除主线程之外还有non-daemon线程的时候,主线程退出时,也不会杀掉所有daemon线程,直到 所有non-daemon线程全部结束,如果还有daemon线程,主线程需要退出(主线程退出也可以理解为后一个 non-daemon线程也要退出了),会结束所有daemon线程,程序退出。

  • daemon线程的应用场景
    简单来说就是,本来并没有 daemon thread,为了简化程序员的工作,让他们不用去记录和管理那些后台线程, 创造了一个 daemon thread 的概念。这个概念唯一的作用就是,当你把一个线程设置为 daemon,它可以会随主 线程的退出而退出。
    主要应用场景有
    1. 后台任务。如发送心跳包、监控,这种场景多。
    2. 主线程工作才有用的线程。如主线程中维护这公共的资源,主线程已经清理了,准备退出,而工作线程使用这 些资源工作也没有意义了,一起退出合适。
    3. 随时可以被终止的线程

如果主线程退出,想所有其它工作线程一起退出,就使用daemon=True来创建工作线程。 比如,开启一个线程定时判断WEB服务是否正常工作,主线程退出,工作线程也没有必须存在了,应该随着主线程 退出一起退出。这种daemon线程一旦创建,就可以忘记它了,只用关心主线程什么时候退出就行了。 daemon线程,简化了程序员手动关闭线程的工作。
如果在non-daemon线程A中,对另一个daemon线程B使用了join方法,这个线程B设置成daemon就没有什么意 义了,因为non-daemon线程A总是要等待B。
如果在一个daemon线程C中,对另一个daemon线程D使用了join方法,只能说明C要等待D,主线程退出,C和D 不管是否结束,也不管它们谁等谁,都要被杀掉。

线程中的作用域与threading.local类

  • 父线程中的变量,在子线程中可以使用,类似于方法的作用域
  • python提供 threading.local 类,将这个类实例化得到一个全局对象,但是不同的线程使用这个对象存储的数据 其他线程看不见。
  • threading.local类构建了一个大字典,存放所有线程相关的字典,定义如下:
    1. { id(Thread) -> (ref(Thread), thread-local dict) }
    2. 每一线程实例的id为key,元组为value。
    3. value中2部分为,线程对象引用,每个线程自己的字典。
  • threading.local本质
    1. 运行时,threading.local实例处在不同的线程中,就从大字典中找到当前线程相关键值对中的字典,覆盖 threading.local实例的 __dict__ 。这样就可以在不同的线程中,安全地使用线程独有的数据,做到了线程间数据隔离,如同本地变量一样安全。
import threading
import time
import sys

def print(st):
    sys.stdout.write(st+"\n")

class A:pass

num1 = threading.local()
num2 = A()
num1.x = 0
num2.x = 0

def worker(num1,num2):
    num1.x = 0 #应为local对象是线程安全的,每个线程中的属性都不一样,为了防止属性报错,先赋值
    for i in range(100):
        num1.x += 1
        num2.x += 1
    print("{} 中 num1.x = {},num2.x = {}".format(threading.current_thread(),num1.x,num2.x))

t1 = threading.Thread(target=worker,args=(num1,num2))
t1.start()
t2 = threading.Thread(target=worker,args=(num1,num2))
t2.start()

t1.join() 
t2.join()
print("main Thread Exits")
print("num1.x = {},num2.x = {}".format(num1.x,num2.x))
  • 具体情况可查考threading.local源码,非常清楚。

    1. 部分源码解析:(local切换字典)
    @contextmanager #被contextmanager装饰,支持with语法,
    def _patch(self):
        impl = object.__getattribute__(self, '_local__impl')
        try:
            dct = impl.get_dict() #会根据自己线程id查找对应的字典
        except KeyError: #找不到字典,会动态增加字典
            dct = impl.create_dict() #动态创建字典
            args, kw = impl.localargs
            self.__init__(*args, **kw)
        with impl.locallock:
            object.__setattr__(self, '__dict__', dct) #动态跟换字典
            yield  #只写到yield,只对进入with做增强
    
    class local:
        def __getattribute__(self, name): #所有属性访问都从这个方法进入
            with _patch(self):
                return object.__getattribute__(self, name)
    
  • 8
    点赞
  • 1
    评论
  • 27
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页

打赏作者

带着梦想飞翔

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值