进程、线程、协程

优点

  • 使用线程可以把占据长时间的程序中的任务放到后台去处理。

  • 用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。

  • 程序的运行速度可能加快。

  • 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等

线程不能独立运行,每个线程都有一个入口,执行顺序,出口

  • 线程可以被中断
  • 当线程正在运行时,可以暂时把线程睡眠,先让其他线程运行

线程可以分为以下几种

  1. 内核线程

  2. 用户线程

常用的两个模块为

  1. _thread

  2. threading(推荐使用)

多任务

  • 同一时间有多个任务再运行就是多任务

  • python默认执行使单任务

# 线程概念

  • 可以理解为程序执行的一条分支

  • python中实现多任务可以使用线程来实现

  • 程序执行默认有一个主线程,我们可以通过代码来创建子线程

    • 主线程可以创建子线程
    • 主线程一般来是最后执行完毕

使用threading来创建子线程

  • 核心方法

    • 线程对象需要传入一个函数名,不需要括号
  • 变量名 = threading.Thread(target = 函数名),使用它来创建子线程对象

    • 线程对象.start(),使用它来启动子线程
    • strat()方法只能调用一次
import threading
import time

def main():
    print("123456")
    time.sleep(1)

if __name__ == '__main__':
    for i in range(5):
        # 类里边需要传入target=函数名
        r = threading.Thread(target=main)
        r.start()
        main()
  1. 导入模块
  2. 创建线程对象 :threading.Thread(traget=函数名)
  3. 开启线程:线程对象.start()
  • 主线程会等待子线程结束完再退出

线程数量

  • cpu调度的最小单位(线程)

  • 返回正在运行的所有线程对象数量,可以使用len()来查看线程个数

    • threading.enumerate()
  • 获取线程的名称

    • threading.current_thread()
import threading
import time

def main():
    print("123456")
    time.sleep(1)

if __name__ == '__main__':
    for i in range(5):
        # 类里边需要传入target=函数名
        r = threading.Thread(target=main)
        # 获取当前执行的线程对象
        f = threading.enumerate()
        # 获取当前线程对象的名称
        print(threading.current_thread())
        # 获取当前线程个数
        print(len(f))
        # 启动线程
        r.start()
        main()
  • 线程中传入参数
    • threading.Thread(target=函数名, args=(参数1,参数2,…))
      • 该形式可以在函数中通过几个形参来接收,也可以通过形参中直接把一个元组全部接收
    • 还可以在传入参数中使用**kwargs={}**来传入一个字典,但形参中也需要以__**args__来接收这个参数
    • 也可以同时使用argskwargs传递两组变量,但函数中需要用元组和字典可可变参数来接收
import threading

def main1(*args):
    print(args)

def main2(**a):
    print(a)

def main3(*a, **b):
    print(a, b)

if __name__ == "__main__":
    r1 = threading.Thread(target=main1, args=(1, 2, 3))
    r2 = threading.Thread(target=main2, kwargs={"h": 132})
    r3 = threading.Thread(target=main3, args=(123, 456, 789), kwargs={"h": 132})
    r1.start()
    r2.start()
    r3.start()
  • 线程的执行是无序的,是电脑cpu通过自己的算法来实现的

守护线程

  • 如果设置的守护进程,则子线程在主线程退出后也会跟着退出
    • 通过threading.setDaemon(True)来把子线程设置成守护进程
    • 默认情况下,主线程结束了子线程还会继续执行下去
import threading
import time

def work1(args):
    for i in range(10):
        print("正在执行。。。", i)
        print(args)
        time.sleep(0.5)

if __name__ == '__main__':
    r = threading.Thread(target=work1, args=(None,))
    # 把子线程设置为守护进程
    r.setDaemon(True)
    r.start()
    time.sleep(2)
    print("主线程结束")
    # 主动结束主线程
    exit()

并发与并行

  • 并发

    • 当任务数量大于cpu核心的数量时,就是使用并发的方式处理任务
  • 并行

    • 当任务数小于cpu核心数量时,就是使用并行的方式处理任务

自定义线程类

  • 通过继承threading.Thread()来自定义线程类

    • 用于多线程下载与多线程爬虫

    • 如果要往对象中传入参数,则需要先调用父类的__init__()方法,再增加想要的变量

      • 使用super().init()
    • 调用start()方法时,会执行类中的run()方法

"""
1、导入模块
2、让自定义类继承threading.Thread类
3、重写父类(threading.Thread)的run方法
4、通过创建子类对象,通过子类对象.start()就可以启动子线程
"""

import threading
import time

