高级编程(三)多任务编程

一.多任务编程

1.什么是“多任务”?

多任务:就是操作系统可以同时运⾏多个任务。打个 ⽐⽅,你⼀边在⽤浏览器上⽹,⼀边在听MP3,⼀边在⽤Word赶作业,这就是多任务,⾄少同时有3个任务正在运⾏。还有很多任务悄悄地在后台同时运 ⾏着,只是桌⾯上没有显示⽽已。

2.单核CPU如何实现“多任务”?

操作系统轮流让各个任务交替执⾏,每个任务执⾏0.01秒,这样反复执⾏下去。 表⾯上看,每个任务交替执⾏,但CPU的执⾏速度实在是太快了,感觉就像所有任务都在同时执⾏⼀样。
在这里插入图片描述

3.多核CPU如何实现“多任务”?

真正的并⾏执⾏多任务只能在多核CPU上实现,但是,由于任务数量远远多 于CPU的核⼼数量,所以,操作系统也会⾃动把很多任务轮流调度到每个核 ⼼上执⾏。
在这里插入图片描述

二.多进程编程

1.进程VS程序

编写完毕的代码,在没有运⾏的时候,称之为程序
正在运⾏着的代码,就成为进程
注意: 进程,除了包含代码以外,还有需要运⾏的环境等,所以和程序是有区别的。

2.进程的五状态模型

在这里插入图片描述

3.创建子进程

Python的os模块封装了常⻅的系统调⽤,其中就包括fork,可以在Python程 序中轻松创建⼦进程:
在这里插入图片描述
代码描述

1). Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,
调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(
称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。

2). 子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork
出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()
就可以拿到父进程的ID。

3). Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松
创建子进程:
原理:
父进程和子进程:, 如果父进程结束, 子进程也随之结束;
先有父进程, 再有子进程. 类Linux系统中(redhat,mac), fork函数;
常用函数:

    os.fork()

    os.getpid()   # 获取当前进程的pid  (process id)

    os.getppid()    # 获取当前进程的父进程pid (parent process id)

