首先,你需要知道什么是线程。线程是一个执行流,也就是说,它是一个程序中的一段代码,可以独立地运行。一个程序可以有多个线程,这样就可以同时执行多个任务。比如,你在浏览网页的时候,可能有一个线程负责显示网页内容,另一个线程负责加载图片,还有一个线程负责处理用户的输入。这样就可以提高程序的效率和用户体验。
Python中有两个模块可以用来创建和管理线程,一个是_thread模块,另一个是threading模块。_thread模块是比较低级的,功能比较简单,而threading模块是基于_thread模块的高级封装,提供了更多的功能和易用性。所以,一般来说,我们推荐使用threading模块来进行多线程编程。
要使用threading模块创建线程,有两种方法:
import threading
import time
# 定义一个函数
def print_time(thread_name, delay):
# 循环5次
for i in range(5):
# 每次循环前暂停delay秒
time.sleep(delay)
# 打印当前时间和线程名
print(thread_name, time.ctime())
# 创建两个线程对象
t1 = threading.Thread(target=print_time, args=("Thread-1", 1))
t2 = threading.Thread(target=print_time, args=("Thread-2", 2))
# 启动两个线程
t1.start()
t2.start()
# 等待两个线程结束
t1.join()
t2.join()
# 打印主线程结束
print("Main thread finished")
import threading
import time
# 定义一个类,继承自threading.Thread类
class MyThread(threading.Thread):
# 重写__init__()方法
def __init__(self, thread_name, delay):
# 调用父类的__init__()方法
threading.Thread.__init__(self)
# 初始化属性
self.thread_name = thread_name
self.delay = delay
# 重写run()方法
def run(self):
# 调用自定义的函数
print_time(self.thread_name, self.delay)
# 定义一个函数
def print_time(thread_name, delay):
# 循环5次
for i in range(5):
# 每次循环前暂停delay秒
time.sleep(delay)
# 打印当前时间和线程名
print(thread_name, time.ctime())
# 创建两个类的实例
t1 = MyThread("Thread-1", 1)
t2 = MyThread("Thread-2", 2)
# 启动两个线程
t1.start()
t2.start()
# 等待两个线程结束
t1.join()
t2.join()
# 打印主线程结束
print("Main thread finished")
以上两种方法的效果是一样的,都会创建两个子线程,并让它们分别每隔一秒和两秒打印一次当前时间和自己的名字。主线程会等待子线程都结束后再结束。
你可能会注意到,在上面的例子中,我们都使用了join()方法来等待子线程结束。这是因为如果不这样做,主线程可能会在子线程还没结束的时候就退出,导致程序异常。join()方法可以让主线程阻塞,直到子线程结束或者超时。如果不传入超时时间,就会一直等待。
在多线程编程中,有一个很重要的问题就是线程同步。线程同步是指多个线程在访问共享的资源或数据时,需要保证数据的一致性和正确性。比如,如果有两个线程都要修改一个变量的值,那么就需要保证它们不会同时修改,否则就会出现数据混乱的情况。
为了实现线程同步,Python提供了一个简单的锁对象,也叫互斥锁。锁对象有两种状态:锁定和未锁定。每当一个线程要访问共享数据时,它必须先获得锁定;如果已经有别的线程获得锁定了,那么它就必须等待;当它使用完毕后,它必须释放锁定,以便其他线程也能获得锁定。
Python的threading模块提供了一个Lock类来创建锁对象。Lock类提供了两个方法:
例如:
import threading
import time
# 创建一个全局变量
num = 0
# 创建一个锁对象
lock = threading.Lock()
# 定义一个函数
def add_num(thread_name, delta):
# 声明全局变量
global num
# 循环10次
for i in range(10):
# 尝试获得锁定
lock.acquire()
# 修改全局变量
num += delta
# 打印当前值和线程名
print(thread_name, num)
# 释放锁定
lock.release()
# 暂停0.1秒
time.sleep(0.1)
# 创建两个线程对象
t1 = threading.Thread(target=add_num, args=("Thread-1", 1))
t2 = threading.Thread(target=add_num, args=("Thread-2", -1))
# 启动两个线程
t1.start()
t2.start()
# 等待两个线程结束
t1.join()
t2.join()
# 打印主线程结束和最终值
print("Main thread finished")
print("Final num:", num)
以上程序会创建两个子线程,分别对一个全局变量num进行加一和减一的操作,并打印出每次修改后的值。由于使用了锁对象来保证每次只有一个线程能够修改num,所以最终的结果是num始终为0。
如果不使用锁对象,那么就可能出现这样的情况:假设num初始为0,然后Thread-1执行了num += 1,此时num为1;但是在Thread-1打印之前,Thread-2执行了num -= 1,此时num又变成了0;然后Thread-1打印出num为0,而不是预期的1;接着Thread-2也打印出num为0。这样就造成了数据的不一致性。
当然,并不是所有的情况都需要使用锁对象来实现线程同步。有些情况下,我们可以利用Python的特性来避免数据竞争。比如,Python中的队列(queue)模块提供了一个先进先出(FIFO)的数据结构,它可以安全地在多个线程之间共享数据。队列模块提供了两个方法:
例如:
import threading
import queue
import time
import random
# 创建一个队列对象
q = queue.Queue()
# 定义一个生产者函数
def producer(thread_name):
# 循环10次
for i in range(10):
# 生成一个随机数
item = random.randint(1, 100)
# 将随机数放入队列
q.put(item)
# 打印生产者名字和随机数
print(thread_name, "produced", item)
# 暂停0.5秒
time.sleep(0.5)
# 定义一个消费者函数
def consumer(thread_name):
# 循环10次
for i in range(10):
# 从队列中取出一个元素
item = q.get()
# 打印消费者名字和元素
print(thread_name, "consumed", item)
# 暂停1秒
time.sleep(1)
# 创建两个线程对象,分别是生产者和消费者
t1 = threading.Thread(target=producer, args=("Producer-1",))
t2 = threading.Thread(target=consumer, args=("Consumer-1",))
# 启动两个线程
t1.start()
t2.start()
# 等待两个线程结束
t1.join()
t2.join()
# 打印主线程结束
print("Main thread finished")
以上程序会创建两个子线程,分别是生产者和消费者。生产者每隔0.5秒就会生成一个随机数,并放入队列中。消费者每隔1秒就会从队列中取出一个元素,并打印出来。由于队列是线程安全的,所以不需要使用锁对象来保证数据的一致性。