多任务编程(多进程,多线程,协程)

一、多任务编程

1.多任务的概念

  1. 现实⽣活中的多任务:有很多的场景中的事情是同时进⾏的,⽐如开⻋的时候 ⼿和脚共同来驾驶汽⻋,再⽐如唱歌跳舞也是同时进⾏的。

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

  3. 现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
    ——答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

  4. 真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

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

操作系统轮流让各个任务交替执⾏,每个任务执⾏0.01秒,这样反复执⾏下去。 表⾯上看,每个任务交替执⾏,但CPU的执⾏速度实在是太快了,感觉就像所有任务都在同时执⾏⼀样。
在这里插入图片描述
三个执行实例A,B,C在单个CPU上交替执行
逻辑上表现为三个执行实例并发执行
但实质物理上仍然时串行执行

  • 串行:一个处理完再一个
  • 并行:指两个或者多个事件在同一时刻发生;
  • 并发:指两个或多个事件在同一时间间隔内发生。
  • 例如:并发时你正在吃饭,吃到一半电话响,去接电话,接完后继续吃饭;并行时边吃饭边打电话

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

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

二、多进程编程

1.进程的创建

1)程序和进程的区别

程序:编写完的代码,没有运行
进程:正在运行着的代码/程序,需要运行的环境等

2)进程的五状态模型

在这里插入图片描述
程序运行时的三种流程:

  1. 创建(created)----就绪(ready)----运行(running)----阻塞(waiting)(这时如果是多进程,在等待的过程就可以让进程去做别的事情)----结束(terminated)
  2. 创建(created)----就绪(ready)----运行(running)----就绪(ready)----运行(running)----结束(terminated)
  3. 创建(created)----就绪(ready)----运行(running)----结束(terminated)
3)创建子进程

python的os模块封装了常见的系统调用,其中就包括fork(分支,其实是克隆),可以在python程序中轻松创建子进程;
python的os模块中的fork()函数,用来创建子进程,但是只能用在Linux系统中。
在这里插入图片描述
fork()函数理解加图解:

  • 调用fork()函数时,操作系统会得到一个父进程本身,并创建一个子进程复制父进程所有的信息。
  • 普通的函数调用,调用一次,返回一次,但是fork()函数调用一次,返回两次
  • 新得到的父进程和子进程都会从fork()函数中得到一个返回值,子进程返回的是0,父进程返回新创建的子进程的id(即pid=process id=进程的id)
  • 多进程中,虽然子进程数据是由父进程的数据复制而来,但是子进程和父进程之间是不关联的,互相独立,互不影响(如代码中的全局变量)

实例1

import os
import time
#定义一个全局变量money
money=100
print('当前进程的pid:',os.getpid()) #pid=process id 进程的id。 os.getpid()表示获取当前进程的
time.sleep(5)
print('当前进程的父进程pid:',os.getppid())
#ppid=parent process id 。os.getppid()表示获取当前进程的父进程的id

p=os.fork()
#子进程返回的是0
if p ==0:
    money=200
    print('子进程返回的信息,money=%d'%(money))
#父进程返回的是新创建的子进程的pid
else:
    print('创建子进程%s,父进程是%d'%(p,os.getppid()))
    print(money)

windows系统中的os模块没有fork()函数,只能在linux系统下进行操作

2.多进程编程

1)概念

由于Windows没有fork调⽤,但是Python是跨平台的,因此有一个multiprocessing模块就是跨平台版本的多进程模块。
multiprocessing模块提供了⼀个Process类来代表⼀个进程对象。
1.muitiprocessing模块的参数:
Process([group [, target [, name [, args [, kwargs]]]]])

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

2.Process类常⽤⽅法:

  • is_alive() : 判断进程实例是否还在执⾏;
  • join([timeout]) : 是否等待进程实例执⾏结束,或等待多少秒;
  • start() : 启动进程实例(创建⼦进程);
  • run() : 如果没有给定target参数,对这个对象调⽤start()⽅法时,就将执⾏对象中的run()⽅法;
  • terminate() : 不管任务是否完成,⽴即终⽌;

3.Process类常⽤属性:

  • name:当前进程实例别名,默认Process-N,N为从1开始计数;
  • pid:当前进程实例的PID值
2)多进程编程方法
1.方法一 :实例化对象
import time
from multiprocessing import Process
def task1():
    print('正在听音乐')
    time.sleep(1)