# 子线程对象
class MyThread(threading.Thread):
    # 重写父类run()方法
    def run(self):
        for i in range(5):
            print("正在执行子线程的run方法", i)
            time.sleep(0.5)

if __name__ == '__main__':
    r = MyThread()
    r.setDaemon(True)
    r.start()
    time.sleep(1)
    print("主线程结束")
    exit()

多线程共享全局变量

  • 线程与线程之间可以共享全局变量(global 变量名)
import threading
import time

# 定义一个全局变量
num = 0

# 对全局变量修改
def work1():
    # 声明num是一个全部变量
    global num
    for i in range(10):
        time.sleep(0.5)
        num += 1

# 查看修改后的结果
def work2():
    for i in range(10):
        time.sleep(0.5)
        print(num)

if __name__ == '__main__':
    # 创建两个子线程
    r1 = threading.Thread(target=work1)
    r2 = threading.Thread(target=work2)
    # 开启线程
    r1.start()
    r2.start()
  • 引发的问题

    1. 资源竞争问题,如果cpu性能跟不上,那么数据量大时数据会乱套

      使用线程对象.join()方法使一个对象先执行

      不好的地方是把多线程变成单线程影响总体效率

同步与异步(可以解决资源竞争问题)

  • 同步:多个任务之间有执行的先后顺序

  • 异步:多个任务直接没有执行顺序,相互不影响

  • 线程锁:当一个线程在使用一个资源时,会把资源锁住,不让其他线程访问,保证资源同一时间只有一个线程在访问,这个锁也被成为互斥锁

  • threading模块中引入了Lock类,方便管理

    • 通过r = threading.Lock来创建锁
  • 通过r.acquire()来锁定,r.release()来解锁

    • 原则:尽可能锁定较少的资源
    """
    1、创建互斥锁
    2、在使用资源前锁定资源
    3、使用完之后释放资源
    """
    import threading
    num = 0
    
    def work1():
        global num
        for i in range(100):
            # 加锁
            lock1.acquire()
            num += 1
            # 解锁
            lock1.release()
        print("work1---", num)
    
    def work2():
        global num
        for i in range(100):
            # 加锁
            lock1.acquire()
            num += 1
            # 解锁
            lock1.release()
        print("work2---", num)
    
    if __name__ == '__main__':
        # 创建一把互斥锁
        lock1 = threading.Lock()
        r1 = threading.Thread(target=work1)
        r2 = threading.Thread(target=work2)
    
        r1.start()
        r2.start()
        print("main---", num)
    
  • 避免死锁:两个线程同时占用了对方的资源并且在等待对方的资源就产生死锁了

    • 比如当线程加锁后直接return没有释放锁就形成死锁了,所以加锁后一定要释放

TCP服务端实现多用户连接与多用户发送消息

"""
1、导入模块
2、创建套接字
3、设置地址可以重用
4、绑定端口
5、设置监听,套接字由主动设置为被动
6、接收客户端连接
7、接收客户端发送的信息
8、解码数据进行输出
9、关闭连接
"""
# 1、导入模块
import socket
import threading

def recv_msg(new_client_socket, ip_port):
    while True:
        # 7、接收客户端发送的信息
        recv_data = new_client_socket.recv(1024)
        # 8、解码数据进行输出
        recv_data = recv_data.decode("GBK")

        if not recv_data:
            print(f"{str(ip_port[0])}客户端断开连接")
            # 9、关闭连接
            new_client_socket.close()
            break
        print(f"收到来自{str(ip_port[0])}的信息:{recv_data}")

# 2、创建套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 3、设置地址可以重用
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 4、绑定端口
tcp_server_socket.bind(("", 8080))
# 5、设置监听,套接字由主动设置为被动
tcp_server_socket.listen(128)
n = 0
while True:
    # 6、接收客户端连接
    new_client_socket, ip_port = tcp_server_socket.accept()
    print("欢迎客户端连接:", ip_port[0])
    client = threading.Thread(target=recv_msg, args=(new_client_socket, ip_port))
    # 设置子线程守护主线程
    client.setDaemon(True)
    client.start()

进程以及状态

  • 进程是资源分配的基本单位,是线程的容器
  • 进程往往分为5个状态:创建-就绪-运行-结束,其中就绪后由一个等待的状态,这个状态是当运行条件没有满足的情况下发生的
  • 进程运行时会有一个主进程,而主线程只有在主进程中才有

进程的创建

  • 在windows上创建进程有点问题,估计是操作系统的原因,Ubuntu不会有问题
"""
1、导入模块
2、通过模块提供的Process类来创建进程
3、启动进程
"""
import multiprocessing
import time

def main():
	# 获取子进程的名称        
    print(f"正在运行{multiprocessing.current_process().name}")
    # 获取子进程ID
    print(f"正在运行{multiprocessing.current_process().pid}")

