进程,线程,协程

目录

进程

进程的定义

  • 进程是资源分配的最小单位
  • 一个运行起来的程序就是一个进程

进程的注意点

  1. 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程
  2. 程序是指令的集合,它是进程运行的静态描述文本,进程是程序的一次执行活动,属于动态概念
  3. 在多道编程中,允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发的执行
  4. 进程的出现让每个用户感觉到自己独享CPU,因此,进程是为了在CPU上实现多道编程而提出的
  5. 进程之间有独立的内存,各进程之间不能相互访问
  6. 创建新进程需要对父进程进此复制

多道编程:在计算机内存中同时存放几道相互独立的程序,共享系统资源,相互穿插运行

单道编程:计算机内存中只允许一个程序运行

进程并发性:

  • 在一个系统中,同时会存在多个进程被加载到内存中,同处于开始到结束之间的状态
  • 对于一个单CPU系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽然都已经开始运行,但就微观而言,任意时刻,CPU上运行的程序只有一个
  • 由于操作系统分时,让每个进程都觉得自己独占CPU等资源

注:如果是多核CPU(处理器)实际上是可以实现真正意义的同一时间点有多个线程同时运行

注意:进程具有独立的内存空间,所以互相之间不能访问对方数据

进程间互相访问数据的四种方式:

  • 利用Queues实现父进程到子进程的数据传递(父子进程通信)
  • 使用管道pipe实现同一程序下两个进程通信
  • Managers实现同一程序下多个进程通信
  • 借助redis,Rabbit MQ中间件进行数据传递(不同进程间通信)

进程池

为什么需要进程池:

  • 一次性开启指定数量的进程
  • 防止进程开启数量过多导致服务器压力过大
from  multiprocessing import Process,Pool
import time,os
def foo(i):
    time.sleep(2)
    print("in the process",os.getpid()) #打印子进程的pid
    return i+100

def call(arg):
    print('-->exec done:',arg,os.getpid())

if __name__ == '__main__':
    pool = Pool(3)                      #进程池最多允许5个进程放入进程池
    print("主进程pid:",os.getpid())     #打印父进程的pid
    for i in range(10):
       #用法1 callback作用是指定只有当Foo运行结束后就执行callback调用的函数,父进程调用的callback函数
        pool.apply_async(func=foo, args=(i,),callback=call)
        #用法2 串行 启动进程不在用Process而是直接用pool.apply()
        pool.apply(func=foo, args=(i,))
    print('end')
    pool.close()    #关闭pool
    pool.join()     #进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭

进程和程序的区别

  • 程序是一个机器代码指令和数据的集合,所以程序是一个静态的实体
  • 进程是程序运行在数据集上的动态过程
  • 进程是系统进行资源分配和调度的一个独立单位
  • 一个程序对应多个进程,一个进程为多个程序服务
  • 一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一标识每个进程

有了进程为什么还要线程?

进程优点:

提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率

进程的两个重要缺点

  • 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
  • 进程在执行的过程中如果阻塞,即使进程中有些工作不依赖于输入的数据,也将无法执行(例如等待输入,整个进程就会挂起)。

线程

线程的定义

  • 线程是操作系统的调度的最小单位
  • 线程是进程的真正的执行者,是一些指令的集合(进程资源的拥有者)
  • 同一进程下的读多个线程共享内存空间,数据直接访问(数据共享)
  • 为了保证数据安全,必须使用线程锁

线程的注意点

  1. 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运行单位
  2. 一条线程是指进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
  3. 无论启动多少个线程,有多少个CPU,python在执行的时候在同一时刻只允许一个线程运行
  4. 所有在同一进程里的线程是共享同一块内存空间的,不同进程间内存空间不同
  5. 同一进程中的各线程可以相互访问资源,线程可以操作 同进程中的其他线程,但进程仅能操作子进程
  6. 两个进程想通信,必须要通过一个中间代理
  7. 对主线程修改可能会影响其他子线程,对主进程修改不会影响其他进程因为进程间内存相互独立,但是同一进程下的线程共享内存

