PythonPython

python多任务--线程

2020-10-14  本文已影响0人  小啊小狼

一、基本概念

什么是线程

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

并发和并行

并发:

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

特点

并行:

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

特点

同步和异步

同步(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条执行之后,当多个线程同时修改同一条数据时可能会出现脏数据,所以,出现了线程锁,即同一时刻允许一个线程执行操作。

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

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


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语言的原生线程。

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密集型任务时的性能效率

上一篇下一篇

猜你喜欢

热点阅读