def task2():
    print('正在编程...')
    time.sleep(0.5)
def no_multi():
    for i in range(2):
        task1()
    for i in range(5):
        task2()
def use_multi():
    processes=[]
    for i in range(2):
        p=Process(target=task1,)#实例化一个对象,就创建一个子进程,进程执行task1
        p.start()#让进程执行任务
        processes.append(p)
    for i in range(5):
        p=Process(target=task2,)
        p.start()
        processes.append(p)
    [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)

运行结果

正在编程...
正在听音乐
正在听音乐
正在编程...
正在编程...
正在编程...
正在编程...
1.4573063850402832

当不使用多进程时,运行结果为

正在听音乐
正在听音乐
正在编程...
正在编程...
正在编程...
正在编程...
正在编程...
4.503279209136963

由此可见,使用多进程编程可以有效提高效率

注意:对于代码中join()方法的使用

  • 在主进程的任务与子进程的任务彼此独立的情况下,主进程的任务先执行完毕后,主进程还需要等待子进程执行完毕,然后统一回收资源。
  • 如果主进程的任务在执行到某一个阶段时,需要等待子进程完毕后才能继续执行,就需要有一种机制能够让主进程检测子进程是否运行完毕。
  • 在子进程执行完毕后才继续执行,否则一直在原地阻塞,这就是join方法的作用。
def use_multi():
    processes=[]
    p1=Process(target=task1,)#实例化一个对象,就创建一个子进程,进程执行task1
    p1.start()#让进程执行任务
    # p1.join() 若将阻塞写在这里,由于p2还没有启动,只有当p1执行结束后,才开始执行p2,所以不是多进程同时进行,然后再
    p2=Process(target=task2,)
    p2.start()
    p1.join()
    p2.join()#将阻塞写在这里就是将p1和p2都开启了,然后让这两个进程交替进行,阻塞是在两个同时开启的情况下,一个一个进行       
2.方法二:创建子类(继承的式)

即重写run()方法,run()方法里就是要执行的任务

from multiprocessing import Process
import time
class MyProcess(Process):
    """
    创建自己的进程,父类是Process
    """
    def __init__(self,music_name):
        super(MyProcess, self).__init__()#继承父类的构造方法
        self.music_name=music_name#将self对象和参数绑定在一起
    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('音乐%s'%(i))#这里的音乐是music_name,是定义类时定义的参数
        p.start()

运行结果

听音乐音乐2
听音乐音乐0
听音乐音乐4
听音乐音乐6
听音乐音乐1
听音乐音乐9
听音乐音乐5
听音乐音乐3
听音乐音乐7
听音乐音乐8
3.方法三:使用进程池Pool
(1)为什么需要进程池
  • 当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态成生多个进程, 十几个还好,但如果是上百个,上千个目标,手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。
  • Pool可以提供指定数量的进程供用户调用,当有新的请求提交到pool中时,如果池还没有满, 那么就会创建一个新的进程用来执行该请求;
  • 但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来它。

在这里插入图片描述

(2)进程池的使用方法
from multiprocessing import Pool
from multiprocesssing import cpu_count
p=Pool(cpu_count()) #cpu_count表示计算是几核的cpu,表示返回一个有几核的进程池对象
p.map(task,list())#执行的任务,list中是任务需要的参数
p.close() #关闭进程池对象
p.join()
(3)实例求1000-1200之间的素数:
from multiprocessing import Process
import time
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))
#判断1000-1200之间的素数
def no_multi():
    for num in range(1000,1201):
        task(num)
def use_multi():
    processes=[]
    for num in range(1000,1201):
        #实例化子进程对象
        p=Process(target=task,args=(num,))#args是元组
        #开启子进程
        p.start()
        #存储所有的子进程
        processes.append(p)
        #阻塞子进程,等待每个子进程执行结束
        for process in processes:
            process.join()
def use_pool():
    from multiprocessing import Pool
    from multiprocessing import cpu_count
    p=Pool(cpu_count())#计算本机有几个cpu,并实例化出一个有几个cpu的进程池
    p.map(task,list(range(1000,1201)))
    p.close()
    p.join()#等所有子进程结束后再执行主程序
if __name__ == '__main__':
    start_time=time.time()
    no_multi()
    #use_multi()
    #use_pool()
    end_time=time.time()
    print(end_time-start_time)

运行结果–不使用多进程和进程池

.....
1187是素数
1193是素数
0.0019941329956054688