if __name__ == '__main__':
    # 创建进程
    # name指定子进程的名字
    process = multiprocessing.Process(target=main, name="p1")
    # 启动进程
    process.start()
  • 使用os.getpid()和os.getppid()获取进程ID与进程父的ID
import multiprocessing
import time
import os

def main():
    print(f"子进程名称:{multiprocessing.current_process().name}")
    print(f"子进程ID:{os.getpid()}")
    time.sleep(30)
    print(f"使用os获取子进程ID:{os.getpid()}")
    print(f"使用os获取子进程父ID:{os.getppid()}")

if __name__ == '__main__':
    # target 指定子进程要执行的分支
    # name 指定子进程的名称
    process = multiprocessing.Process(target=main, name="p1")
    process.start()
    print("主进程名称:", multiprocessing.current_process().name)
    print("主进程ID:", os.getpid())
    print("-"*80)
  • multiprocessing.current_process()获取进程的名字

注意:在windows操作系统中,创建一个子线程并在里边写入输出语句时,会输出但是不是输出到当前控制台,可以在cmd中使用python xxx.py来运行就能输出子进程中的输出语句了

  • 还可以使用os模块来获取进程的ID(pid),一般来说是获取子进程的父ID(ppid)

  • kill -9:强制结束进程,windowsx下是taskkill /f /pid processid

  • 结束子进程id与主进程id都可以使运行的进程关闭

进程的参数传递

  • 进程的参数传递以线程的参数传递一模一样

    元组传递----------args=(a, b, c)

    字典传递----------kwargs={“key”: value}

    混合传递----------args=(a, b, c), kwargs={“key”: value}

  • 进程与进程之间不共享全局变量,进程在读取全局变量时会在自身所在内存中新建一个一模一样的值

    import multiprocessing
    import time
    import os
    
    num = 0
    
    def work1():
        global num
        for i in range(10):
            time.sleep(0.5)
            num += 1
            print(f"子进程1更改全局变量{num}")
    
    def work2():
        global num
        for i in range(10):
            time.sleep(0.5)
            print(f"子进程2读取的全局变量{num}")
    
    if __name__ == '__main__':
        process = multiprocessing.Process(target=work1, name="p1")
        process.start()
    
        process = multiprocessing.Process(target=work2, name="p2")
        process.start()
    
        print("主进程名称:", multiprocessing.current_process().name)
        print("主进程ID:", os.getpid())
        print("-" * 80)
        time.sleep(10)
        print(f"主进程读取的全局变量{num}")
    

守护主进程

  • 跟守护线程是一样的,当主进程结束后,子进程也随之结束

    进程对象.daemon = True---------设置守护进程

    进程对象.terminate()---------------杀死指定进程

import multiprocessing
import time
import os

def work1():
    for i in range(10):
        time.sleep(0.5)
        print(f"子进程正在运行")

if __name__ == '__main__':
    process1 = multiprocessing.Process(target=work1, name="p1")
    # 这里要注意一下,设置守护进程需要在进程还没运行之前
    process1.daemon = True
    process1.start()
    print(f"主进程名字:{multiprocessing.current_process().name}")
    # 杀死进程
    process1.terminate()
    print(f"主进程ID:{os.getpid()}")
    time.sleep(5)

