多线程简介
多线程是Python中实现并发编程的重要方式之一,它允许程序在同一时间内执行多个任务。在某些环境中使用多线程可以加快我们代码的执行速度,例如我们通过爬虫获得了一个图片的url数组,但是如果我们一个一个存储很明显会非常缓慢,如果我们多创建几个线程执行这样我们的代码就会快上若干倍,同时在之前的实验中我们做了一个socket的聊天室如果它是单线程的话,就只能进行一问一答,于是我们创建了两个线程,一个线程用来监听对方的信息,另一个线程用来发送消息。
多线程编程
使用threading库创建线程并使用
接下来使用一个小代码来理解一下线程的概念。
from threading import Thread
def print_name(name):
for i in range(100):
print(f'name:{name},num:{i}')
def main():
t1 = Thread(target=print_name, args=('张三',))
t2 = Thread(target=print_name, args=('李四',))
t1.start()
t2.start()
t1.join()
t2.join()
if __name__ == '__main__':
main()
在代码中Thread类代表的是线程,我们定义了一个函数print_name作为任务,传入参数name代表名字,我们在函数中使用循环,让名字重复打印100次,同时显示第几次打印。在主函数中创建了两个线程t1和t2,再同时启动它们,join代表等待任务完成。
在结果中我们发现,输出的张三和李四是掺杂在一起的,于是我们可以想象到它的执行过程应该是这样的:
t1
主进程 ---------------------------------->
----------------------------> -------------------------------------->
---------------------------------->
t2
但是,我们思考一下,即使是输出乱套了,它依然是按照张三……李四……一条一条地执行print语句,输出数据的。而不是输出张李三四……的数据。
这是因为在计算机中,打印资源为互斥资源,在访问的过程中需要互斥的访问,就是有的进程一旦占用了该资源,就必须等待它结束或者中断释放以后,别的进程才可以使用。
睡眠排序
睡眠排序作为一种最没用的排序方式,虽然我们在实操的过程中根本就用不到这种方法。但是作为一个小练习,让我们对多线程的理解更加深刻,也是不错的。
from threading import Thread
import time
def act(num):
time.sleep(num/10)
print(num)
def main():
c = [5,4,2,9,8,7,6,1,3]
for i in range(len(c)):
t = Thread(target=act,args=(c[i],))
t.start()
if __name__ == '__main__':
main()
这个代码的思路是,把要排序的数组进行遍历,通过在循环中给每一个数值创建线程再执行。在线程中,规定先睡眠,根据传入的数字的大小决定谁先醒来,这样先醒来的线程就会先输出,所以先输出的就是较小的数。最后数字会按照从小到大的顺序输出。这种思路和传统的排序相比较它会把压力全部给到cpu。
线程池
from concurrent.futures import ThreadPoolExecutor
def func(a):
for i in range(10):
print(a)
def main():
with ThreadPoolExecutor(10) as t:
t.submit(func,'1')
t.submit(func,'2')
t.submit(func,'3')
t.submit(func,'4')
t.submit(func,'5')
t.submit(func,'6')
if __name__ == '__main__':
main()
在ThreadPoolExecutor(10)中传入的参数是线程池中可以容纳的最大的线程的数量,这个数值和电脑的核数量有关,在一般情况下,推荐线程数 ≈ CPU核心数 + 1。
至于电脑的cpu是几核的,可以通过下面的代码查看。
锁🔒
锁(Lock)是多线程编程中最基本的同步机制,用于解决多线程环境下对共享资源访问的竞争问题,防止数据不一致的情况发生。多个线程同时访问/修改共享数据时,执行结果依赖于线程执行的顺序,如果进程之间有资源竞争的情况,那么就可能会造成资源的混乱。
例如count+=1和读写文件的操作。
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock:
counter += 1
threads = []
for _ in range(5):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"counter: {counter}")
但是在使用锁的过程中,一定要注意简单的锁不能重复使用,否则会导致进程无法被唤醒造成死锁。
异步编程介绍
异步编程和多线程编程是现代软件开发中两种主要的并发处理方式,它们各有特点,适用于不同的场景。异步编程核心概念是任务发起后不必等待完成,可以继续执行其他操作,核心调度机制,管理所有异步任务的执行,所有任务在一个线程内交替执行,任务主动让出控制权。
使用async和await关键字
async
用于声明一个函数为异步函数(协程函数),被修饰的函数在被调用时会返回一个协程对象,而不是立即执行。
await
暂停当前协程的执行,等待一个可等待对象完成,非阻塞地等待。
import asyncio
async def fetch_data(n):
print(f"开始获取数据{n}")
await asyncio.sleep(2)
print(f"数据获取完成{n}")
async def main():
task1 = asyncio.create_task(fetch_data(1))
task2 = asyncio.create_task(fetch_data(2))
await task1
await task2
asyncio.run(main())
它的运行结果是
在执行完task1的第一个打印后,有一个await关键字,到这里程序会暂时挂起当前的任务转而去执行其它的任务。