python 多任务_python多任务--线程

一、基本概念

什么是线程

线程(Thread)也叫轻量级进程,是操作系统能够进行运算调度的最小单位,它被包涵在进程之中,是进程中的实际运作单位。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。

并发和并行

并发:

指的是任务数多于cpu核数,通过操作系统的任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,为交替执行的状态,因为切换任务的速度相当快,看上去一起执行而已)

特点

微观角度:所有的并发处理都有排队等候,唤醒,执行等这样的步骤,在微观上他们都是序列被处理的,如果是同一时刻到达的请求(或线程)也会根据优先级的不同,而先后进入队列排队等候执行。

宏观角度:多个几乎同时到达的请求(或线程)在宏观上看就像是同时在被处理。

d0768fae939b

image.png

并行:

指的是任务数小于等于cpu核数,即任务真的是一起执行的

特点

同一时刻发生,同时执行。

不存在像并发那样竞争,等待的概念。

d0768fae939b

image.png

同步和异步

同步(synchronous): 所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。

简言之,要么成功都成功,失败都失败,两个任务的状态可以保持一致。

异步(asynchronous):所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。

为什么要使用多线程?

线程在程序中是独立的、并发的执行流。与分隔的进程相比,进程中线程之间的隔离程度要小,它们共享内存、文件句柄和其他进程应有的状态。

因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程之中拥有独立的内存单元,而多个线程共享内存,从而极大的提升了程序的运行效率。

线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性,多个线程共享一个进程的虚拟空间。线程的共享环境包括进程代码段、进程的共有数据等,利用这些共享的数据,线程之间很容易实现通信。

操作系统在创建进程时,必须为改进程分配独立的内存空间,并分配大量的相关资源,但创建线程则简单得多。因此,使用多线程来实现并发比使用多进程的性能高得要多。

二、线程实现

threading模块

import time

import datetime

from threading import Thread

# Thread类可以用来创建线程对象

# target:指定执行线程执行的任务(任务函数)

# args:kwargs: 接收任务函数的参数

# name:指定线程的名字

def work1(name):

for i in range(4):

time.sleep(1)

print(f'{name}浇花的第{i + 1}秒')

def work2(name):

for i in range(3):

time.sleep(1)

print(f'{name}打墙的第{i + 1}秒')

# 创建2个线程

t1 = Thread(target=work1, args=('小狼',), name="线程一")

t2 = Thread(target=work2, kwargs={"name": "liang"}, name='线程二')

# 启动线程 :异步执行的状态

begin_time = datetime.datetime.now().strftime('%H:%M:%S.%f')

print("begin_time:",begin_time)

t1.start()

t2.start()

t1.join() # 默认等待子线程1执行结束

t2.join() # 默认等待子线程2执行结束

end_time = datetime.datetime.now().strftime('%H:%M:%S.%f')

print("end_time:",end_time)

# 主线程等待子线程执行结束之后再往下执行

print("执行结束")

#输出

begin_time: 14:45:15.724900

liang打墙的第1秒

小狼浇花的第1秒

小狼浇花的第2秒

liang打墙的第2秒

liang打墙的第3秒

小狼浇花的第3秒

小狼浇花的第4秒

end_time: 14:45:19.729378

执行结束

可以看到,如果两个任务单线程分别执行,则会消耗7秒的时间,而运用多线程实现多个任务,耗费的总时间不是多个任务时间之和,而是单个运行时间最长的任务(4s)。

自定义线程

继承threading.Thread来自定义线程类,其本质是重构Thread类中的run方法,同时执行多个相同任务,就是创建多次线程对象

import time

from threading import Thread

"""

通过线程类的形式来实现多线程

"""

class MyThread(Thread):

"""自定义的线程类"""

def __init__(self, name):

super().__init__()

self.name = name

def run(self):

"""线程执行的任务函数"""

for i in range(4):

time.sleep(1)

print(f'{self.name}浇花的第{i + 1}秒')

m = MyThread('小狼')

m2 = MyThread('liang')

m.start()

m2.start()

m.join()

m2.join()

print('执行结束')

#输入

liang浇花的第1秒

小狼浇花的第1秒

liang浇花的第2秒

小狼浇花的第2秒

liang浇花的第3秒

小狼浇花的第3秒

小狼浇花的第4秒

liang浇花的第4秒

执行结束

三、多线程特点

守护线程

里使用setDaemon(True)把所有的子线程都变成了主线程的守护线程,因此当主进程结束后,子线程也会随之结束。所以当主线程结束后,整个程序就退出了。

import time

from threading import Thread

"""

通过线程类的形式来实现多线程

"""

