【python】多线程

48 阅读7分钟

进程与线程:

什么是进程?

电脑中时会有很多单独运行的程序,每个程序有一个独立的进程,

而进程之间是相互独立存在的。比如电脑上开启的 Chrome、网易云音乐等等。

什么是线程?

一个可以独立运行的程序单位,它是线程的集合,进程就是由一个或多个线程构成的。

线程是进程中的实际运行单位,是操作系统进行运算调度的最小单位。 可理解为线程是进程中的一个最小运行单元。

什么是多进程?

指计算机同时执行多个进程,一般是同时运行多个软件。

什么是多线程?

首先了解一下串行、并行:

串行:

相对于单条线程来执行多个任务来说的。

比如:

当我们下载多个文件时,在串行中它是按照一定的顺序去进行下载的, 也就是说,必须等下载完A之后才能开始下载B,它们在时间上是不可能发生重叠的。

并行:

下载多个文件,开启多条线程,多个文件同时进行下载, 这里是严格意义上的,在同一时刻发生的,并行在时间上是重叠的。

多线程:

指一个进程中同时有多个线程正在执行。

比如:电脑管家:本身是一个程序,也是一个进程,里面包含了很多功能,可以查杀病毒、清理垃圾、电脑加速等等...

即:可以同一时刻处理多种事情,没有执行上的先后顺序。

多线程爬虫:

为什么需要多线程爬虫?

能够处理大批量的数据、提高效率..

多任务:

不同于多线程;多线程是在程序内部实现“多任务”。

多任务,多线程,多进程的较易理解说法和区别:

一个进程就是一个程序,进程与应用程序的不同之处在于,进程它工作在后台,应用程序工作在前台,应用程序是能够与用户进行人机交互的。应用程序由进程组成。

多线程是为了使得多个线程并行的工作以完成多项任务,以提高系统的效率。线程是在同一时间需要完成多项任务的时候被实现的;多任务是指用户可以在同一时间内运行多个应用程序,每个应用程序被称作一个任务。

多进程可以理解为运行多个程序 例如WINDOWS就能管理多个进程,在任务管理器里面能看到 多任务可以理解我同时执行多个任务,但实际上不是同时执行多个任务,而是CPU处理速度太快了让我们感觉是同时执行多个任务。

进程可以创建线程,也可以创建进程。线程是由进程管理的,线程之间、线程和父进程(创建线程的进程)之间可以共享内存变量(需要使用策略的)。进程之间一般不可以直接共享内存变量,需要使用一些进程间的控制共享内存变量。

多线程的创建:

通过函数来创建

import threading
import time


# 通过函数来创建
# 通过 threading 模块当中的一个 Thread 类,有一个 target 对象
# 这个参数需要传递一个函数对象,这个函数可以实现多线程的逻辑
def demo():
    print('hi,子线程')


if __name__ == '__main__':
    for i in range(0, 5):
        # 创建多线程
        t = threading.Thread(target=demo)
        time.sleep(1)
        t.start()

通过类来创建

import threading


# 通过类来创建
# 自定义一个类,需要继承父类 threading.Thread,并重写 run() 方法
class demo(threading.Thread):
    # 线程的活动
    def run(self) -> None:
        for i in range(0, 5):
            print('----')


if __name__ == '__main__':
    d = demo()  # 创建对象
    d.start()  # 开启线程的钥匙

主线程和子线程的执行关系

  • 主线程会等待子线程结束之后再结束
  • join() 等待子线程结束之后,主线程继续执行
  • setDaemon() 守护线程,不会等待子线程结束
import threading
import time


# 子线程
def demo():
    for i in range(5):
        print('我是子线程')


if __name__ == '__main__':
    t = threading.Thread(target=demo)

    # t.setDaemon(True)  # 守护线程,不会等待子线程结束
    t.start()
    # time.sleep(2)
    # t.join()  # 等待子线程结束后,主线程继续执行

    print(123456)  # 主线程

查看线程数量:

threading.enumerate()	查看当前线程的数量
import threading
import time


def demo():
    for i in range(5):
        print(f'我是子线程demo--------{i}')
        time.sleep(1)


def test():
    for i in range(10):
        print(f'我是子线程test*******{i}')
        time.sleep(1)


def main():
    t1 = threading.Thread(target=demo)
    t2 = threading.Thread(target=test)

    t1.start()
    t2.start()
    # print(threading.enumerate())  # 查看当前的线程数量:3个线程 主线程、demo、test

    # 判断有多少线程在活动
    while True:
        print(threading.enumerate())  # 只打印已存活的线程及数量

        if len(threading.enumerate()) <= 1:
            break

        time.sleep(1)


