python 多线程

本文介绍了Python的多任务编程,包括单核CPU下的并发概念,多进程和多线程的原理及实现,以及协程的概念。在多进程编程中,详细讲解了进程的状态模型、Python创建子进程的方法、进程池和进程通信。多线程部分阐述了线程的定义、与进程的区别,并提到了线程间通信的问题和GIL带来的限制。最后简要提及了协程作为解决并发问题的一种方案。
摘要由CSDN通过智能技术生成

python 多任务编程

  • 单核CPU

    ​ 其在实现多任务时候,实质上是CPU交替执行多个任务A、B、C等,由于CPU执行速度较快,表象为多个任务同时进行。

    ​ 宏观上是同时执行,微观上仍是顺序执行

  • 并发

    ​ 并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

  • 在操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。

    真正的并行执行多任务需要依靠多核CPU

多进程编程

  • 进程

正在运行中的程序称之为进程

它是程序和运行环境的结合

  • 进程的五状态模型

在这里插入图片描述

  1. 创建进程
  2. 进程就绪,等待CPU分配时间片段
  3. 进程执行,就绪状态被分配了时间片段开始执行
  4. 分配的时间片段走完之后,该进程回到就绪状态
  5. 当执行的任务需要其他I/O输入时需要进行等待,进入阻塞状态
  6. 当I/O输入完成后,阻塞状态进入就绪状态,等待CPU分配时间片段
  7. 最后进程进入终止状态,进程结束结束
多进程编程提高运行效率主要是利用任务在阻塞状态时空闲的CPU时间片段,将这个时间片段分配给其他进程。
  • python中创建子进程
# 利用fork进程(分支函数,相当于创建一个进程的副本 )
# fork函数调用一次,有两个返回值,一个是子进程的pid,另外一个是0
# 为什么返回子进程pid?可以利用子进程os.getppid()获取父进程
import os

print('当前进程的pid为:%s' % os.getpid())
print('当前进程的父进程pid为:%s' % os.getppid())
print('使用fork函数创建进程')
process = os.fork()
if process == 0:
    print('创建的子进程返回值为0,子进程pid为:%s,父进程的pid为:%s' % (os.getpid(), 					os.getppid()))
else:
    # 父进程返回的是子进程的pid
    print('返回的子进程pid为:%s' % process)
    
___________________________________
当前进程的pid为:18330
当前进程的父进程pid为:4791
使用fork函数创建进程
返回的子进程pid为:18335
创建的子进程返回值为0,子进程pid为:18335,父进程的pid为:18330
fork函数在windows下不支持
  • 子进程和父进程之间的数据互相不影响

    子进程的数据相当于对父进程数据的重新拷贝

  • Process类(通过实例化对象实现多进程)

# Process继承BaseProcess类,需要传入的参数如下
_________________________________________________

def __init__(self, group=None, target=None, name=None, args=(), kwargs={},
                 *, daemon=None):
    # target:处理的对象
    # name :进程的别名
    # args 传入对象所需要传递的参数
    # kwargs :传入所需的关键字参数
    …………
# 未使用多进程
def ListenMusic():
    time.sleep(1)
    print('listening musics……')
def DoHomework():
    time.sleep(1)
    print('doing homework……')
def no_use_multiProcess():
    for i in range(4):
        ListenMusic()
        DoHomework()
if __name__ == '__main__':
    start_time = datetime.now()
    no_use_multiProcess()
    end_time = datetime.now()
    print('所用时间为:%d秒' % (end_time - start_time).seconds)
____________________________________________________________
所用时间为:8# 使用多进程
    def use_multiProcess():
    processPool = []
    for i in range(4):
        P1 = Process(target=ListenMusic)
        P2 = Process(target=DoHomework)
        P1.start()
        P2.start()
        processPool.append(P1)
        processPool.append(P2)
    # 需要使用join函数:等所有创建的子进程先执行完,再执行主进程
    # 使用列表生成式
    [process.join() for process in processPool]
_______________________________________________________________
所用时间为:1
  • note
1.实例化Process类就相当于创建了子进程
2.新创建的进程不会自动启动,需要调用start函数,进程处于就绪状态
	start和run函数的区别:
    	start是让进程处于就绪状态,但是并没有运行。一旦得到CPU的时间片段,进程就可以运行,执行run方法。