协程(又称微线程)

  • 协程就是特殊的生成器

    • 使函数执行到yield关键字时保留当前的资源,下次再继续执行

    • 当CPU不需要大量操作的时候(I/O),适用于协程

    • 使用next(生成器对象)来运行协程

      """
      协程就是一个特殊的生成器,可以在不开辟新的空间情况下实现多任务
      """
      import time
      
      def work():
          while True:
              print("正在执行work1...")
              yield
              time.sleep(1)
      
      def work2():
          while True:
              print("正在执行work2...")
              yield
              time.sleep(1)
      
      if __name__ == '__main__':
          w1 = work()
          w2 = work2()
          while True:
              next(w1)
              next(w2)
      
    • 当调用work使时,会运行work中的代码,直到遇到yield,此时会返回一个值给调用它的变量,当下次调用的时候,会从yield的下一句代码开始执行

  • greenlet:一个c拓展,可以不使用yield关键字来实现协程之间的切换(实现协程)

    • 提供了一个可自行调度的微线程(即协程),通过greenlet对象.switch来切换不同协程

      """
      使用greenlet来实现协程的步骤
      1、导入模块greenlet
      2、创建任务
      3、创建greenlet对象
      4、手动switch切换任务
      """
      import time
      from greenlet import greenlet
      
      def work1():
          while True:
              print("正在执行work1......")
              time.sleep(1)
              # 切换到第二个任务
              g2.switch()
      
      def work2():
          while True:
              print("正在执行work2......")
              time.sleep(1)
              # 切换到第一个任务
              g1.switch()
      
      if __name__ == '__main__':
          # 创建对象,带参数的函数名没有括号,用一个变量接收
          g1 = greenlet(work1)
          g2 = greenlet(work2)
      
          # 执行work2这个任务
          g1.switch()
      
    • 使用greenlet来写协程时,只需要告诉程序先执行哪个任务,在遇到阻塞时去执行哪个任务就可以了,相对于使用yield来说可控很多

    • 创建任务对象使用greenlet(函数名),切换任务使用协程对象.switch

  • 使用gevent实现协程,能自动检测代码的耗时时长来切换任务

    • 能保证总有任务时在执行,特别时I/O

      """
      gevent实现协程,自动识别程序中耗时操作,在耗时的时候自动切换到其他的任务
      1、导入gevent模块
      2、指派任务
      """
      from gevent import monkey
      monkey.patch_all()
      
      import gevent
      import time
      
      def work1():
          while True:
              print("正在执行work1........")
              time.sleep(0.5)
      
      def work2():
          while True:
              print("正在执行work2........")
              time.sleep(0.5)
              # gevent.sleep(0.5)
      
      if __name__ == '__main__':
          # 指派任务:gevent.spawn(函数名,参数1,参数2,参数3。。。。)
          g1 = gevent.spawn(work1)
          g2 = gevent.spawn(work2)
          # 让主线程等待协程执行完毕再退出
          g1.join()
          g2.join()
      
      
    • 要让gevent包可以识别python自带的耗时操作需要在代码中导入monkey补丁模块

    • 或者使用gevent的sleep方法休眠(gevent.sleep(num))

    • 使用gevent包中的monkey.patch_all()方法时,一般放在代码最开头,避免一些不必要的影响

    • 在主程序中需要加入协程对象.join()方法让主程序等待协程执行完再结束进程

总结

  • 区别

    进程:类似多个程序在运行

    • 资源分配的基本单位,能够独立运行,占用资源比较多,进程与进程之间相互切换对于线程与协程相比时比较慢,但相对来说比较稳定

    线程:一个程序内多个窗口在运行

    • 相比进程没有独立空间,运行在进程之下,数据容易丢失,但相对进程来说执行速度比较快,有GIL锁

    协程:一个线程内不开辟新线程的情况下实现多任务(green-let模块)

    • 一种用户态的轻量级线程,由用户自己来调度,上下文切换会保留之前的数据,运行速度非常快,可以不加锁来访问全局变量,协程是在主线程中运行的
    • 切换效率
      • 协程 > 线程 > 进程

实例1-----使用协程下载图片

# -*-coding:utf-8-*-
"""
并发下载器----实例
1、定义图片路径
2、调用文件下载的函数,专门下载文件

文件下载函数
1、根据url地址请求网络资源
2、在本地创建文件,准备保存
3、读取网络资源数据(循环)
4、读取的网络资源写入到本地文件中
5、做异常捕获
"""
from gevent import monkey

monkey.patch_all()
import urllib.request
import gevent

def download_img(url, filename):
    # 5、做异常捕获
    try:
        # 1、根据url地址请求网络资源
        response_data = urllib.request.urlopen(url)
        # 2、在本地创建文件,准备保存
        with open(filename, "wb") as f:
            # 3、读取网络资源数据(循环)
            while True:
                file_data = response_data.read(1024)
                # 4、读取的网络资源写入到本地文件中
                if file_data:
                    f.write(file_data)
                else:
                    break
    except Exception:
        print("文件下载失败:", filename)
    else:
        print("文件下载成功:", filename)

def main():
    # 1、定义图片路径
    url1 = "https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1999921673,816131569&fm=26&gp=0.jpg"
    url2 = "https://dss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3206689113,2237998950&fm=26&gp=0.jpg"
    url3 = "https://dss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3228549874,2173006364&fm=26&gp=0.jpg"

    # download_img(url1, "1.jpg")
    # download_img(url2, "2.jpg")
    # download_img(url3, "3.jpg")
    # 批量把协程给join
    gevent.joinall(
        [
            # 调用下载函数
            gevent.spawn(download_img, url1, "1.jpg"),
            gevent.spawn(download_img, url2, "2.jpg"),
            gevent.spawn(download_img, url3, "3.jpg"),
        ]
    )

if __name__ == '__main__':
    main()
posted @ 2021-06-01 15:51  耿集  阅读(60)  评论(0)    收藏  举报