if __name__ == '__main__':
    main()

验证子线程的创建与执行:

import threading
import time


# 验证子线程的创建与执行  ==> 相当于 验证子线程是否是存活
def demo():
    for i in range(5):
        print(f'demo---{i}')
        time.sleep(1)


def main():
    print(threading.enumerate(), '--01')  # 打印当前已存活的线程

    t1 = threading.Thread(target=demo)  # 这里并没有创建线程,仅仅创建了线程活动
    print(threading.enumerate(), '--02')

    t1.start()  # 当调用 start 方法后才成功创建这个子线程
    print(threading.enumerate(), '--03')


if __name__ == '__main__':
    main()

线程中的资源竞争问题:

生产者与消费者:

生成者和消费者模式,是多线程开发中常见的一种模式。通过生产者和消费者模式,可以让代码达到高内聚 低耦合 的目标,线程管理更加方便,程序分工更加明确。

生产者的线程专门用来生产一些数据,然后存放到容器中(中间变量)。

消费者再从这个中间的容器中取出数据进行消费。

image.png

代码实例:

import threading
import re
import requests
from lxml import etree
from queue import Queue
from urllib.request import urlretrieve


# 生产者模型  生成图片名字  图片href属性值
class Producer(threading.Thread):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36'
    }

    def __init__(self, page_queue, img_queue):  # RuntimeError: thread.__init__() not called
        # 在自写的类中的init中,先初始化Thread
        threading.Thread.__init__(self)  # 或则 super().__init__()
        self.page_queue = page_queue
        self.img_queue = img_queue

    def run(self):
        while True:  # 让我们创建的三个生产者一直工作
            if self.page_queue.empty():
                break
            else:
                url = self.page_queue.get()
                print(url)
            # 获取到了url就可以去解析数据了
            self.Parse_html(url)

    def Parse_html(self, url):
        # 上锁
        lock.acquire()
        # 发请求,获取响应
        res = requests.get(url, headers=self.headers)
        text = res.text
        # 随机延迟
        # time.sleep(random.random())
        # 解析数据,拿真的图片地址
        element = etree.HTML(text)

        # 将获取的所有img标签放到列表里面
        alldiv = element.xpath('//div[@class="ui segment imghover"]/div[@class="tagbqppdiv"]')

        # 解锁
        lock.release()
        # 取出每一个图片的地址
        for j in alldiv:
            everyhref = j.xpath('./a/img/@data-original')[0]
            print(everyhref)
            title = j.xpath('./a/@title')[0]  # 必须要是合法的
            print(title)
            newtitle = re.sub('[/:*?<>|]', '', title)
            # 将获取到的img_url和title数据存放在另一个队列种然后再交给消费者进行处理
            self.img_queue.put((everyhref, newtitle))  # 用元组打包作为整体进行处理
            # 检测我获取的数据量是否正确
        print(self.img_queue.qsize())


# 消费者模型   下载图片
class Consumer(threading.Thread):
    def __init__(self, img_queue):  # RuntimeError: thread.__init__() not called
        # 在自写的类中的init中,先初始化Thread
        threading.Thread.__init__(self)  # 或则 super().__init__()
        self.img_queue = img_queue

    def run(self):
        while True:  # 让我们创建的三个生产者一直工作
            if self.img_queue.empty():
                break
            else:
                img_data = self.img_queue.get()  # 元组类型数据
            # 解包
            img_url, filename = img_data
            # 下载操作
            if str(img_url).endswith('jpg'):
                urlretrieve(img_url, f'images/{filename}.jpg')
                print(f'{filename}.jpg下载成功!')
            else:
                urlretrieve(img_url, f'images/{filename}.gif')
                print(f'{filename}.gif下载成功!')


# 程序主入口
if __name__ == '__main__':
    # 创建一把锁
    lock = threading.Lock()
    # 1 将所有的url存放在队列中
    page_queue = Queue()  # 创建一个队列 然后通过put方法存放进去

    #  创建一个存放数据的队列
    img_queue = Queue()  # 同样将这个队列通过init初始化传到生产者模型中

    for i in range(1, 11):
        url = f'http://www.godoutu.com/face/hot/page/{i}.html'
        page_queue.put(url)

    p_list = []
    # 2 创建生产者对象  三个
    for i in range(3):
        t = Producer(page_queue, img_queue)  # 将队列传给生产者处理 那再创建对象进行传参的过程中我们需要进行接收 init
        t.start()  # 开启多线程   执行的是run方法
        p_list.append(t)

    for p in p_list:
        p.join()

    # 创建三个消费者
    for j in range(3):
        t = Consumer(img_queue)
        t.start()