class MyThread(Thread):

"""自定义的线程类"""

def __init__(self, name):

super().__init__()

self.name = name

def run(self):

"""线程执行的任务函数"""

for i in range(4):

time.sleep(1)

print(f'{self.name}浇花的第{i + 1}秒')

m = MyThread('小狼')

m2 = MyThread('liang')

m.setDaemon(True)

m2.setDaemon(True)

m.start()

m2.start()

我们可以发现,设置守护线程之后,当主线程结束时,子线程也将立即结束,不再执行。

主线程等待子线程结束

为了让守护线程执行结束之后,主线程再结束,我们可以使用join方法,让主线程等待子线程执行。

#此处自定义线程类代码与之前一致,省略

m = MyThread('小狼')

m2 = MyThread('liang')

m.start()

m2.start()

m.join()

m2.join()

time.sleep(2.5)

print('执行结束')

多线程共享全局变量

线程是进程的执行单元,进程是系统分配资源的最小单位,所以在同一个进程中的多线程是共享资源的。

n = 0

def work1():

global n

for i in range(10):

n += 1

def work2():

global n

for i in range(10):

n += 1

t1 = Thread(target=work1)

t2 = Thread(target=work2)

# 启动线程

t1.start()

t2.start()

t1.join()

t2.join()

print("n:", n)

#输出

n: 20

可以看出,多线程之间是共享全局变量的

互斥锁

由于线程之间是进行随机调度,并且每个线程可能只执行n条执行之后,当多个线程同时修改同一条数据时可能会出现脏数据,所以,出现了线程锁,即同一时刻允许一个线程执行操作。

由于线程之间是进行随机调度,如果有多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,我们也称此为“线程不安全”。

当把计算的数据很大,就会出现资源竞争,导致执行的结果不准确

d0768fae939b

image.png

为了方式上面情况的发生,就出现了互斥锁(Lock),在可能出现资源竞争的地方加上互斥锁,保证线程的安全

import threading

n = 100

def work1():

global n

for i in range(500000):

lock.acquire()

n += 1

lock.release()

def work2():

global n

for i in range(500000):

lock.acquire()

n += 1

lock.release()

# 创建一把锁

lock = threading.Lock()

# 上锁

t1 = threading.Thread(target=work1)

t2 = threading.Thread(target=work2)

# 启动线程

t1.start()

t2.start()

t1.join()

t2.join()

print("n:", n)

三、GIL(Global Interpreter Lock)全局解释器锁

在非python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少核,同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的。

GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。GIL只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython中是没有GIL的。

Python多线程的工作过程:

python在使用多线程的时候,调用的是c语言的原生线程。

拿到公共数据

申请GIL

python解释器调用os原生线程

os操作cpu执行运算

当该线程执行时间到后,无论运算是否已经执行完,gil都被要求释放

进而由其他进程重复上面的过程

等其他进程执行完后,又会切换到之前的线程(从他记录的上下文继续执行),整个过程是每个线程执行自己的运算,当执行时间到就进行切换(context switch)。

python中线程的缺陷,以及适用场景:

由于GIL锁的存在,python中的多线程在同一时间没办法同时执行(即没办法实现并行)

1、CPU密集型代码(各种循环处理、计算等等),在这种情况下,由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。

2、IO密集型代码(文件处理、网络爬虫等涉及文件读写的操作),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。

练习:

有10000个url地址(假设请求每个地址需要0.5秒),请设计程序一个程序,获取列表中的url地址,使用1000个线程去发送这10000个请求,计算出总耗时!

# 计算时间的装饰器

def decorator(func):

def wrapper():

# 函数执行之前获取系统时间

start_time = time.time()

func()

# 函数执行之后获取系统时间

end_time = time.time()

print('执行时间为:', end_time - start_time)

return end_time - start_time

return wrapper

#生成器生成10000个url地址

urls_g = (f"https://www.baidu.com{j}" for j in range(10000))

#定义线程类

class MyThread(threading.Thread):

#重写run方法,指定要执行的任务逻辑

def run(self):

while True:

try:

url = next(urls_g)

except StopIteration:

break

else:

print(F'{self}发送请求{url}')

time.sleep(0.5)

@decorator

def main():

t_list = []

# 创建4个线程

for i in range(1000):

t = MyThread() # 创建线程对象

t.start() # 开启该线程

t_list.append(t)

# 遍历所有线程对象,设置主线等待子线程执行完

for t in t_list:

t.join()

res = main()

最终执行时间为5.16秒,如果单线程执行的话至少需要5000多秒,体现了多线程在执行IO密集型任务时的性能效率

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值