08_多任务的实现(线程,进程,协程)

什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。

注意:

  • 并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
  • 并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的

1.线程

1.1线程的使用方法

python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用 

函数的多线程使用

import time
import threading

# 一个程序开始执行之后,会有一个主线程,当遇见xxx.start时创建子线程,主线程和子线程同时进行,
# 当主线程没有代码执行时,等待子线程运行完毕,主线程先结束,子线程肯定结束
# 线程的运行是没有顺序的(主线程和子线程以及子线程之间),可以添加time.sleep(xx)


def sing():
    for i in range(5):
        print("正在唱歌....")
        time.sleep(1)


def dance():
    for i in range(5):
        print("正在跳舞....")
        time.sleep(1)


def main():
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()  # start之后子线程1才开始
    # time.sleep(1)
    t2.start()  # start之后子线程2才开始


if __name__ == "__main__":
    main()

类的多线程使用

import threading
import time


class mythread(threading.Thread):

    def run(self):
        for i in range(5):
            print("run方法在使用xx.start之后调用")
        mythread.sing(self)
        mythread.dance(self)

    def sing(self):
        print("正在唱歌...")

    def dance(self):
        print("正在跳舞...")


if __name__ == "__main__":
    t = mythread()
    t.start()  
    # mythread继承了threading.Thread,调用start会自动调用run方法,其他的函数可以写到run方法里面

1.2多线程共享全局变量

from threading import Thread
import time

g_num = 100


def work1():
    global g_num
    for i in range(3):
        g_num += 1

    print("----in work1, g_num is %d---" % g_num)


def work2():
    global g_num
    print("----in work2, g_num is %d---" % g_num)


print("---线程创建之前g_num is %d---" % g_num)

t1 = Thread(target=work1)
t1.start()

#延时一会,保证t1线程中的事情做完
time.sleep(1)

t2 = Thread(target=work2)
t2.start()

print("---线程创建之后g_num is %d---" % g_num)

1.3 线程-共享全局变量问题(互斥锁)

假设两个线程t1和t2都要对全局变量g_num(默认是0)进行加1运算,t1和t2都各对g_num加10次,g_num的最终的结果应该为20。

但是由于是多线程同时操作,有可能出现下面情况:

  1. 在g_num=0时,t1取得g_num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得g_num=0
  2. 然后t2对得到的值进行加1并赋给g_num,使得g_num=1
  3. 然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给g_num。
  4. 这样导致虽然t1和t2都对g_num加1,但结果仍然是g_num=1
import threading
import time

g_num = 0


def test1(num):
    global g_num
    # 上锁,此时如果之前没有被上锁,此时上锁成功.
    # 如果之前被上锁那么此时会堵塞在这里,直到这把锁被解开为止
    mutex.acquire()
    for i in range(num):
        g_num += 1
    mutex.release()
    print("----in work1, g_num is %s---" % str(g_num))


def test2(num):
    global g_num
    mutex.acquire()
    for i in range(num):
        g_num += 1
    mutex.release()
    print("----in work2, g_num is %s---" % str(g_num))


# 创建一个互斥锁,默认是没有上锁的
mutex = threading.Lock()


def main():

    print("---线程创建之前g_num is %s---" % str(g_num))

    t1 = threading.Thread(target=test1, args=(1000000,))  # 将100增大到1000000 程序就会出错,在执行程序时会出现抢资源问题
    t2 = threading.Thread(target=test2, args=(1000000,))  # 循环次数较少时,这种情况出现的概率比较小

    t1.start()
    t2.start()

    time.sleep(5)  # 等待两个线程执行完毕

    print("---线程创建之后g_nums is %s---" % str(g_num))


if __name__ == "__main__":
    main()

上锁解锁过程

当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。

每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。

线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。

总结

锁的好处:

  • 确保了某段关键代码只能由一个线程从头到尾完整地执行

锁的坏处:

  • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
  • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁

2.进程

程序:例如xxx.py这是程序,是一个静态的

进程:一个程序运行起来后,代码+用到的资源 称之为进程,它是操作系统分配资源的基本单元。

不仅可以通过线程完成多任务,进程也是可以的

 使用进程实现多任务,在执行程序时,会将程序复制一份给子线程.

使用线程实现多任务子线程并没有把程序复制一遍进行执行

2.1 使用进程解决多任务问题

import time
import multiprocessing


def test1():
    while True:
        print("正在唱歌....")
        time.sleep(1)


def test2():
    while True:
        print("正在跳舞....")
        time.sleep(1)


def main():
    t1 = multiprocessing.Process(target=test1)
    t2 = multiprocessing.Process(target=test2)
    t1.start()  # start之后子线程1才开始
    # time.sleep(1)
    t2.start()  # start之后子线程2才开始


