给程序提速 | 多进程与多线程

目录

一、背景

1.1、前言

1.2、说明

二、线程与进程

2.1、什么是进程

2.2、什么是线程

2.3、进程与线程的关系

2.4、多进程与多线程的最佳使用条件

2.5、线程与进程的锁

2.6、特别注意

三、第一个线程、线程池

3.1、线程测试

3.2、执行结果

3.3、线程池测试

3.4、执行结果

四、线程池批量下载美妞图片

4.1、步骤

4.2、完整代码

4.3、结果

五、一些线程函数

5.1、start()函数

5.2、join()函数

5.3、setDaemon()函数

5.4、Lock()函数、RLock()函数、acquire()函数与release()函数

六、总结


一、背景

1.1、前言

在爬虫获取资源的过程中,有时需要批量下载资源,但一次次的发起请求在下载速度实在太慢,需要提高获取资源的速度。那么线程与进程的知识就必不可少,下面对进程与线程进行学习并结合实战提高知识掌握程度。

1.2、说明

操作系统:win 10

编辑器:pycharm edu

语言及版本:python 3.10

使用的库:ThreadPoolExecutor、requests、BeautifulSoup

实现思路:写出单线程的代码,在把任务提交到线程池或者进程池即可

说明:下文不使用类的继承方法重写run函数,进行线程的运行

tips:以下代码url不会放真实的,移植测试注意识别更改

二、线程与进程

2.1、什么是进程

进程(运行中的程序)。每次执行一个程序,操作系统自动为该程序分配资源(如内存分配,创建一个可执行线程)。

2.2、什么是线程

线程(在运行中的程序内),是进程中的实际运作单位。可以直接被CPU调度的执行过程,是操作系统中可以进行运算调度的最小单位。

注意:不同进程之间的内存,默认不允许其它进程进行使用;

2.3、进程与线程的关系

进程是资源单位,线程是执行这些资源的运行单位。

小例子:进程是银行开设的某个业务,线程是办理这些业务的具体窗口。

关键词:资源单位、执行单位

2.4、多进程与多线程的最佳使用条件

1)什么时候使用多进程比较好?

计算密集型,使用多进程,如:大量的数据计算

各个部分相互独立,很少产生交集;

例子:代理池

1)获取到IP

2)验证IP是否可用

3)对外提供接口

以上三个步骤,相互独立,几乎不产生交集,使用多进程的效果更好

2)什么时候使用多线程比较好

I/O密集型,使用多线程,如:文件读写、网络数据传输(下载视频);

各个部分完成过程相似;

例子:批量获取图片、音乐、菜价等

1)获取内容url

2)发起请求

3)下载保存

以上的获取图片,音乐等步骤都是一致的,都是获取到该内容的url,然后对url发起请求,在下载保存,高度相似,使用多线程的效果更好。

2.5、线程与进程的锁

1)GIL锁

全局解释锁,是CPython解释器特有的,让一个进程中同一时刻只能有一个线程可以被CPU调用。(注意,此时如果有多个线程,那么CPU会轮流切换到不同的线程进行运行(不管当前线程是否结束),保证了线程的唯一运行性);

2)线程安全

单依靠线程的GIL锁数据是不安全的,所以要为线程申请锁,存在以下两种情况:

情况一:当多个线程同时申请一把锁时,第一个申请到锁的线程可以完整执行当前线程的程序,在释放所后;第二个申请到锁的线程继续执行当前线程的程序(此时其它没申请到锁的线程是阻塞状态);

情况二:多个线程申请不同的锁时,第一个申请到锁的线程开始执行,其它线程是阻塞状态,然后轮到第二个申请到锁的线程进行执行,直到全部执行结束;

3)死锁

出现原因:由于线程间竞争资源或者由于彼此通信而造成的一种阻塞现象;

结果:此时程序会卡着不动,一动不动;

例子一:模块1给申请了锁1,模块1申请了锁2,过了一会,模块1又跑去申请锁2(此时锁1、2都没有释放),或者模块2又跑去申请锁1,此时就会出现死锁。

2.6、特别注意

使用多线程、多进程、携程、进程池、线程池等方法提高程序运行速度时,需要考虑访问网站的承受能力,避免高速访问造成攻击。

三、第一个线程、线程池

3.1、线程测试

from threading import Thread  # 导入线程库


def function1(name):  # 函数1
    for i in range(1, 500):
        print(name, i)


def function2(name):  # 函数2
    for j in range(1, 500):
        print(name, j)


