使用threading模块中的Thread类即可创建一个线程,Thread类中有一个target参数,需要指定一个函数,将此线程执行的时候,这个代码就会被执行,示例代码如下:
import threading
import time
def coding():
for i in range(3):
print(i)
time.sleep(1)
def drawing():
for i in range(4, 7):
print(i)
time.sleep(1)
def multi_thread():
thr1 = threading.Thread(target=coding)
thr2 = threading.Thread(target=drawing)
thr1.start()
thr2.start()
if __name__ == '__main__':
multi_thread()
import threading
import time
class coding(threading.Thread):
def run(self) -> None:
for i in range(3):
print(i)
time.sleep(1)
class drawing(threading.Thread):
def run(self) -> None:
for i in range(4, 7):
print(i)
time.sleep(1)
def multi_thread():
th1 = coding()
th2 = drawing()
th1.start()
th2.start()
if __name__ == '__main__':
multi_thread()
多线程都是在同一个进程中运行的,因此在进程中的全局变量所有线程都是共享的,这就造成了一个问题,因为线程执行的顺序是无序的,有可能会造成数据错误,比如以下代码:
import threading
x = 0
def add():
# 如果在函数中修改全局变量,需要使用关键字global进行申明
global x
for i in range(10000000):
x += 1
print(x)
def main():
for i in range(2):
th = threading.Thread(target=add)
th.start()
if __name__ == '__main__':
main()
按照正常情况,最后的结果应该是10000000和20000000,但是因为多线程运行的不确定性,导致最后的结果也会是随机的
为了解决共享全局变量的问题,threading提供了一个Lock类,这个类可以在某个线程访问某个变量的时候加锁,其他线程此时就不能进来,知道当前线程处理完后,把锁释放了,其他线程才能进来处理,示例代码如下:
import threading
x = 0
glock = threading.Lock() # 创建一个锁
def add():
global x
glock.acquire() # 上锁
for i in range(10000000):
x += 1
print(x)
glock.release() # 释放锁
def main():
for i in range(2):
th = threading.Thread(target=add)
th.start()
if __name__ == '__main__':
main()
结果如下:
使用锁的原则:
(1)把尽量少的和不耗时的代码放到锁中执行
(2)代码执行完成后记得释放锁
生产者和消费者模式是多线程开发中经常见到的一种模式。生产者的线程专门用来生成一些数据,然后存放到一个中间变量中。消费者再从这个中间变量中取出数据进行消费。通过生产者和消费者模式,可以让代码达到高内聚低耦合的目标,程序分工更加明确,线程更加方便管理
生产者和消费者因为要使用中间变量,而中间变量经常是一些全局变量,因此需要使用锁在保证数据完整性
生产者和消费者模式模拟的示例代码如下:
import threading
import random
import time
# 创建中间变量
moneys = 0
glock = threading.Lock() # 创建lock锁
# 全局变量,记录生产者的生产次数
times = 0
# 创建生产者
class producer(threading.Thread):
def run(self) -> None:
global moneys
global times
while 1:
glock.acquire()
# 加条件,不然生产者会一直生产下去,同时退出循环前一定要解锁,不然会一直卡在这里重复退出
if times >= 10:
glock.release()
break
money = random.randint(0, 100)
moneys += money
times += 1
print("%s生产了%d元" % (threading.current_thread().name, money))
time.sleep(1)
glock.release()
# 创建消费者
class consumer(threading.Thread):
def run(self) -> None:
global moneys
while 1:
glock.acquire()
money = random.randint(0, 100)
# 当钱不够且生产者不再生产时退出程序,需要提前释放锁
if moneys < money and times >= 10:
glock.release()
break
if moneys >= money:
moneys -= money
print("%s消费了%d元" % (threading.current_thread().name, money))
else:
print("消费者想消费%d元,但是余额只有%d元" % (money, moneys))
time.sleep(1)
glock.release()
def main():
# 创建5个生产者
for i in range(5):
th = producer(name="生产者%s号" % i)
th.start()
# 创建5个消费者
for i in range(5):
th = consumer(name="消费者%s号" % i)
th.start()
if __name__ == '__main__':
main()
Lock版的生产者和消费者模式可以正常运行。但是存在一个不足之处,在消费者中,总是通过while 1死循环并且上锁的方式去判断钱够不够。上锁是一个很耗费CPU资源的行为,因此这种方式不是最好的。还有一种更好的方式便是使用threading.Condition来实现。threading.Condition可以在没有数据的时候处于阻塞等待状态,一旦有合适的数据了,还可以使用notify相关的函数来通知其他处于等待状态的线程。这样就可以不用做一些无用的上锁和解锁的动作。可以提高程序的性能。
首先对threading.Condition相关的函数做个介绍,threading.Condition类似threading.Lock,可以在修改全局数据的时候进行上锁,也可以在修改完毕后进行解锁。以下将一些常用的函数做个简单的介绍:
(1)acquire:上锁
(2)release:解锁
(3)wait:将当前线程处于等待状态,并且会释放锁,可以被其他线程使用notify和notify_all函数唤醒。被唤醒后会继续等待上锁,上锁后继续执行下面的代码
(4)notify:通知某个正在等待的线程,默认是第一个等待的线程
(5)notify_all:通知所有正在等待的线程。notify和notify_all不会释放锁。并且需要在release之前调用
以下是对模拟lock版的生产者和消费者模式代码的改进:
import threading
import random
import time
# 全局变量,也是中间变量
moneys = 0
# 创建一个Condition对象
condition = threading.Condition()
# 全局变量,记录生产者生产的次数
times = 0
class producer(threading.Thread):
def run(self) -> None:
global moneys
global times
while 1:
condition.acquire() # 上锁
# 判断语句,当生产者生产10次之后将不再生产
if times >= 10:
condition.release()
break
money = random.randint(0, 100)
moneys += money
times += 1
print("%s生产了%d元" % (threading.current_thread().name, money))
# 通知所有正在等待状态的线程,且condition.notify_all()不会释放锁,所以必须在释放之前使用
condition.notify_all()
time.sleep(0.5)
condition.release() #解锁
class consumer(threading.Thread):
def run(self) -> None:
global moneys
while 1:
condition.acquire() #上锁
money = random.randint(0, 100)
while moneys < money:
if times >= 10:
print("消费者想消费%d元,但是余额只有%d元,并且生产者不再生产金钱了" % (money, moneys))
condition.release()
return
print("消费者想消费%d元,但是余额只有%d元" % (money, moneys))
# 当金钱不够的时候,让该线程处于等待状态
condition.wait()
moneys -= money
print("%s消费了%d元,剩下%d元" % (threading.current_thread().name, money, moneys))
time.sleep(0.5)
condition.release() #解锁
def main():
# 创建5个生产者
for i in range(5):
th = producer(name="生产者%s号" % i)
th.start()
# 创建5个消费者
for i in range(5):
th = consumer(name="消费者%s号" % i)
th.start()
if __name__ == '__main__':
main()
在线程中,访问一些全局变量,加锁是一个经常的过程。如果你想要把一些数据存储到某个队列中,那么Python内置了一个线程安全的模块叫queue模块。Python中的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列Queue,LIFO(后入先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能够在多线程中直接使用,可以使用队列来实现线程间的同步。相关的函数如下:
初始化Queue(maxsize):创建一个先进先出的队列
(1)qsize():放回队列的大小
(2)empty():判断队列是否为空
(3)full():判断队列是否满了
(4)get():从队列中取最后一个数据(当队列已空的时候,队列会处于阻塞等待状态,直到可以有新的元素添加进队列,因此可以加上block=False使它处于非阻塞状态,如果关掉阻塞,当队列已空再想往里取出元素的话,程序会报异常)
(5)put():将一个数据放到队列中(当队列已满的时候,队列会处于阻塞等待状态,直到可以放进新的元素,因此可以加上block=False使它处于非阻塞状态,当队列已满再想往里添加元素的话,程序会报异常)
示例代码如下:
from queue import Queue
import random
import threading
import time
def add_value(q):
while 1:
q.put(random.randint(0, 10))
time.sleep(1)
def get_value(q):
while 1:
print(q.get())
def main():
q = Queue(10)
th1 = threading.Thread(target=add_value, args=[q])
th2 = threading.Thread(target=get_value, args=[q])
th1.start()
th2.start()
Python自带的解释器为CPython,Cpython解释器的多线程实际上是一个假的多线程(在多核CPU中,只能利用一核,不能利用多核)。同一时刻只有一个线程在执行,为了保证同一时刻只有一个线程在执行,在CPython解释器中有一个东西叫GIL(全局解释器锁),因为CPython解释器的内存管理不是线程安全的,所以这个解释器锁是有必要的。(当然除了CPython外还有其他的解释器,而有些解释器是没有GIL的,比如Jython、IronPython等等)
GIL虽然是一个假的多线程,但是在处理一些IO操作(比如文件读写和网络请求)还是可以在很大程度上提高效率的,在IO操作上建议使用多线程提高效率。在一些CPU计算操作上不建议使用多线程,而建议使用多进程
GIL只是保证全局同一时刻只有一个线程在执行,但是它并不能保证执行代码的原子性。也就是这一个操作可能会被分成几部分来完成,这样就会导致数据出现问题,所以需要Lock锁来保证操作的原子性
在一般的程序中,GIL锁并不会带来什么影响,但是如果在进行一些例如矩阵乘法等数学计算的程序中,GIL锁还是会存在一定的影响
import queue
import requests
from urllib import parse
import os
from urllib import request
import threading
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36",
"referer": "https://pvp.qq.com/"
}
class producer(threading.Thread):
def __init__(self, page_queue, image_queue, *args, **kwargs):
super(producer, self).__init__(*args, **kwargs)
self.page_queue = page_queue
self.image_queue = image_queue
def run(self) -> None:
while not self.page_queue.empty():
page_url = self.page_queue.get()
resp = requests.get(page_url, headers=headers)
result = resp.json()
datas = result['List']
for data in datas:
sProdImgNo_8 = parse.unquote(data['sProdImgNo_8']).replace("200", "0")
name = parse.unquote(data["sProdName"]).replace("1:1", "").strip()
self.image_queue.put({"sProdImgNo_8": sProdImgNo_8, "name": name})
class consumer(threading.Thread):
def __init__(self, image_queue, *args, **kwargs):
super(consumer, self).__init__(*args, **kwargs)
self.image_queue = image_queue
def run(self) -> None:
while 1:
try:
image_obj = self.image_queue.get()
sProdImgNo_8 = image_obj.get("sProdImgNo_8")
name = image_obj.get("name")
request.urlretrieve(sProdImgNo_8, os.path.join("王者荣耀壁纸", name+".jpg"))
print("%s下载完成" % name)
except:
break
def main():
page_queue = queue.Queue(maxsize=28)
image_queue = queue.Queue(maxsize=1000)
for page in range(28):
page_url = f"https://apps.game.qq.com/cgi-bin/ams/module/ishow/V1.0/query/workList_inc.cgi?activityId=2735&sVerifyCode=ABCD&sDataType=JSON&iListNum=20&totalpage=0&page={page}&iOrder=0&iSortNumClose=1&iAMSActivityId=51991&_everyRead=true&iTypeId=2&iFlowId=267733&iActId=2735&iModuleId=2735&_=1647064214252"
page_queue.put(page_url)
# 创建3个生产者
for i in range(3):
th = producer(page_queue, image_queue)
th.start()
# 创建5个消费者
for x in range(5):
th = consumer(image_queue)
th.start()
if __name__ == '__main__':
main()
本篇的内容到这里差不多就结束啦!
希望能让你们对多线程以及一些相关的知识有一个基本的理解
大家如果对一些知识还是不太理解也可以通过多实践来学习,实践是检验真理的唯一标准!希望能和大家一起互勉
希望大家给个点赞关注收藏三连~