if __name__ == "__main__":
    main()

 2.2 进程间不同享全局变量,使用队列解决进程间通信问题

import multiprocessing


def download_from_web(q):
    """下载数据"""
    # 模拟从网上下载数据
    data = [11, 22, 33]
    # 向队列写入数据
    for i in data:
        q.put(i)
    print("下载数据完毕....")


def analysis_data(q):
    """分析数据"""
    store_data = list()
    while True:
        data = q.get()
        store_data.append(data)
        if q.empty():   # 判断队列是否为空,为空跳出循环
            break
    print(store_data)


def main():
    # 创建队列
    q = multiprocessing.Queue()
    # 创建多个进程,将队列的引用当做实参传递到里面
    p1 = multiprocessing.Process(target=download_from_web, args=(q,))
    p2 = multiprocessing.Process(target=analysis_data, args=(q,))

    p1.start()
    p2.start()


if __name__ == "__main__":
    main()

2.3进程池pool

当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块提供的Pool方法。

初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务,请看下面的实例:

# -*- coding:utf-8 -*-
from multiprocessing import Pool
import os, time, random


def worker(msg):
    t_start = time.time()
    print("%s开始执行,进程号为%d" % (msg,os.getpid()))
    # random.random()随机生成0~1之间的浮点数
    time.sleep(random.random()*2)
    t_stop = time.time()
    print(msg, "执行完毕,耗时%0.2f" % (t_stop-t_start))


po = Pool(3)  # 定义一个进程池,最大进程数3
for i in range(0,10):
    # Pool().apply_async(要调用的目标,(传递给目标的参数元祖,))
    # 每次循环将会用空闲出来的子进程去调用目标
    po.apply_async(worker, (i,))

print("----start----")
po.close()  # 关闭进程池,关闭后po不再接收新的请求
po.join()  # 等待po中所有子进程执行完成,必须放在close语句之后
print("-----end-----")

3.协程

协程是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)。 为啥说它是一个执行单元,因为它自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。

通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定

3.1迭代器

迭代是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。

可迭代对象的本质就是可以向我们提供一个这样的中间“人”即迭代器帮助我们对其进行迭代遍历使用。

可迭代对象通过__iter__方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据.

那么也就是说,一个具备了__iter__方法的对象,就是一个可迭代对象。

通过上面的分析,我们已经知道,迭代器是用来帮助我们记录每次迭代访问到的位置,当我们对迭代器使用next()函数的时候,迭代器会向我们返回它所记录位置的下一个位置的数据。实际上,在使用next()函数的时候,调用的就是迭代器对象的__next__方法(Python3中是对象的__next__方法,Python2中是对象的next()方法)。所以,我们要想构造一个迭代器,就要实现它的__next__方法。但这还不够,python要求迭代器本身也是可迭代的,所以我们还要为迭代器实现__iter__方法,而__iter__方法要返回一个迭代器,迭代器自身正是一个迭代器,所以迭代器的__iter__方法返回自身即可。

一个实现了__iter__方法和__next__方法的对象,就是迭代器。

import time


class Classmate(object):
    def __init__(self):
        self.names = list()
        self.current = 0

    def add(self, name):
        self.names.append(name)

    def __iter__(self):
       return self

    def __next__(self):
        if self.current < len(self.names):
            ret = self.names[self.current]
            self.current += 1
            return ret
        else:
            raise StopIteration

classmate = Classmate()
classmate.add("张三")
classmate.add("李四")
classmate.add("王五")

# print("判断classmate是不是可迭代的对象:", isinstance(classmate, Iterable))

for name in classmate:
    print(name)
    time.sleep(1)

3.2 生成器

利用迭代器,我们可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成。但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据。为了达到记录当前状态,并配合next()函数进行迭代使用,我们可以采用更简便的语法,即生成器(generator)。生成器是一类特殊的迭代器

简单来说:只要在def函数中有yield关键字的 就称为 生成器

# 生成器的作用是产生结果的方式,不能直接输出结果
nums = [x for x in range(10)]  # 列表表达式
print(nums)

num = (x for x in range(10))  # 生成器
for i in num:  # 生成器产生的结果需要for循环依次取得
    print(i)

print("---------分割线--------")


def Fibonacii(all_nums):
    a = 0
    b = 1
    current_num = 0

    while current_num < all_nums:
        # print(a)
        yield a
        a, b = b, a+b
        current_num += 1


print("----------获取生成器结果的方式--------")
f = Fibonacii(10)
for i in f:  # 产生生成器的结果
    print(i)

print("----------获取生成器结果的方式--------")
f1 = Fibonacii(10)
print(next(f1))
print(next(f1))
print(next(f1))
print(next(f1))
print(f1.send(None))  # send也可以获取生成器的值,还可以传参,send不能作为第一个获取方式
print(next(f1))
print(f1.send("传参"))
print(next(f1))