–使用多进程

1181是素数
1187是素数
1193是素数
14.319111347198486#这里时间反而更长,因为开启的子进程太多了,耗费时间和内存空间。

–使用进程池

1181是素数
1187是素数
1193是素数
0.9870932102203369#还是比不用进程池的时间长,是因为数据量有点少

3.进程间通信

1)进程间通信的目的:

在这里插入图片描述
数据传输、共享数据、通知事件、资源共享、进程控制(kill -9 id)

2)进程间通信的方式

在这里插入图片描述
管道、信号(kill -15 id正常关闭,kill -1 id 重新加载进程)、消息队列、信号量、套接字(在不同主机间进行通信)

3)消息队列

可以使⽤multiprocessing模块的Queue实现多进程之间的数据传递,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]]) :
    ⼊队,block默认值 为True(如果队列满了不能入队,一直等待,等待有位置可以入队了就入队)
  • Queue.put_nowait(item) : 相当于Queue.put(item, False)
实例:完成进程A和进程B之间的9次通信
from multiprocessing import Process
from multiprocessing import Queue
import time
class Product(Process):
    def __init__(self,queue):
        super(Product, self).__init__()
        self.queue=queue
    def run(self):
        """将需要通信的数据写入队列中"""
        for i in range(10):
            self.queue.put(i)
            time.sleep(0.1)
            print('进程A向进程B传递信息,内容为%s'%(i))
class Consumer(Process):
    def __init__(self,queue):
        super(Consumer, self).__init__()
        self.queue=queue
    def run(self):
        while True:
            time.sleep(0.1)
            data=self.queue.get()
            print('进程B接收到进程A传递的信息:',data)
if __name__ == '__main__':
    q1=Queue()
    p1=Product(q1)
    c1=Consumer(q1)
    p1.start()
    c1.start()
    p1.join()
    c1.join()

运行结果

进程B接收到进程A传递的信息: 0
进程A向进程B传递信息,内容为0
进程A向进程B传递信息,内容为1
进程B接收到进程A传递的信息: 1
进程A向进程B传递信息,内容为2
进程B接收到进程A传递的信息: 2
进程A向进程B传递信息,内容为3
进程B接收到进程A传递的信息: 3
进程B接收到进程A传递的信息: 4
进程A向进程B传递信息,内容为4
进程A向进程B传递信息,内容为5
进程B接收到进程A传递的信息: 5
进程A向进程B传递信息,内容为6
进程B接收到进程A传递的信息: 6
进程A向进程B传递信息,内容为7
进程B接收到进程A传递的信息: 7
进程A向进程B传递信息,内容为8
进程B接收到进程A传递的信息: 8
进程A向进程B传递信息,内容为9
进程B接收到进程A传递的信息: 9

三、多线程编程

1.什么是线程

  • 线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程
  • 线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
  • 同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
  • 一个进程可以有很多线程,每条线程并行执行不同的任务。
  • 在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。

2.适用范围

  1. 服务器中的文件管理或通信控制
  2. 前后台处理
  3. 异步处理

3.特点

在多线程OS中,通常在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。
线程具有以下属性:

1)轻型实体

线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。

线程的实体包括程序数据TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。

TCB包括以下信息:

  1. 线程状态
  2. 当线程不运行时,被保存的现场资源。
  3. 一组执行堆栈
  4. 存放每个线程的局部变量主存区。
  5. 访问同一个进程中的主存和其它资源。

总结: 用于指示被执行指令序列的程序计数器保留局部变量少数状态参数返回地址等的一组寄存器和堆栈

2)独立调度和分派的基本单位

在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)

3)可并发执行

在**一个进程中的多个线程之间,可以并发执行,**甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。

4)共享进程资源
  • 在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。

在这里插入图片描述

  • 每个进程至少由一个线程,即进程本身。进程可以启动多个线程。操作系统像并行“进程”一样并行执行这些线程。
    在这里插入图片描述
  • 线程的集中状态:
    在这里插入图片描述

4.线程和进程的区别

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

5.线程分类

  • 内核线程–运行程序时计算机默认开启的线程
  • 用户线程或用户空间线程

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

6.多线程编程

跟多进程类似,python的thread模块是⽐较底层的模块(更加详细,建议不要用它来管理多线程),python的threading模块(其中thread类)是对thread做了⼀些包装的,可以更加⽅便的被使⽤。
(python2中是thread,python3中成为_thread)