"""

import os



print("当前进程(pid=%d)正在运行......." %(os.getpid()))

# 在pycharm编写代码, 程序的父进程就是pycharm;

print("当前进程的父进程为(pid=%d)正在运行....." %(os.getppid()))

print("开始创建子进程.....")



pid = os.fork()

if  pid == 0:

    print("这是子进程返回的是0, 子进程的pid为%d, 父进程为%d" %(os.getpid(), os.getppid()))

else:

    print("这是父进程返回的,返回值为子进程的pid, 为%d" %(pid))
  • 执⾏到os.fork()时,操作系统会创建⼀个新的进程复制⽗进程的所有信息到⼦进程中
  • 普通的函数调⽤,调⽤⼀次,返回⼀次,但是fork()调⽤⼀次返回两次
  • ⽗进程和⼦进程都会从fork()函数中得到⼀个返回值,⼦进程返回是0,⽽⽗进程中返回⼦进程的 id号
    多进程修改全局变量
    多进程中,每个进程中所有数据(包括全局变量)都各有拥有⼀份,互不影响。

4.multiprocessing模块

Windows没有fork调⽤,由于Python是跨平台的, multiprocessing模块就是跨平台版本的多进程模块。multiprocessing模块提供了⼀个Process类来代表⼀个进程对象。

Process([group [, target [, name [, args [, kwargs]]]]]) 

target:表示这个进程实例所调⽤对象;
args:表示调⽤对象的位置参数元组;
kwargs:表示调⽤对象的关键字参数字典;
name:为当前进程实例的别名;
group:⼤多数情况下⽤不到;

  • Process类常用方法
    is_alive():判断进程实例是否还在执⾏;
    join([timeout]):是否等待进程实例执⾏结束,或等待多少秒;
    start():启动进程实例(创建⼦进程);
    run(): 如果没有给定target参数,对这个对象调⽤start()⽅法时,就将执⾏对象中的run()⽅法;
    terminate():不管任务是否完成,⽴即终⽌;
  • Process类常用属性
    name:当前进程实例别名,默认Process-N,N为从1开始计数;
    pid:当前进程实例的PID值;

5.多进程编程方法

(1)实例化对象

from multiprocessing import Process
import time


def task1():
    print("正在听音乐")
    time.sleep(1)


def task2():
    print("正在编程......")
    time.sleep(0.5)


def no_multi():
    task1()
    task2()

def use_multi():
    p1 = Process(target=task1)
    p2 = Process(target=task2)
    p1.start()
    p2.start()
    p1.join()
    p2.join()

    # p.join()  阻塞当前进程, 当p1.start()之后, p1就提示主进程, 需要等待p1进程执行结束才能向下执行, 那么主进程就乖乖等着, 自然不会执行p2.start()
    # [process.join() for process in processes]

if __name__ == '__main__':
    # 主进程
    start_time= time.time()
    # no_multi()
    use_multi()
    end_time = time.time()
    print(end_time-start_time)

(2)创建子类

from multiprocessing import  Process
import time
class MyProcess(Process):
    """
    创建自己的进程, 父类是Process
    """
    def __init__(self, music_name):
        super(MyProcess, self).__init__()
        self.music_name = music_name

    def run(self):
        """重写run方法, 内容是你要执行的任务"""

        print("听音乐%s" %(self.music_name))
        time.sleep(1)

# 开启进程: p.start()  ====== p.run()
if __name__ == '__main__':
    for i in range(10):
        p = MyProcess("音乐%d" %(i))
        p.start()  

6.进程池

(1)为什么需要进程池Pool?

  • 当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态成生多个进程,十几个还好,但如果是上百个,上千个目标,手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。
  • Pool可以提供指定数量的进程供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来它。
    在这里插入图片描述

(2)对比多进程与进程池-------判断素数

def is_prime(num):
    """判断素数"""
    if num == 1:
        return False

    for i in range(2, num):
        if num % i == 0:
            return False
    else:
        return True

def task(num):
    if is_prime(num):
        print("%d是素数" % (num))
from multiprocessing import Process
# 判断1000-1200之间所有的素数
def use_mutli():
    ps = []
    # 不要开启太多进程, 创建子进程会耗费时间和空间(内存);
    for num in range(1, 10000):
        # 实例化子进程对象
        p = Process(target=task, args=(num,))
        # 开启子进程
        p.start()
        # 存储所有的子进程对象
        ps.append(p)

    # 阻塞子进程, 等待所有的子进程执行结束, 再执行主进程;
    [p.join() for p in ps]


# 判断1000-1200之间所有的素数
def no_mutli():
    for num in range(1, 100000):
        task(num)

def use_pool():
    """使用进程池"""
    from multiprocessing import Pool
    from multiprocessing import  cpu_count  # 4个
    p = Pool(cpu_count())
    p.map(task, list(range(1, 100000)))
    p.close()  # 关闭进程池
    p.join()  # 阻塞, 等待所有的子进程执行结束, 再执行主进程;

if __name__ == '__main__':
    import time
    start_time = time.time()
    end_time = time.time()
    print(end_time - start_time)

7.进程间通信

(1)进程通信的目的

在这里插入图片描述

(2)进程间通信方式

在这里插入图片描述

(3)进程间通信方式------消息队列

可以使⽤multiprocessing模块的Queue实现多进程之间的数据传递,Queue 本身是⼀个消息列队程序。
Queue.qsize():返回当前队列包含的消息数量;
Queue.empty():如果队列为空,返回True,反之False ;
Queue.full(): 如果队列满了,返回True,反之False;
Queue.get([block[, timeout]]):获取队列中的⼀条消息,然后将其从列队中移除,block默认值为True;
Queue.get_nowait():相当Queue.get(False);
Queue.put(item,[block[, timeout]]):将item消息写⼊队列,block默认值 为True;
Queue.put_nowait(item):相当Queue.put(item, False)

三.多线程编程

1.什么是线程

**线程(英语:thread)**是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
在这里插入图片描述
每个进程至少有一个线程,即进程本身。进程可以启动多个线程。操作系统像并行“进程”一样执行这些线程。
在这里插入图片描述

2.线程和进程的区别和优劣

  • 进程是资源分配的最小单位,线程是程序执行的最小单位。
  • 进程有自己的独立地址空间。线程是共享进程中的数据的,使用相同的地址空间.
  • 进程之间的通信需要以通信的方式(IPC)进行。线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,难点:处理好同步与互斥。

3.线程分类

有两种不同的线程

  • 内核线程
  • 用户空间线程或用户线程,内核线程是操作系统的一部分,而内核中没有实现用户空间线程。

4.多线程实现方式

(1)实例化对象实现多线程

import time
import threading
def task():
    """当前要执行的任务"""
    print("听音乐........")
    time.sleep(1)

if __name__ == '__main__':
    start_time = time.time()
    threads = []
    for  count in range(5):
        t = threading.Thread(target=task)
        # 让线程开始执行任务
        t.start()
        threads.append(t)

    # 等待所有的子线程执行结束, 再执行主线程;
    [thread.join() for thread in threads]
    end_time = time.time()
    print(end_time-start_time)

方法一分析

  • 多线程程序的执⾏顺序是不确定的。
  • 当执⾏到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进⼊就绪(Runnable)状态,等待调度。⽽线程调度将⾃⾏选择⼀个线程执⾏。
  • 代码中只能保证每个线程都运⾏完整个run函数,但是线程的启动顺序、 run函数中每次循环的执⾏顺序都不能确定。

(2)创建子类实现多线程

from threading import  Thread
class GetHostAliveThread(Thread):
    """
    创建子线程, 执行的任务:判断指定的IP是否存活
    """
    def __init__(self, ip):
        super(GetHostAliveThread, self).__init__()
        self.ip = ip
    def run(self):
        # # 重写run方法: 判断指定的IP是否存活
        # """
        # >>> # os.system()  返回值如果为0, 代表命令正确执行,没有报错; 如果不为0, 执行报错;
        # ...
        # >>> os.system('ping -c1 -w1 172.25.254.49 &> /dev/null')
        # 0
        # >>> os.system('ping -c1 -w1 172.25.254.1 &> /dev/null')
        # 256
        # """
        import os
        # 需要执行的shell命令
        cmd = 'ping -c1 -w1 %s &> /dev/null' %(self.ip)
        result = os.system(cmd)
        #  返回值如果为0, 代表命令正确执行,没有报错; 如果不为0, 执行报错;
        if result != 0:
            print("%s主机没有ping通" %(self.ip))