3.加入进程池中各个进程竞争CPU的时间片段
4.当进程处于休眠状态时把时间片段分配给其他的进程使用
5.join函数:阻塞住主进程再等待子进程结束,然后再往下执行
 
  • join源码
    def join(self, timeout=None):
        '''
        Wait until child process terminates
        '''
        self._check_closed()
        assert self._parent_pid == os.getpid(), 'can only join a child process'
        assert self._popen is not None, 'can only join a started process'
        res = self._popen.wait(timeout)
        if res is not None:
            _children.discard(self)
    # 里面调用了wait()方法,阻塞主进程
  • 进程池

    当具有成百上千以及更多的任务时候,创建这么多的进程是不合适的,因为创建子进程会重新复制父进程的信息,并且会占用很多的内存空间,所以当任务数量大时,我们采用进程池来处理

    1. 资源进程

      预先创建好的空闲进程,管理进程会把工作分发到空闲进程来处理

    2. 管理进程

      管理进程负责创建资源进程,把工作交给空闲资源进程处理,回收已经处理完工作的资源进程

def use_pool():
    # 使用线程池处理
    from multiprocessing import Pool, cpu_count
    # 创建线程池,设置线程池中资源线程的个数
    pool = Pool(processes=cpu_count())
    pool.map(task, list(range(1, 100000)))
    # 这里close表示关闭进程池的使用,不再接受新的进程
    # 当进程池close的时候并未关闭进程池,只是会把状态改为不可再插入元素的状态,完全关闭进程池使用
    pool.close()
    # 阻塞主进程,等待进程池中子进程的完成
    pool.join()
    # close方法需要在join方法之前调用
  • 利用创建Process子类实现多进程
import time
from multiprocessing import Process

class MyProcess(Process):
    # 可以使用构造方法传递参数
    def __init__(self, num):
        super(MyProcess, self).__init__()
        self.num = num
    # 要重写run方法
    def run(self):
        print('这是子任务%d' % self.num)
if __name__ == '__main__':
    # 创建多个子进程
    for i in range(1, 10):
        process = MyProcess(i)
        process.start()
  • 进程通信的目的
  1. 数据传输
  2. 数据共享
  3. 资源共享
  4. 进程控制
  • 进程间通讯的方式
  1. 管道
  2. 信号
  3. 消息队列
这三种方式用于同一主机通信
  1. 套接字
不同主机之间实现进程通信用套接字体
  • 消息队列
from multiprocessing.context import Process
# 使用消息队列实现生产者消费者模型
from multiprocessing import Queue

class Producer(Process):
    def __init__(self, queue):
        super(Producer, self).__init__()
        self.queue = queue
        
    def run(self):
        for i in range(1, 11):
            print('生产了产品%d' % i)
            # put方法当队列满的时候会一直等待
            self.queue.put(i)
            
class Consumer(Process):
    def __init__(self, queue):
        super(Consumer, self).__init__()
        self.queue = queue
        
    def run(self):
        while True:
            # get出队操作当队列为空时也会一直等待,即阻塞当前进程
            product = self.queue.get()
            print('消费了产品%d' % product)
            
if __name__ == '__main__':
    queue = Queue()
    p = Producer(queue)
    c = Consumer(queue)
    p.start()
    c.start()

多线程编程

  • 什么是线程?

    线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以有很多线程,每条线程并行执行不同的任务。

  • 进程和线程的区别

  1. 进程是资源分配的最小单位,一个新的进程会分配一块新的内存空间,而线程是程序执行的最小单位。
  2. 进程之间的内存空间是互相独立的,而线程之间共享内存空间。
  3. 进程间通信需要依靠管道、消息队列、套接字等通信方式,而线程之间因为共享内存空间,所以通信更加方便,但需要处理同步和互斥的问题。
  • 一个进程中必然有一个主线程
import threading

if __name__ == '__main__':
    print('当前存在的线程个数:%d' % threading.activeCount())
    print('当前线程的信息:%s' % threading.currentThread())
________________________________________________________
当前存在的线程个数:1
当前线程的信息:<_MainThread(MainThread, started 139683240683328)>
import threading
import time
from datetime import datetime

def task():
    print('正在执行任务……')
    time.sleep(1)

if __name__ == '__main__':任务
    start_time = datetime.now()
    threadPool = []
    for i in range(10):
        #  使用多线程
        thread = threading.Thread(target=task)
        # 开启线程
        thread.start()
        threadPool.append(thread)
    [thread.join() for thread in threadPool]
    end_time = datetime.now()
    print('耗费时间为:%d' % (end_time - start_time).seconds)
