Python-线程开发【threading、threading.local】

1、简介

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

2、Thread类

2.1、初始化介绍

def __init__(self, group=None, target=None, name=None,args=(), kwargs=None, *, daemon=None)

参数名   含义
target   # 线程调用的对象,就是目标函数
name     # 为线程起个名字
args     # 为目标函数传递实参,元组
kwargs   # 为目标函数关键字传参,字典

2.2、线程启动

2.2.1、示例简单运行即退出

# 最简单的线程程序
def worker():
    print("I'm working")
    print('Fineshed')


t = threading.Thread(target=worker, name='worker')  # 线程对象
t.start()  # 启动
通过threading.Thread创建一个线程对象,target是目标函数,可以使用name为线程指定名称。
但是线程没有启动,需要调用start方法。
线程之所以执行函数,是因为线程中就是要执行代码的,而最简单的代码封装就是函数,所以还是函数调用。
函数执行完,线程也就退出了。

2.2.2、示例 :不让线程退出或者让线程一直工作

import threading
import time


def worker():
    while True:  # for i in range(10):
        time.sleep(0.5)
        print("I'm working")
    print('Fineshed')


t = threading.Thread(target=worker, name='worker')  # 线程对象
t.start()  # 启动
print('=' * 30)  # 注意看这行等号什么时候打印的?首行运行

2.3、线程退出

2.3.1、简介

Python没有提供线程退出的方法,线程在下面情况时退出
1、线程函数内语句执行完毕
2、线程函数中抛出未处理的异常

2.3.2、示例 

import threading
import time


def worker():
    for i in range(10):
        time.sleep(0.5)
        if i > 5:
            # break # 终止循环
            # return # 函数返回
            raise RuntimeError  # 抛异常
        print('I am working')
    print('finished')


t = threading.Thread(target=worker, name='worker')
t.start()
print('=' * 30)
Python的线程没有优先级、没有线程组的概念,也不能被销毁、停止、挂起,那也就没有恢复、中断了。

2.4、线程的传参

import threading
import time

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

t1 = threading.Thread(target=add, name='add', args=(4, 5))
t1.start()
time.sleep(2)

t2 = threading.Thread(target=add, name='add', args=(6,), kwargs={'y': 7})
t2.start()
time.sleep(2)

t3 = threading.Thread(target=add, name='add', kwargs={'x': 8, 'y': 9})
t3.start()

# 线程传参和函数传参没什么区别,本质上就是函数传参。

2.5、threading的属性和方法

2.5.1、属性介绍

名称              含义
current_thread()  # 返回当前线程对象
main_thread()     # 返回主线程对象
active_count()    # 当前处于alive状态的线程个数
enumerate()       # 返回所有活着的线程的列表,不包括已经终止的线程和未开始的线程
get_ident()       # 返回当前线程的ID,非0整数

active_count、enumerate方法返回的值还包括主线程。

2.5.2、示例

import threading
import time


def showtreadinfo():
    print('current thread = {}\nmain thread = {}\nactive count = {}'.format(
        threading.current_thread(),
        threading.main_thread(),
        threading.active_count()))


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


t = threading.Thread(target=worker, name='worker')  # 线程对象
showtreadinfo()
time.sleep(1)
t.start()  # 启动
print('===end===')

2.6、Thread实例的属性和方法

2.6.1、简介

名称         含义
name         #只是一个名字,只是个标识,名称可以重名。getName()、setName()获取、设置这个名词       
ident        #线程ID,它是非0整数。线程启动后才会有ID,否则为None。线程退出,此ID依旧可以访问。此ID可以重复使用
is_alive()   #返回线程是否活着
注意:线程的name这是一个名称,可以重复;ID必须唯一,但可以在线程退出后再利用。

2.6.2、示例

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='worker')  # 线程对象
print(t.name, t.ident)
time.sleep(1)
t.start()  # 启动
print('===end===')
while True:
    time.sleep(1)
    print('{} {} {}'.format(t.name, t.ident,
                            'alive' if t.is_alive() else 'dead'))
    if not t.is_alive():
        print('{} restart'.format(t.name))
        t.start()  # 线程重启??不可以重启

2.7、start和run方法

2.7.1、示例:不能重复start