例如:

import threading
if __name__ == '__main__':
    """
    一个进程中一定有一个线程,叫主线程,用来管理其他线程
    """
    print('当前线程个数:',threading.active_count())#运行这个程序就是一个进程,一个进程一定有一个线程
    print('当前线程信息:',threading.current_thread())

运行结果

当前线程个数: 1
当前线程信息: <_MainThread(MainThread, started 21520)>

表明只有一个线程在运行,这个线程为主线程

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)

运行结果

听音乐....
听音乐....
听音乐....
听音乐....
听音乐....
1.0358271598815918

项目实例: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:
        # 默认返回的是字符串
        """
        {"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:anning@127.0.0.1/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())

分析:

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

项目案例:基于多线程的批量主机存活探测

项目描述: 如果要在本地网络中确定哪些地址处于活动状态或哪些计算机处于活动状态,则可以使用此脚本。我们将依次ping地址, 每次都要等几秒钟才能返回值。这可以在Python中编程,在IP地址的地址范围内有一个for循环和一个os.popen(“ping -q -c2”+ ip)。
项目瓶颈: 没有线程的解决方案效率非常低,因为脚本必须等待每次ping。

import os
from threading import Thread
class GetHostAliveThread(Thread):
    """创建子线程,执行的任务:判断指定的ip是否活着"""
    def __init__(self,ip):
        super(GetHostAliveThread, self).__init__()
        self.ip=ip
    def run(self):
        """
        重写run()方法:判断指定的IP是否活着
        执行shell命令行语句
        os.system()返回值为0:命令正确执行,不报错;返回值不为0:执行报错
        os.system('ping -c1 -w1 127.0.0.1 &> /dev?null') 其中&> /dev?null不显示信息
        0
        os.system('ping -c1 -w1 172.25.254.1 &> /dev/null')
        256
        """
        #执行的shell命令
        cmd='ping -c1 -w1 %s'%(self.ip)
        result=os.system(cmd)
        if result!=0:
            print('%s主机没有ping通'%(self.ip))

if __name__ == '__main__':
    print('打印127.0.0.0网段没有适用的IP地址'.center(10,'*'))
    for i in range(1,255):
        ip='127.0.0.'+str(i)
        thread=GetHostAliveThread(ip)
        thread.start()

运行结果

....
127.0.0.65主机没有ping通
127.0.0.58主机没有ping通
127.0.0.50主机没有ping通
127.0.0.66主机没有ping通
127.0.0.27主机没有ping通
127.0.0.5主机没有ping通
....

6.GIL全局解释器锁

1)共享全局变量

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

2)GIL–解决线程不安全问题

GIL(global interpreter lock):python解释器中任意时刻都只有一个线程在执行。

  • python代码的执行由python虚拟机(也叫解释器主循环,Cpython版本)来控制,python在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
  • 即当python解释器中有一个线程在运行时,就给解释器上一个锁,不允许其他线程在这个线程运行时使用全局解释器,直到这个线程运行结束后,锁才打开,别的线程可以使用。
  • 若想要允许同时有多个线程运行,可以使用Jpython版本的解释器。
    如果是IO密集型任务,使用多线程;如果是CPU密集型,使用多进程
    在这里插入图片描述

7.线程同步和线程锁

1)线程同步
  • 同步就是协同步调,按预定的先后次序进⾏运⾏。如:你说完,我再说。
    "同" 字从字⾯上容易理解为⼀起动作 其实不是,
    "同" 字应是指协同、协助、互相配合。
  • 线程同步: 即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。
    在这里插入图片描述
    用一个例子进一步理解:银行存取钱
from threading import Thread
money=0
def add():
    for i in range(1000000):
        global money
        money +=1
def reduce():
    for i in range(1000000):
        global money
        money -=1
if __name__ == '__main__':
    t1=Thread(target=add)
    t2=Thread(target=reduce)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(money)

运行结果

12353

结果分析:
按我们代码预想,想要的结果应该是0 ,然而结果并不是;
这是因为在数据量非常大的时候,在运算的过程中,多个线程可能会出现不同步的现象;
一开始money=0,当第一个线程add来的时候money += 1 ,money只是加了1,这个时候还没有将1赋给money,money仍然是0,这时候第二个线程reduce来了,他直接在money还是0的基础上就去减1;或者是在进行reduce线程时还没有将运算结果提交,只是在0的基础上加了1 ,因此种种差错结果并不是我们想要的。
原因:
这是因为多线程共享全局变量;
线程对全局变量随意修改可能造成多线程之间对全局变量的混乱。

因此,在对数据进行更改时怎么实现线程同步?
需要用到线程锁

2)线程锁

线程锁:多线程可以同时运行多个任务但是当多个线程同时访问共享数据时,可能同时对数据修改,造成数据不同步,从而产生错误的结果。因此需要线程锁。

线程锁的实现:

  • #实例化一个锁对象,主函数中
    lock = threading.Lock()
  • #操作变量之前进行加锁
    lock.acquire()
  • #操作变量之后进行解锁
    lock.release()

项目实例:银行存取钱

from threading import Thread
from threading import Lock

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__':
    t1=Thread(target=add)
    t2=Thread(target=reduce)
    #实例化一个线程锁
    lock=Lock()
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(money)

运行结果

0

8.死锁

1)定义

在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时 等待对方的资源,就会造成死锁。
在这里插入图片描述

在这里插入图片描述
比如说:
A有1个苹果1个手机,B有1个梨1个电脑
A说:你先把你的梨给我,我就把我的苹果给你
B说:你得先把你的手机给我,我就把我的电脑给你
他俩都在等待对方给资源,这样造成的情况就可以简单理解为死锁。

项目实例:转账操作

import time
import threading
class Account(object):
    def __init__(self,id,money,lock):
        self.id=id
        self.money=money
        self.lock=lock
    def reduce(self,money):
        self.money-=money
    def add(self,money):
        self.money+=money
def transfer(_from,to,money):
    if _from.lock.acquire():
        _from.reduce(money)
        #进程间有一个争夺资源的过程,所以设置一个时间的延迟
        #time.sleep(1)
        if to.lock.acquire():
            to.add(money)
            to.lock.release()
        _from.lock.release()
if __name__ == '__main__':
    a=Account('a',1000,threading.Lock())
    b=Account('b',1000,threading.Lock())

    t1=threading.Thread(target=transfer,args=(a,b,200))
    t2=threading.Thread(target=transfer,args=(b,a,100))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(a.money)
    print(b.money)

运行结果

900
1100

若将在两个线程之间加上一个时间等待,则程序就会产生死锁。

2)产生死锁的原因
  • 竞争系统资源
  • 进程运行推进的顺序不当
  • 资源分配不当
3)产生死锁的四个必要条件
  • 互斥条件:一个资源每次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
4)解决死锁的方法
  1. 减少资源占用时间,可以降低死锁放生的概率。
  2. 银行家算法。银行家算法的本质是优先满足占用资源较少的任务。
  3. 理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。

所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。

四、协程

1、什么是协程

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

  • 进程 — 线程 — 协程
  • 可以对应理解为:程序 — 函数 — 函数片段,在执行到一个函数的某个语句时,跳到另外一个函数的某个语句去执行,执行一会儿后又跳回到原来停止的地方执行;
  • 和yield类似,可以通过yield去实现协程。

2.协程的优势

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

3.协程的实现

1)方法一:yield实现
import time
def consumer():
    r=''
    while True:
        n=yield r #停止
        if not n :
            return
        print('[consumer]Consuming %s..'%(n))
        time.sleep(1)
        r='200 ok'
def produce(c):
    c.next() #执行consumer生成器内容
    n=0
    while n<5:
        n=n+1
        print('[producer] Producing %s..'%(n))
        r=c.send(n) #把n=1发送给consumer
        print('[producer] Consumer return: %s'%(r))
    c.close()
if __name__ == '__main__':
    c=consumer()  #consumer()函数中有yeild,所以返回的c是一个生成器
    produce(c)
2)方法二:gevent模块实现

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

@timeit
def use_gevent():
    gevents=[gevent.spawn(get_page_length,url)for url in urls]#gevent.spawn与之前的target类似,就是协程接收的任务,及需要的参数url
    gevent.joinall(gevents)#等待左右的协程执行结束再去执行主进程或主线程
    print('协程执行结束...')

项目实例

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

#打补丁,为了让程序自动
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:anning@127.0.0.1/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. IO密集型的任务:有阻塞状态,需要等待,不会一直占用CPU。
    建议使用多线程编程
  2. 计算密集型的任务(即CPU密集型):没有阻塞状态,一直占用CPU
    建议使用多进程编程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值