threading:Python线程的基础知识

发布时间 2023-09-28 14:12:01作者: 阿瑞娜

前言

前面的subprocess库主要讲解的是进程知识与进程间的交互。而进程有可以拥有多个线程,所以threading库提供了管理多个线程执行的API,允许程序在同一个进程空间并发地运行多个操作。

本篇,将详细的介绍Python线程库:threading。

Thread对象

要使用threading库,最简单的方式是使用Thread,它可以直接通过函数实例化一个Thread对象,并调用start让它工作。毕竟,我们用线程也是为了执行耗时任务,把任务封装到一个函数中,直接创建往往最简单。

示例如下:

import threading

def print1tonum(num):
    for i in range(10000):
        print(i)

t = threading.Thread(target=print1tonum,args=(10000,))
t.start()

运行之后,效果如下:

这里,我们创建了一个线程,并向它传递参数告诉它要完成什么工作。

区分线程

一般来说,我们创建线程是避免在主线程中处理耗时的任务,但是有时候,就算是基本的运算任务,因为其叠加起来非常的多,我们会考虑用多个线程进行处理。示例如下:

import threading
import time

def printThreadName1():
    print(threading.current_thread().getName() + " start")
    time.sleep(0.2)
    print(threading.current_thread().getName() + ' end')

def printThreadName2():
    print(threading.current_thread().getName() + " start")
    time.sleep(0.2)
    print(threading.current_thread().getName() + ' end')

def printThreadName3():
    print(threading.current_thread().getName() + " start")
    time.sleep(0.2)
    print(threading.current_thread().getName() + ' end')

t1 = threading.Thread(name='t1', target=printThreadName1)
t2 = threading.Thread(name='t2', target=printThreadName2)
t3 = threading.Thread(name='t3', target=printThreadName3)
t1.start()
t2.start()
t3.start()

运行之后,效果如下:

这里,我们会发现print打印非常的混乱,虽然3个线程都是一摸一样的,但结束的时候并不是按顺序结束的,因为它们是同时运行的。(这里需要注意,更多内容接着往下看)

守护线程

运行上面的代码我们会发现,主程序都是在等线程运行完成之后,才结束的。也就是说,创建线程的主程序,无法在线程结束前安全退出。那么,可不可保证运行线程时,主线程可以退出呢?

答案是可以的,这个时候我们需要用到守护线程,这个线程可以一直运行而不阻塞主程序的退出,比如在服务器监控的工具线程,对于这些服务,守护线程往往更有用。

要构造守护线程,需要将上面创建线程的方式增加一个参数daemon,它是一个布尔值,默认值为False,普通线程,改为True就是守护线程。

import threading
import time

def printThreadName1():
    print(threading.current_thread().getName() + " start")
    time.sleep(0.2)
    print(threading.current_thread().getName() + ' end')

def printThreadName2():
    print(threading.current_thread().getName() + " start")
    time.sleep(0.2)
    print(threading.current_thread().getName() + ' end')

def printThreadName3():
    print(threading.current_thread().getName() + " start")
    time.sleep(0.2)
    print(threading.current_thread().getName() + ' end')

t1 = threading.Thread(name='t1', target=printThreadName1, daemon=True)
t2 = threading.Thread(name='t2', target=printThreadName2)
t3 = threading.Thread(name='t3', target=printThreadName3)
t1.start()
t2.start()
t3.start()

运行之后,效果如下:

可以看到,博主这里将t1设置为守护线程,但是没有看到t1结束主程序就结束了。如果需要等待一个守护线程完成工作,可以增加join()函数。

t1 = threading.Thread(name='t1', target=printThreadName1, daemon=True)
t2 = threading.Thread(name='t2', target=printThreadName2, daemon=True)
t3 = threading.Thread(name='t3', target=printThreadName3, daemon=True)
t1.start()
t2.start()
t3.start()
t1.join()
t2.join()
t3.join()

输出结果就不展示了,与前文非守护的线程一样。

需要注意的是,join()函数会无限阻塞,直到线程任务结束。当然,如果想设置一个最大的等待时间,超过时间就不在等待也行。