if __name__ == '__main__':
    print("打印172.25.254.0网段没有使用的IP地址".center(50, '*'))
    for i in range(1, 255):
        ip = '172.25.254.' + str(i)
        thread = GetHostAliveThread(ip)
        thread.start()

5.线程的几种状态

在这里插入图片描述

6.项目案例

(1)实现IP归属地址批量查询任务

import requests
import json
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from threading import Thread


def task(ip):
    """获取指定IP的所在城市和国家并存储到数据库中"""
    # 获取网址的返回内容
    url = 'http://ip-api.com/json/%s' % (ip)
    try:
        response = requests.get(url)
    except Exception as e:
        print("网页获取错误:", e)
    else:
        # 默认返回的是字符串
 
        contentPage = response.text
        # 将页面的json字符串转换成便于处理的字典;
        data_dict = json.loads(contentPage)
        # 获取对应的城市和国家
        city = data_dict.get('city', 'null')  # None
        country = data_dict.get('country', 'null')

        print(ip, city, country)
        # 存储到数据库表中ips
        ipObj = IP(ip=ip, city=city, country=country)
        session.add(ipObj)
        session.commit()


if __name__ == '__main__':
    engine = create_engine("mysql+pymysql://root:westos@172.25.254.123/pymysql",
                           encoding='utf8',
                           # echo=True
                           )
    # 创建缓存对象
    Session = sessionmaker(bind=engine)
    session = Session()

    # 声明基类
    Base = declarative_base()


    class IP(Base):
        __tablename__ = 'ips'
        id = Column(Integer, primary_key=True, autoincrement=True)
        ip = Column(String(20), nullable=False)
        city = Column(String(30))
        country = Column(String(30))

        def __repr__(self):
            return self.ip


    # 创建数据表
    Base.metadata.create_all(engine)

    # 1.1.1.1 -- 1.1.1.10
    threads = []
    for item in range(10):
        ip = '1.1.1.' + str(item + 1)  # 1.1.1.1 -1.1.1.10
        # task(ip)
        # 多线程执行任务
        thread = Thread(target=task, args=(ip,))
        # 启动线程并执行任务
        thread.start()
        # 存储创建的所有线程对象;
        threads.append(thread)

    [thread.join() for thread in threads]
    print("任务执行结束.........")
    print(session.query(IP).all())