一个py文件执行就相当于一个程序,创建多个线程来执行这个任务。
  • IP地址归属地查询
# 使用实例化对象实现多线程
import json
from datetime import datetime
from json import JSONDecodeError
import requests
import threading
from fake_useragent import UserAgent

def get_addr(ip):
    url = 'http://ip-api.com/json/%s' % ip
    # 该网址返回的是json字符串
    ua = UserAgent()
    headers = {'User-Agent': ua.random}
    response = requests.get(url, headers=headers)
    try:
        info = response.json()
        print(info)
        print('%s的所属城市为:%s-%s' % (ip, info['country'], info['city']))
    except  JSONDecodeError  as e:
        print('未找到%s的信息' % ip)

if __name__ == '__main__':
    start_time = datetime.now()
    base_ip = '1.1.1.'
    threadPool = []
    for i in range(1, 115):
        ip = base_ip + str(i)
        # 使用多线程
        thread = threading.Thread(target=get_addr, args=(ip,))
        # 开启线程
        thread.start()
        threadPool.append(thread)
    [thread.join() for thread in threadPool]
    end_time = datetime.now()
    print('耗费时间为:%s' % (end_time - start_time).seconds)
  • 用继承类方式实现多线程
# 网段IP地址存活探测
import os
from threading import Thread
from colorama import Fore

class taskThread(Thread):
    def __init__(self, ip):
        super(taskThread, self).__init__()
        self.ip = ip

    def run(self):
        cmd = 'ping -c1 -w1 %s &> /dev/null' % (self.ip)
        result = os.system(cmd)
        if not result:
            print(Fore.GREEN + 'ip:%s存活' % self.ip)
        else:
            print(Fore.RED + 'ip:%s不存在' % self.ip)

if __name__ == '__main__':
    base_ip = '192.168.122.'
    for i in range(0, 255):
        ip = base_ip + str(i)
        thread = taskThread(ip)
        thread.start()
  • GIL(全局解释器锁)
GIL本质就是一把互斥锁,是夹在解释器身上的,
    同一个进程内的所有线程都需要先抢到GIL锁,才能执行解释器代码  
优点:
   保证Cpython解释器内存管理的线程安全
缺点:
   同一进程内所有的线程同一时刻只能有一个执行,也就说Cpython解释器的多线程无法实现并行
  • 多线程处理同一全局变量会发生混乱

    虽然CPython中使用了全局解释器锁,进程中同一时刻只能执行一个线程,但是各个线程之间的切换会引发错误,线程执行到一半切换到另外线程就可能导致全局变量的混乱

from threading import Thread

money = 0
def add():
    for i in range(100000):
        # 声明money为全局变量
        global money
        # 这步操作可以分为两部分:1为加法运算 2为赋值运算 需要统一执行
        money += 1
def reduce():
    for i in range(100000):
        global money
        money -= 1
if __name__ == '__main__':
    # 使用多线程
    task1 = Thread(target=add)
    task2 = Thread(target=reduce)
    task1.start()
    task2.start()
    task1.join()
    task2.join()
    print(money)
_______________________________
-33576
  • 使用线程锁
from threading import Thread, Lock
money = 0
def add():
    for i in range(1000000):
        # 声明money为全局变量
        global money
        # 加锁
        lock.acquire()
        money += 1
        # 解锁
        lock.release()
…………

if __name__ == '__main__':
    # 使用多线程
    # 实例化线程锁
    lock = Lock()
    …………
    print(money)

  • 死锁问题

两个线程分别占有一部分资源,但是都需要对方的资源才能继续,就会造成死锁问题。

尽量避免:1.尽量使用同一把锁

​ 2.对锁的优先级进行排序

简单理解:两个线程各自拥有不同的锁,一个线程上锁之后执行任务,遇到sleep()函数,途中进入阻塞状态,然后另一个线程开始执行,上锁之后也遭遇阻塞,之后两个线程都无法正常执行,锁不同。

协程

一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。
import time
import gevent
from gevent import monkey

monkey.patch_all()

def task(num):
    print('正在执行任务%d' % num)
    time.sleep(1)

if __name__ == '__main__':
    # 一个进程下多个协程
    # 列表生成式
    gevents = [gevent.spawn(task, num) for num in range(1, 11)]
    gevent.joinall(gevents)
    print('任务执行结束')
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值