import threading
import time


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


class MyThread(threading.Thread):
    def start(self):
        print('start~~~~')
        super().start()

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


t = MyThread(target=worker, name='worker')  # 线程对象
t.start()  # 启动
t.start()
# t.run() # 或调用run方法
# t.run()
尝试start两次,或run两次都失败了,但是它们抛出的异常不一样。
但是单独运行start或者run都可以,是否可以不需要start方法了吗?在worker中打印线程名称、id。

2.7.2、示例:在worker中打印线程名称、id

import threading
import time


def worker():
    t = threading.current_thread()
    for i in range(5):
        time.sleep(1)
        print('I am working', t.name, t.ident)
    print('finished')


class MyThread(threading.Thread):
    def start(self):
        print('start~~~~')
        super().start()

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


t = MyThread(target=worker, name='worker')  # 线程对象
t.start()  # 启动
start方法才能启动操作系统线程,并运行run方法。run方法内部调用了目标函数。

3、多线程

3.1、简介

顾名思义,多个线程,一个进程中如果有多个线程运行,就是多线程,实现一种并发。

3.2、示例:到worker1和work2交替执行

import threading
import time
import sys


def worker(f=sys.stdout):
    t = threading.current_thread()
    for i in range(5):
        time.sleep(1)
        print('i am working', t.name, t.ident, file=f)
    print('finished', file=f)


t1 = threading.Thread(target=worker, name='worker1')
t2 = threading.Thread(target=worker, name='worker2', args=(sys.stderr,))
t1.start()
t2.start()

3.3、小结

可以看到worker1和work2交替执行。
当使用start方法启动线程后,进程内有多个活动的线程并行的工作,就是多线程。
一个进程中至少有一个线程,并作为程序的入口,这个线程就是主线程。
一个进程至少有一个主线程。
其他线程称为工作线程。

4、线程安全

多线程执行一段代码,不会产生不确定的结果,那这段代码就是线程安全的。
多线程在运行过程中,由于共享同一进程中的数据,多线程并发使用同一个数据,那么数据就有可能被
相互修改,从而导致某些时刻无法确定这个数据的值,最终随着多线程运行,运行结果不可预期,这就
是线程不安全。

5、daemon线程

5.1、简介

注:有人翻译成后台线程,也有人翻译成守护线程。
Python中,构造线程的时候,可以设置daemon属性,这个属性必须在start方法前设置好。

5.2、源码Thread的__init__方法中

# 源码Thread的__init__方法中
if daemon is not None:
    self._daemonic = daemon # 用户设定bool值
else:
    self._daemonic = current_thread().daemon
线程daemon属性,如果设定就是用户的设置,否则就取当前线程的daemon值。
主线程是non-daemon线程,即daemon = False。
class _MainThread(Thread):
    def __init__(self):
        Thread.__init__(self, name="MainThread", daemon=False)

5.3、daemon示例

import time
import threading


def foo():
    time.sleep(5)
    for i in range(20):
        print(i)


# 主线程是non-daemon线程
t = threading.Thread(target=foo, daemon=False)
t.start()
print('Main Thread Exits')
发现线程t依然执行,主线程已经执行完,但是一直等着线程t。
修改为 t = threading.Thread(target=foo, daemon=True) 试一试,结果程序立即结束了,进程根本没有等daemon线程t。

5.4、daemon函数介绍

名称                 含义
daemon属性           # 表示线程是否是daemon线程,这个值必须在start()之前设置,否则引发RuntimeError异常
isDaemon()           # 是否是daemon线程
setDaemon            # 设置为daemon线程,必须在start方法之前设置

5.5、示例:看主线程何时结束daemon线程

import time
import threading


def worker(name, timeout):
    time.sleep(timeout)
    print('{} working'.format(name))


# 主线程是non-daemon线程
t1 = threading.Thread(target=worker, args=('t1', 5), daemon=True) 
t1.start()

t2 = threading.Thread(target=worker, args=('t2', 10), daemon=False)
t2.start()

print('Main Thread Exits')
上例说明,如果还有non-daemon线程在运行,进程不结束,进程也不会杀掉其它所有daemon线程。
直到所有non-daemon线程全部运行结束(包括主线程),不管有没有daemon线程,程序退出。