(2)基于多线程的批量主机存活探测

"""
创建子类
"""


from threading import  Thread
class GetHostAliveThread(Thread):
    """
    创建子线程, 执行的任务:判断指定的IP是否存活
    """
    def __init__(self, ip):
        super(GetHostAliveThread, self).__init__()
        self.ip = ip
    def run(self):
        # # 重写run方法: 判断指定的IP是否存活
        # """
        # >>> # os.system()  返回值如果为0, 代表命令正确执行,没有报错; 如果不为0, 执行报错;
        # ...
        # >>> os.system('ping -c1 -w1 172.25.254.49 &> /dev/null')
        # 0
        # >>> os.system('ping -c1 -w1 172.25.254.1 &> /dev/null')
        # 256
        # """
        import os
        # 需要执行的shell命令
        cmd = 'ping -c1 -w1 %s &> /dev/null' %(self.ip)
        result = os.system(cmd)
        #  返回值如果为0, 代表命令正确执行,没有报错; 如果不为0, 执行报错;
        if result != 0:
            print("%s主机没有ping通" %(self.ip))
if __name__ == '__main__':
    print("打印172.25.254.0网段没有使用的IP地址".center(50, '*'))
    for i in range(1, 255):
        ip = '172.25.254.' + str(i)
        thread = GetHostAliveThread(ip)
        thread.start()

7.共享全局变量

(1)共享全局变量的优缺点

  • 优点: 在⼀个进程内的所有线程共享全局变量,能够在不使⽤其他⽅式的前提 下完成多线程之间的数据共享(这点要⽐多进程要好)
  • 缺点: 线程是对全局变量随意遂改可能造成多线程之间对全局变量 的混乱(即线程⾮安全)

(2)如何解决线程不安全问题

GIL(global interpreter lock): python解释器中任意时刻都只有一个线程在执行;
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
在这里插入图片描述
在这里插入图片描述

8.线程同步

线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。
同步就是协同步调,按预定的先后次序进⾏运⾏。如:你说完,我再说。
"同"字从字⾯上容易理解为⼀起动作 其实不是,
"同"字应是指协同、协助、互相配合。
在这里插入图片描述
在一个线程对内存地址进行操作时,对这个内存进行‘上锁‘,直到操作完解锁后下一个线程才能对该内存进行操作。否则内存里的数据就会混乱出错。(线程多的时候更容易出错,所以要上锁)

money = 0


def add():
    for i in range(1000000):
        global money
        lock.acquire()
        money += 1
        lock.release()
def reduce():
    for i in range(1000000):
        global money
        lock.acquire()
        money -= 1
        lock.release()
if __name__ == '__main__':
          from threading import  Thread, Lock
          # 创建线程锁
          lock = Lock()
          t1 = Thread(target=add)
          t2 = Thread(target=reduce)
          t1.start()
          t2.start()
          t1.join()
          t2.join()
      print(money)

死锁
在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时 等待对⽅的资源,就会造成死锁。

四.协程

1.协程

协程,又称微线程,纤程。英文名Coroutine。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