GIL全局解释器锁

  • 在python全局解释器下,保证同一时间仅有一个线程对资源操作

  • 防止多个线程都修改数据

线程锁(互斥锁)

  • 当一个线程对某个资源进行CPU计算的操作时加一个线程锁,只有当前线程计算完成主动释放锁,其他线程才能对其操作
  • 可以解决还未计算完成,释放GIL锁后其他线程对这个资源操作导致混乱的问题
  • 线程锁本质把线程中的数据加了一把互斥锁
  • mysql中共享锁 & 互斥锁
  • mysql共享锁:共享锁,所有线程都能读,而不能写
  • mysql排它锁:排它,任何线程读取这个这个数据的权利都没有
  • 加上线程锁之后所有其他线程,读都不能读这个数据

线程和进程的区别

  • 进程包含线程
  • 线程共享内存空间
  • 进程内存是独立的(不可相互访问)
  • 进程可以生成子进程,子进程之间不能互相访问
  • 线程可以帮助应用程序同时做几件事情

for循环同时启动多个线程

import threading
import time
def sayhi(num): #定义每个线程要运行的函数
    print("running on number:%s" %num)
    time.sleep(3)
for i in range(10):
    t = threading.Thread(target=sayhi,args=('t-%s'%i,))
    t.start()

join()实现所有线程都执行结束后再执行主线程

import threading
import time
start_time = time.time()

def sayhi(num): #定义每个线程要运行的函数
    print("running on number:%s" %num)
    time.sleep(3)
t_objs = []    #将进程实例对象存储在这个列表中
for i in range(50):
    t = threading.Thread(target=sayhi,args=('t-%s'%i,))
    t.start()          #启动一个线程,程序不会阻塞
    t_objs.append(t)
print(threading.active_count())    #打印当前活跃进程数量
for t in t_objs: #利用for循环等待上面50个进程全部结束
    t.join()     #阻塞某个程序
print(threading.current_thread())    #打印执行这个命令进程

print("----------------all threads has finished.....")
print(threading.active_count())
print('cost time:',time.time() - start_time)

setDaemon()守护线程,主线程退出时,需要子线程随主线程退出

import threading
import time
start_time = time.time()
def sayhi(num): #定义每个线程要运行的函数
    print("running on number:%s" %num)
    time.sleep(3)
for i in range(50):
    t = threading.Thread(target=sayhi,args=('t-%s'%i,))
    t.setDaemon(True)  #把当前线程变成守护线程,必须在t.start()前设置
    t.start()          #启动一个线程,程序不会阻塞
print('cost time:',time.time() - start_time)

线程实现并发

import requests
from concurrent.futures import ThreadPoolExecutor

def fetch_request(url):
    result = requests.get(url)
    print(result.text)

url_list = [
    'https://www.baidu.com',
    'https://www.google.com/',         #google页面会卡住,知道页面超时后这个进程才结束
    'http://dig.chouti.com/',          #chouti页面内容会直接返回,不会等待Google页面的返回
]

pool = ThreadPoolExecutor(10)            # 创建一个线程池,最多开10个线程
for url in url_list:
    pool.submit(fetch_request,url)       # 去线程池中获取一个线程,线程去执行fetch_request方法

pool.shutdown(True)  

协程

协程的定义

  • 协程又称微线程,是一种用户状态的轻量级线程
  • 协程的本质是个单线程
  • 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈
  • 协程主要作用是在单线程的条件下实现并发的效果,但实际上还是串行

协程的缺点(无法利用多核资源)

  • 无法利用多核资源,协程的本质是个单线程,不能同时将单个CPU的多个核上,协程需要和进程配合才能运行在多CPU上
  • 线程阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

协程的优点

  • 可以处理高并发
  • 节省资源
  • 无需线程上下文切换的开销(可以理解为协程切换就是在不同函数间切换,不用像线程那样切换上下文CPU)
  • 用法: 最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