if __name__ == '__main__':
    # 创建线程并设置参数,target是执行的函数,args是传参并且传递是参数规定是元组,
    t1 = Thread(target=function1, args=("海绵宝宝",))  
    t2 = Thread(target=function2, args=("派大星",))
    t1.start()  # 开启线程
    t2.start()
    for k in range(1, 500):  
        print("我们去抓水母吧", k)

3.2、执行结果

可以看到多个函数并发运行输出结果,说明此方法可行。

如下图1:

 图1

3.3、线程池测试

from concurrent.futures import ThreadPoolExecutor  # 线程池库


def function(name):  # 测试函数
    print("我是", name)
    return name


def get_return(res):  # 拿返回结果函数
    print(res.result())


if __name__ == '__main__':
    with ThreadPoolExecutor(3) as t:  # 分配三个线程池,并且命名为t
        # 线程池t提交,添加返回绑定函数
        t.submit(function, "海绵宝宝").add_done_callback(get_return)
        t.submit(function, "派大星").add_done_callback(get_return)
        t.submit(function, "章鱼哥").add_done_callback(get_return)

3.4、执行结果

如下图2:

 图2

说明:由于进程与进程池的代码与线程的高度相似,只是导入的包不同,所以对该部分内容进行了跳过;

四、线程池批量下载美妞图片

4.1、步骤

1)请求到合集url

2)拿到每一张图片跳转url

3)拿到每一张图片下载地址

4)线程池执行

4.2、完整代码

代码需要注意的是:我们请求的url只有4个,线程池里的线程数量最好不要大于4,因为只有4个任务,你给多了线程也没啥用。

"""
    批量下载美妞图片实现思路
    1、

"""

from concurrent.futures import ThreadPoolExecutor
import requests
from bs4 import BeautifulSoup


def get_img_url():
    # all_url = "https://www.yeitu.com/meinv/siwameitui/2.html"

    for x in range(2, 6):
        url = "https://www.yeitu.com/meinv/siwameitui/{}.html".format(x)
        resp = requests.get(url, headers=header)
        resp.encoding = "utf-8"
        # print(resp.status_code)
        result = BeautifulSoup(resp.text, "html.parser")
        li_list = result.find("div", class_="list-box-p").find("ul").find_all("li")
        o = 0
        for i in li_list:
            src = i.find("a").get("href")
            two_resp = requests.get(src, headers=header)
            #  print(two_resp.status_code)
            two_resp.encoding = "utf-8"
            two_result = BeautifulSoup(two_resp.text, "html.parser")
            img_list = two_result.find("div", class_="img_box").find("a")

            for j in img_list:
                o += 1
                all_url = j.get("src")
                three_resp = requests.get(all_url, headers=header)
                name = all_url.split("/")[-1]
                #  print(three_resp.status_code)
                img_name = "{}".format(name)
                with open("tupian/"+img_name, mode="wb") as f:
                    f.write(three_resp.content)
                    print("图片{}下载完成".format(name))


if __name__ == '__main__':
    header = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0'
    }
    with ThreadPoolExecutor(3) as t:
        t.submit(get_img_url)




4.3、结果

可以看到下载的图片整整齐齐的保存到了文件中,并且下载速度很快,这就是线程池的威力。

五、一些线程函数

5.1、start()函数

用法:线程对象.start()

含义:表示子线程已经准备好了,CPU可以开始调度该子线程

5.2、join()函数

用法:线程对象.join()

含义:等待该线程执行结束后,程序才往后继续执行

5.3、setDaemon()函数

用法:线程对象.setDaemon(布尔值),守护线程(必须放置start之前)

含义:布尔值为True,则设置为守护线程,主线程执行完毕后,子线程也自动关闭;

布尔值为False,则设置为非守护线程,主线程等待子线程,子线程执行完毕后,主线程才结束;

注意:非守护线程是默认值

5.4、Lock()函数、RLock()函数、acquire()函数与release()函数

用法:线程对象.Lock()、线程对象.RLock()、线程对象.acquire()、线程对象.release()

含义:创建同步锁、创建递归锁、申请锁、释放锁

当线程申请到了锁时,其它线程就会处于阻塞状态,等当前线程执行结束并释放锁后,由第二个申请到锁的线程执行,如此反复;

说明:RLock可以多次申请锁和多次释放、Lock不支持

六、总结

难点:

1)概念多;

2)方法多;

优点:

1)快速提高程序运行速度;

2)代码上手速度快;

总结:

1)多学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值