示例如下:

t1 = threading.Thread(name='t1', target=printThreadName1, daemon=True)
t2 = threading.Thread(name='t2', target=printThreadName2, daemon=True)
t3 = threading.Thread(name='t3', target=printThreadName3, daemon=True)
t1.start()
t2.start()
t3.start()
t1.join(0.4)
print(t1.is_alive())
t2.join()
t3.join()

这里,设置了t1等待最长时间为0.4秒,读者可以将t1线程的运行时间增加到1秒,看看其效果,is_alive()的意义是等待了0.4秒后,t1是否还在运行,为True代表是的,如下所示:

需要注意的是join()可能会造成死锁。比如现在有3个线程t1,t2,t3,t1需要使用资源12,t2需要使用资源23,t3需要使用资源13,3个同时运行。开始时,t1在使用资源1等待资源2,t2在使用资源2等待资源3,t3在使用资源3等待资源1,它们互相等待对方释放资源,但都不释放,导致循环等待下去,形成死锁。(与主线程等待线程结束差不多的道理)

自定义线程

从上面的所有线程运行,想必读者肯定发现了一个问题,那就是threading.Thread无法提供返回值。而实际的多线程运行中,往往我们都是获取网络数据,然后再处理,所以必须获取其处理的结果。

这个时候,自定义线程就能实现该需求,示例如下:

import threading
import requests

class GetHTMLThread(threading.Thread):
    def __init__(self, args, kwargs):
        super(GetHTMLThread, self).__init__()
        self.args = args
        self.kwargs = kwargs

    def run(self):
        print(self.args)
        self.result = requests.get(url=self.kwargs['url'])

    def get_result(self):
        return self.result.text

t = GetHTMLThread(args=(1,), kwargs={'url': "https://www.baidu.com"})
t.start()
t.join()
print(t.get_result())

运行之后,效果如下:

定时器线程

在我们使用django搭建服务器时,往往有许多的延时触发任务。比如我们CSDN就是我们搭建的博客网站,那么我们定时更新博客就是一个延时任务,我昨天写完博客,希望明天下午18点发送,那么这个延时任务就是中间的时差。

那么,此时我们使用定时器线程往往效果更好。示例如下:

import threading

def getWeather():
    print("更新博客")

t1 = threading.Timer(0.3, getWeather)
t1.setName('t1')
t2 = threading.Timer(0.3, getWeather)
t2.setName('t2')

t1.start()
t2.start()
t2.cancel()

运行之后,效果如下:

这里会延迟0.3秒执行线程,而之所以t2线程没有执行,是因为我们调用了cancel()函数,取消了该线程的执行。

线程间传送信号

尽管使用多线程的目的是并发地运行单独的任务,但有时候也需要在多个线程间同步操作。而Python中,线程的通信方法是事件对象。

Event管理一个内部标志,调用者可以用set()和clear()方法控制这个标志。其他线程可以使用wait()暂停,直到这个标志被设置,可以有效的阻塞进程直到允许这些线程继续。

import threading
import time

def wait_for_event(e):
    print("wait_for_event")
    event_is_set = e.wait()
    print("wait_for_event", event_is_set)

def wait_for_event_timeout(e, t):
    while not e.is_set():
        print("wait_for_event_timeout")
        event_is_set = e.wait(t)
        print("wait_for_event_timeout", event_is_set)
        if event_is_set:
            print("运行任务")
        else:
            print('运行其他任务')

e = threading.Event()
t1 = threading.Thread(
    name='t1',
    target=wait_for_event,
    args=(e,)
)
t1.start()
t2 = threading.Thread(
    name='t2',
    target=wait_for_event_timeout,
    args=(e, 2)
)
t2.start()
time.sleep(5)
e.set()

运行之后,效果如下:

这个例子中,wait_for_event_timeout()函数将检查事件状态而不会无限阻塞。wait_for_event()在wait()调用的位置阻塞,事件状态改变之前它不会返回。

因为后面的线程知识还有很多,但这篇博文已经很长了,所以接下来的锁,释放锁等知识,将在下一个threading库章节讲解。