3.3 yield -协程

import time


def task1():
    while True:  # 如果这里不使用while true程序只执行一遍
        print("-----task1------")
        yield  # 遇到yield之后暂停去执行其他的
        time.sleep(0.5)


def task2():
    while True:
        print("-----task2------")
        yield
        time.sleep(0.5)


def main():
    t1 = task1()
    t2 = task2()

    while True:
        next(t1)  # 执行t1时遇到yield暂停开始执行t2,在t2执行时遇到yield然后执行t1,循环执行
        next(t2)


if __name__ == "__main__":
    main()

3.4 greenlet-协程

使用如下命令安装greenlet模块:

sudo pip3 install greenlet
from greenlet import greenlet
import time


def task1():
    while True:  # 如果这里不使用while true程序只执行一遍
        print("-----task1------")
        gr2.switch()
        time.sleep(0.5)


def task2():
    while True:
        print("-----task2------")
        gr1.switch()
        time.sleep(0.5)


gr1 = greenlet(task1)
gr2 = greenlet(task2)

gr1.switch()

3.5 gevent-协程(采用此方法实现协程)

import gevent
import time

# 协程其实是单线程交替进行,在这里gevent实现多任务的方法是:在程序中遇到耗时比较大的时候,或者程序堵塞的时候,
# 将目前执行程序切换成其他程序执行,这样来回交替,主要是将耗时的操作转成其他操作


def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        # time.sleep(1)  # 不能实现多任务,需要使用gevent.sleep
        # 用来模拟一个耗时操作,注意不是time模块中的sleep
        gevent.sleep(1)


g1 = gevent.spawn(f, 5)  # 创建一个实例,并不执行
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)

g1.join()  # 等待g1执行完毕,这时堵塞,切换到g2去执行
g2.join()
g3.join()

一般采用下面的写法(采用此方法实现协程):

import gevent
from gevent import monkey
import time

# 协程其实是单线程交替进行,在这里gevent实现多任务的方法是:在程序中遇到耗时比较大的时候,或者程序堵塞的时候,
# 将目前执行程序切换成其他程序执行,这样来回交替,主要是将耗时的操作转成其他操作

# 这条语句在执行程序时会将程序中的耗时操作转化为gevent.sleep,也就是说在程序中可以使用time.sleep,也可以出现
# 堵塞的情况,比如udp或tcp出现的网络堵塞

monkey.patch_all()


def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        time.sleep(1)  # 不能实现多任务,需要使用gevent.sleep
        # 用来模拟一个耗时操作,注意不是time模块中的sleep
        # gevent.sleep(1)


# g1 = gevent.spawn(f, 5)  # 创建一个实例,并不执行
# g2 = gevent.spawn(f, 5)
# g3 = gevent.spawn(f, 5)
#
# g1.join()  # 等待g1执行完毕,这时堵塞,切换到g2去执行
# g2.join()
# g3.join()

# 下面这条语句使用替换了上述调用的方法
gevent.joinall([
    gevent.spawn(f, 5),
    gevent.spawn(f, 5)
])

4.线程 进程 协程的对比

请仔细理解如下的通俗描述

  • 有一个老板想要开个工厂进行生产某件商品(例如剪子)
  • 他需要花一些财力物力制作一条生产线,这个生产线上有很多的器件以及材料这些所有的 为了能够生产剪子而准备的资源称之为:进程
  • 只有生产线是不能够进行生产的,所以老板的找个工人来进行生产,这个工人能够利用这些材料最终一步步的将剪子做出来,这个来做事情的工人称之为:线程
  • 这个老板为了提高生产率,想到3种办法:
    1. 在这条生产线上多招些工人,一起来做剪子,这样效率是成倍増长,即单进程 多线程方式
    2. 老板发现这条生产线上的工人不是越多越好,因为一条生产线的资源以及材料毕竟有限,所以老板又花了些财力物力购置了另外一条生产线,然后再招些工人这样效率又再一步提高了,即多进程 多线程方式
    3. 老板发现,现在已经有了很多条生产线,并且每条生产线上已经有很多工人了(即程序是多进程的,每个进程中又有多个线程),为了再次提高效率,老板想了个损招,规定:如果某个员工在上班时临时没事或者再等待某些条件(比如等待另一个工人生产完谋道工序 之后他才能再次工作) ,那么这个员工就利用这个时间去做其它的事情,那么也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,其实这就是:协程方式

简单总结

  1. 进程是资源分配的单位
  2. 线程是操作系统调度的单位
  3. 进程切换需要的资源很最大,效率很低
  4. 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
  5. 协程切换任务资源很小,效率高
  6. 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发
  7. 按效率来分,协程>线程>进程

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值