概述
Python3的多线程编程中常用的两个模块为:_thread,threading。
推荐使用threading模块。毕竟_thread模块是相当底层的模块,虽然该模块可以让你对线程进行细致的管理,但由于没有提供高级函数,所以用起来比较费劲。(轮子不如threading模块的好使)
以下内容将基于threading模块展开。
创建线程
threading模块支持两种创建线程的方式,分别是函数方式创建,以及使用类来包装线程对象的方式。
""" 案例1:函数式创建线程 """
import threading
import time
# 线程执行函数
def run(num):
time.sleep(1)
print(f'i am threading{num}')
if __name__ == '__main__':
# 获取开始的时间戳
start = time.time()
# 存放线程的列表,用于阻塞线程
thread_list = list()
# 创建4个线程,并将每个创建好的线程对象放到thread_list中
for i in range(1, 5):
# 创建线程
thread = threading.Thread(target=run, args=(i,))
# 启动该线程
thread.start()
thread_list.append(thread)
# 只有线程全部结束,再向下执行
for j in thread_list:
j.join()
# 结束的时间戳
end = time.time()
# 打印该程序运行了几秒
print(end-start)
# i am threading1
# i am threading3
# i am threading2
# i am threading4
# 1.0026652812957764
函数式创建线程是非常简单的,只需要将实例化Thread对象就可以了。
target参数为线程的执行函数,args参数是一个元组,里面可以存放该执行函数的参数,没有可以省略。
需要注意的是,args元组参数里的逗号,千万不要忘。
""" 案例2:类继承创建线程 """
import threading
import time
# 继承Thread,转变为线程类
class MyThread(threading.Thread):
def __init__(self, threadId):
# 必须实现Thread类的init方法
super().__init__()
self.threadId = threadId
# 重写执行函数
def run(self):
time.sleep(1)
print(f'i am thread{self.threadId}')
if __name__ == '__main__':
# 获取开始的时间戳
start = time.time()
# 存放线程的列表,用于阻塞线程
thread_list = list()
# 创建4个线程,并将每个创建好的线程对象放到thread_list中
for i in range(1, 5):
thread = MyThread(i)
# 启动该线程
thread.start()
thread_list.append(thread)
# 只有线程全部结束,再向下执行
for j in thread_list:
j.join()
# 结束的时间戳
end = time.time()
# 打印该程序运行了几秒
print(end-start)
# i am thread1
# i am thread3
# i am thread4
# i am thread2
# 1.0024833679199219
一般来说,线程的执行函数是没有返回值的。但如果需要返回值,则可以通过定义一个全局变量来获取。当然,如果使用的是类继承的方式创建,则可以直接使用类属性来接收返回值。
守护线程
守护线程作用是为其他线程提供便利服务。只要当前主线程中尚存任何一个非守护线程没有结束,守护线程就全部工作,只有当最后一个非守护线程结束,守护线程随着主线程一同结束工作。
""" 案例3:守护线程 """
import threading
import time
def run():
# 无限循环打印
while True:
time.sleep(0.3)
print('i am alive!')
def fly():
# 打印3次
for i in range(3):
time.sleep(0.3)
print('you alive!')
print('you die...')
if __name__ == '__main__':
# 创建线程1,执行函数为run
thread1 = threading.Thread(target=run)
# 创建线程2,执行函数为fly
thread2 = threading.Thread(target=fly)
# 将线程1设置为守护线程
thread1.daemon = True
# 启动这2个线程
thread1.start()
thread2.start()
# you alive!
# i am alive!
# you alive!
# i am alive!
# i am alive!
# you alive!
# you die...
由上述案例可看出,多线程任务中,如果子线程不是守护线程,主线程运行完毕后会进入停止状态,但是主线程的占用资源没有释放(程序不会退出),一直到所有的子线程全部执行完成后才会释放资源,退出程序。
另外,需要注意,设置守护线程时一定要在start方法之前设置。
threading模块的常用方法
关于这些方法,值得一提的是,threading模块仍然支持 Python 2.x 的以驼峰命名实现的方法,不过笔者还是建议使用下划线的方法。(当然,如果是为了兼容 Python 2.x ,那就请忽略笔者的建议。)
# 启动线程
threading.start()
# 等待至该线程中止,可以达到资源独占的作用
threading.join([time])
# 返回线程是否存活的
threading.is_alive()
# 返回当前的Thread对象
threading.current_thread()
# 设置守护线程
threading.setDaemon(True)
# 代表线程活动的方法,可以在子类型里重载
threading.run()
# 返回一个包含正在运行的线程的list
threading.enumerate()
# 返回正在运行的线程数量,等价于len(threading.enumerate())
threading.active_count()
# 设置线程名
threading.setName()
# 返回线程名
threading.getName()
锁对象与共享变量
多线程同时访问同一变量时,会产生共享变量的问题,造成变量冲突。也就是当线程需要共享数据时,可能存在数据不同步的问题。这时候可以使用threading.Lock来把线程中的变量锁定,使用完再释放。
# 创建锁对象
lock = threading.Lock()
# 获取锁,用于线程同步
lock.acquire()
# 释放锁,开启下一个线程
lock.release()
具体使用可以参考下列案例。
""" 案例4:线程锁 """
import threading
def func():
# 引用全局变量num
global num
# 为num循环加1
for i in range(1, 1000001):
# 为下面1行代码上锁
lock.acquire()
num += 1
# 解锁上1行代码
lock.release()
# threading.current_thread().name为打印线程名
print(threading.current_thread().name, num)
if __name__ == '__main__':
# 计数君
num = 0
# 创建锁对象
lock = threading.Lock()
# 创建2个线程
thread1 = threading.Thread(target=func)
thread2 = threading.Thread(target=func)
# 为这两个Thread对象(线程)起名
thread1.name = 'thread_1'
thread2.name = 'thread_2'
# 启动这2个线程
thread1.start()
thread2.start()
# 等待线程结束
thread1.join()
thread2.join()
print('程序运行完毕!')
# thread_1 1655942
# thread_2 2000000
# 程序运行完毕!
本文中笔者有提到,可以通过使用全局变量来接收返回值。案例4的代码就相当于是使用了num接收函数func的返回值,即num加1000000的值。
然后,这个案例对线程锁的意义在哪?实际上,当range里的数字比较小时,对程序本身的影响并不大。然而,当range里的数字过大时,也就是代表计算密集度非常大时,就会出现数据的混乱。
""" 去掉线程锁时的打印结果 """
# thread_1 1101612
# thread_2 1367565
# 程序运行完毕!
由上述案例便可看出,线程锁的重要性。当然,可能有些读者会问,为什么案例4第一条打印的是1655942而不是1000000?其实呢,只需要在多线程的角度思考一下就可以相通了。
最后还需要提一下,线程锁不仅仅只能通过threading.Lock()创建,还可以使用threading.RLock()多重锁,用法和threading.Lock()相同。因为threading.Lock()在线程中必须等待锁释放后(release)才能再次上锁。而threading.RLock在同一线程中可用被多次acquire。需要注意的是,在threading.RLock中acquire和release必须成对出现。
死锁
在线程共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,这种情况便会造成死锁。
在Python中的死锁一般都是由于threading.Lock的acquire使用不合理导致的。情景就类似于哲学家吃饭的问题:由于服务员的疏忽,导致2个哲学家手中分别拿到的是2把叉子和2把餐刀,其中一个哲学家说,你给我叉子,我给你餐刀,另一个哲学家则说,你给我餐刀,我给你叉子......
""" 案例5:死锁 """
import threading
import time
# 哲学家a
def philosopher_a():
if lock_b.acquire():
print('我有餐刀')
print('给我叉子')
time.sleep(0.5)
if lock_a.acquire():
print('我给你餐刀')
lock_a.release()
lock_b.release()
# 哲学家b
def philosopher_b():
if lock_a.acquire():
print('我有叉子')
print('给我餐刀')
time.sleep(0.5)
if lock_b.acquire():
print('我给你叉子')
lock_b.release()
lock_a.release()
if __name__ == "__main__":
# 创建2个锁对象
lock_a = threading.Lock()
lock_b = threading.Lock()
# 创建2个线程
thread1 = threading.Thread(target=philosopher_a)
thread2 = threading.Thread(target=philosopher_b)
# 启动这2个线程
thread1.start()
thread2.start()
上述代码一旦运行,就会使程序挂死,原理很简单,就是2个锁在互相等待对方解锁。你不解锁,我不解锁,那就停住咯。是不是像极了你和女朋友吵架,你不道歉,我不道歉,那就谁也不理谁咯。
所以说,在程序设计中,一定要严格分析锁定的数据,避免死锁的问题,将死锁扼杀在摇篮里才是最安全的。