在这里插入图片描述

2.协程的优势

  • 执行效率极高,因为子程序切换(函数)不是线程切换,由程序自身控制,
  • 没有切换线程的开销。所以与多线程相比,线程的数量越多,协程性能的优势越明显。
  • 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁,因此执行效率高很多。

3.协程的实现方法

协程既可以用yield实现也可以用gevent实现。

  • gevent的基本思想:
    当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
  • 代码实现
import gevent

import requests
import json

from gevent import monkey
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from threading import Thread

from gevent import monkey
# 打补丁
monkey.patch_all()


def task(ip):
    """获取指定IP的所在城市和国家并存储到数据库中"""
    # 获取网址的返回内容
    url = 'http://ip-api.com/json/%s' % (ip)
    try:
        response = requests.get(url)
    except Exception as e:
        print("网页获取错误:", e)
    else:
        # 默认返回的是字符串
        """
        {"as":"AS174 Cogent Communications","city":"Beijing","country":"China","countryCode":"CN","isp":"China Unicom Shandong Province network","lat":39.9042,"lon":116.407,"org":"NanJing XinFeng Information Technologies, Inc.","query":"114.114.114.114","region":"BJ","regionName":"Beijing","status":"success","timezone":"Asia/Shanghai","zip":""}
        """
        contentPage = response.text
        # 将页面的json字符串转换成便于处理的字典;
        data_dict = json.loads(contentPage)
        # 获取对应的城市和国家
        city = data_dict.get('city', 'null')  # None
        country = data_dict.get('country', 'null')

        print(ip, city, country)
        # 存储到数据库表中ips
        ipObj = IP(ip=ip, city=city, country=country)
        session.add(ipObj)
        session.commit()


if __name__ == '__main__':
    engine = create_engine("mysql+pymysql://root:westos@172.25.254.123/pymysql",
                           encoding='utf8',
                           # echo=True
                           )
    # 创建缓存对象
    Session = sessionmaker(bind=engine)
    session = Session()

    # 声明基类
    Base = declarative_base()


    class IP(Base):
        __tablename__ = 'ips'
        id = Column(Integer, primary_key=True, autoincrement=True)
        ip = Column(String(20), nullable=False)
        city = Column(String(30))
        country = Column(String(30))

        def __repr__(self):
            return self.ip


    # 创建数据表
    Base.metadata.create_all(engine)

    # 使用协程
    gevents = [gevent.spawn(task, '1.1.1.' + str(ip + 1)) for ip in range(10)]
    gevent.joinall(gevents)
    print("执行结束....")

五.总结

在这里插入图片描述1. 线程与进程的区别?
根本区别:进程:是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
2. 进程间内存是否共享?如何实现通讯?
管道、信号、消息队列、信号量、套接字
3. 进程间通信的方式?
管道、信号、消息队列、信号量、套接字
4. 多线程有几种实现方法,都是什么?
继承Thread类,重写run()方法。然后直接new这个对象的实例,创建一个线程的实例,再调用start()方法启动线程。(其实本质上Thread是实现了Runnable接口的一个实例,Thread源文件:public class Thread implements Runnable)
实现Runnable接口,重写run()方法。然后调用new Thread(runnable)的方式创建一个线程,再调用start()方法启动线程。
实现Callable接口,重写call()方法。Callable是类似于Runnable的接口,是属于Executor框架中的功能类。是JDK1.5中引入的新特性。(不常用,作为了解引自于)
5.GIL锁是怎么回事?
GIL(global interpreter lock): python解释器中任意时刻都只有一个线程在执行
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
5. python中是否线程安全?如何解决线程安全?
GIL(global interpreter lock): python解释器中任意时刻都只有一个线程在执行
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
6. 什么叫死锁?
在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时 等待对⽅的资源,就会造成死锁。
7. 什么是协程?常用的协程模块有哪些?
协程,又称微线程,纤程。英文名Coroutine。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
8. 协程中的join是用来做什么用的?它是如何发挥作用的?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值