协程为何能处理大并发

  • greeenlet遇到I/O手动切换
    协程遇到I/O操作就切换,其实Gevent模块仅仅是对greenlet的封装,将I/O的手动切换变成自动切换
  1. 协程之所以快是因为遇到I/O操作就切换(最后只有CPU运算)
  2. 这里先演示用greenlet实现手动的对各个协程之间切换
  3. 其实Gevent模块仅仅是对greenlet的再封装,将I/O间的手动切换变成自动切换

greeenlet遇到I/O手动切换

from greenlet import greenlet

def test1():
    print(12)       #4 gr1会调用test1()先打印12
    gr2.switch()    #5 然后gr2.switch()就会切换到gr2这个协程
    print(34)       #8 由于在test2()切换到了gr1,所以gr1又从上次停止的位置开始执行
    gr2.switch()    #9 在这里又切换到gr2,会再次切换到test2()中执行

def test2():
    print(56)       #6 启动gr2后会调用test2()打印56
    gr1.switch()    #7 然后又切换到gr1
    print(78)       #10 切换到gr2后会接着上次执行,打印78

gr1 = greenlet(test1)    #1 启动一个协程gr1
gr2 = greenlet(test2)    #2 启动第二个协程gr2
gr1.switch()             #3 首先gr1.switch() 就会去执行gr1这个协程
  • Gevent遇到I/O自动切换
    Gevent是一个第三方库,可以通过gevent实现并发同步或异步编程,Gevent原理是只要是遇到I/O操作就自动切换下一个协程
  1. Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程
  2. 在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程
  3. Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
  4. Gevent原理是只要遇到I/O操作就会自动切换到下一个协程

使用Gevent实现并发下载网页与串行下载网页时间比较

from urllib import request
import gevent,time
from gevent import monkey
monkey.patch_all()      #把当前程序所有的I/O操作给我单独做上标记

def f(url):
    print('GET: %s' % url)
    resp = request.urlopen(url)
    data = resp.read()
    print('%d bytes received from %s.' % (len(data), url))

#1 并发执行部分
time_binxing = time.time()
gevent.joinall([
        gevent.spawn(f, 'https://www.python.org/'),
        gevent.spawn(f, 'https://www.yahoo.com/'),
        gevent.spawn(f, 'https://github.com/'),
])
print("并行时间:",time.time()-time_binxing)

#2 串行部分
time_chuanxing = time.time()
urls = [
        'https://www.python.org/',
        'https://www.yahoo.com/',
        'https://github.com/',
                                        ]
for url in urls:
    f(url)
print("串行时间:",time.time()-time_chuanxing)

# 注:为什么要在文件开通使用monkey.patch_all()
# 1. 因为有很多模块在使用I / O操作时Gevent是无法捕获的,所以为了使Gevent能够识别出程序中的I / O操作。
# 2. 就必须使用Gevent模块的monkey模块,把当前程序所有的I / O操作给我单独做上标记
# 3.使用monkey做标记仅用两步即可:
      第一步(导入monkey模块)from gevent import monkey
      第二步(声明做标记)    :   monkey.patch_all()

Gevent实现简单的自动切换小例子

注:在Gevent模仿I/O切换的时候,只要遇到I/O就会切换,哪怕gevent.sleep(0)也要切换一次

import gevent

def func1():
    print('\033[31;1m第一次打印\033[0m')
    gevent.sleep(2)          # 为什么用gevent.sleep()而不是time.sleep()因为是为了模仿I/O
    print('\033[31;1m第六次打印\033[0m')

def func2():
    print('\033[32;1m第二次打印\033[0m')
    gevent.sleep(1)
    print('\033[32;1m第四次打印\033[0m')

def func3():
    print('\033[32;1m第三次打印\033[0m')
    gevent.sleep(1)
    print('\033[32;1m第五次打印\033[0m')

gevent.joinall([            # 将要启动的多个协程放到event.joinall的列表中,即可实现自动切换
    gevent.spawn(func1),    # gevent.spawn(func1)启动这个协程
    gevent.spawn(func2),
    gevent.spawn(func3),
])

# 运行结果:
# 第一次打印
# 第二次打印
# 第三次打印
# 第四次打印
# 第五次打印
# 第六次打印
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值