5.6、总结

线程具有一个daemon属性,可以手动设置为True或False,也可以不设置,则取默认值None
如果不设置daemon,就取当前线程的daemon来设置它
主线程是non-daemon线程,即daemon = False
从主线程创建的所有线程的不设置daemon属性,则默认都是daemon = False,也就是nondaemon线程
Python程序在没有活着的non-daemon线程运行时,程序退出,也就是除主线程之外剩下的只能
都是daemon线程,主线程才能退出,否则主线程就只能等待

6、join方法

6.1、简介

使用了join方法后,当前线程阻塞了,daemon线程执行完了,主线程才退出了。

6.2、示例:join

import time
import threading


def worker(name, timeout):
    time.sleep(timeout)
    print('{} working'.format(name))


t1 = threading.Thread(target=worker, args=('t1', 3), daemon=True)
t1.start()
t1.join()  # 设置join,取消join对比一下
print('Main Thread Exits')

6.3、示例:join设置超时时间

import time
import threading


def worker(name, timeout):
    time.sleep(timeout)
    print('{} working'.format(name))


t1 = threading.Thread(target=worker, args=('t1', 10), daemon=True)
t1.start()
t1.join(2)
print('~~~~~~~~~~~')
t1.join(2)
print('~~~~~~~~~~~')
print('Main Thread Exits')
join(timeout=None)
join方法是线程的标准方法之一
一个线程中调用另一个线程的join方法,调用者将被阻塞,直到被调用线程终止,或阻塞超时
一个线程可以被join多次
timeout参数指定调用者等待多久,没有设置超时,就一直等到被调用线程结束
调用谁的join方法,就是join谁,就要等谁

7、daemon线程应用场景

主要应用场景有:
1. 后台任务。如发送心跳包、监控,这种场景最多
2. 主线程工作才有用的线程。如主线程中维护这公共的资源,主线程已经清理了,准备退出,而工作
线程使用这些资源工作也没有意义了,一起退出最合适
3. 随时可以被终止的线程
如果主线程退出,想所有其它工作线程一起退出,就使用daemon=True来创建工作线程。
比如,开启一个线程定时判断WEB服务是否正常工作,主线程退出,工作线程也没有必须存在了,应该
随着主线程退出一起退出。这种daemon线程一旦创建,就可以忘记它了,只用关心主线程什么时候退
出就行了。
daemon线程,简化了程序员手动关闭线程的工作。

8、threading.local类

8.1、示例:使用多线程,每个线程完成不同的计算任务

import threading
import time
import logging

FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)


def worker():
    x = 0
    for i in range(100):
        time.sleep(0.0001)
        x += 1
    logging.info(x)


for i in range(10):
    threading.Thread(target=worker, name='t-{}'.format(i)).start()

8.2、示例:使用全局变量多线程完成

import threading
import time
import logging

FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)


class A:
    def __init__(self):
        self.x = 0


# 全局对象
global_data = A()


def worker():
    global_data.x = 0
    for i in range(100):
        time.sleep(0.0001)
        global_data.x += 1
    logging.info(global_data.x)


for i in range(10):
    threading.Thread(target=worker, name='t-{}'.format(i)).start()
上例虽然使用了全局对象,但是线程之间互相干扰,导致了不期望的结果。线程不安全。

8.3、示例:threading.local

python提供 threading.local 类,将这个类实例化得到一个全局对象,但是不同的线程使用这个对象
存储的数据其他线程看不见。
import threading
import time
import logging

FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)
# 全局对象
global_data = threading.local()


def worker():
    global_data.x = 0
    for i in range(100):
        time.sleep(0.0001)
        global_data.x += 1
    logging.info(global_data.x)


for i in range(10):
    threading.Thread(target=worker, name='t-{}'.format(i)).start()

8.4、小结

本质
运行时,threading.local实例处在不同的线程中,就从大字典中找到当前线程相关键值对中的字典,覆盖threading.local实例的 __dict__ 。
这样就可以在不同的线程中,安全地使用线程独有的数据,做到了线程间数据隔离,如同本地变量一样安全。

 

posted @ 2023-07-26 11:38  小粉优化大师  阅读(425)  评论(0)    收藏  举报