Python基础之多任务

一:多任务概念

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

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

注意:

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

二:线程

python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用
python的多线程不适合cpu密集操作型的任务,适合io操作密集型的任务

2.1 使用threading模块

单线程代码的执行:

import threading
import time
def coding():
    for x in range(3):
        print('coding --> %s' % x)
        time.sleep(1)
def drawing():
    for x in range(3):
        print('drawing --> %s' %x)
        time.sleep(1)
def main():
    coding()
    drawing()
if __name__ == '__main__':
    main()

多线程方式执行:

import threading
import time
def coding():
    for x in range(3):
        print('coding --> %s' % x)
        time.sleep(1)
def drawing():
    for x in range(3):
        print('drawing --> %s' %x)
        time.sleep(1)   
def multi_thread():
    t1 = threading.Thread(target = coding)
    t2 = threading.Thread(target = drawing)
    t1.start()
    t2.start()
if __name__ == '__main__':
    multi_thread()

2.2 查看线程信息

import threading
import time
def test1():
    for i in range(2):
        print("----- test1-%d-----" % i)
        time.sleep(1)
# 如果创建Thread时执行的函数运行结束那么就意味着这个子线程结束了
def test2():
    for i in range(2):
        print("----- test2-%d-----" % i)
        time.sleep(1)
def main():
    t1 = threading.Thread(target = test1)
    t2 = threading.Thread(target = test2)
    t1.start()      # 只有调用start()方法时子线程才会被创建
    t2.start()
    while True:
        print(threading.enumerate())
        if len(threading.enumerate()) <= 1: # 判断所有子线程是否已经结束
            break
        time.sleep(1)
# 如果主线程较子线程之前结束,那么所有子线程也都结束
if __name__ == '__main__':
    main()

执行结果如下:

----- test1-0-----
----- test2-0-----
[<_MainThread(MainThread, started 19720)>, <Thread(Thread-1, started 14652)>, <Thread(Thread-2, started 18456)>]
[<_MainThread(MainThread, started 19720)>, <Thread(Thread-1, started 14652)>, <Thread(Thread-2, started 18456)>]
----- test2-1-----
----- test1-1-----
[<_MainThread(MainThread, started 19720)>, <Thread(Thread-1, started 14652)>, <Thread(Thread-2, started 18456)>]
[<_MainThread(MainThread, started 19720)>]

2.3 线程执行代码的封装

通过上一小节,能够看出,通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread就可以了,然后重写run方法。t.start()时执行的就是run方法里面的代码(如果有其他方法可将其加入到run方法中执行)

class MyThread(threading.Thread):
    def run(self) -> None:
        for i in range(3):
            time.sleep(1)
            message = 'I am ' + self.name + '@' + str(i)
            # name属性中保存的是当前线程的名字
            print(message)
if __name__ == '__main__':
    t= MyThread()
    t.start()

或者:

class MyThread(threading.Thread):
    def __init__(self,num):
        threading.Thread.__init__(self)
        self.num = num
    def run(self):#定义每个线程要运行的函数
        print("running on number:%s" %self.num)
        time.sleep(3)
if __name__ == '__main__':
    t1 = MyThread(1)
    t2 = MyThread(2)
    t1.start()
    t2.start()

2.4 共享全局变量

在一个函数中对全局变量进行修改时,到底是否需要使用global进行声明?

num = 100
nums = [11, 22]
def test():
    global num
    num += 100
def test2():
    nums.append(33)
print(num)        # 100
print(nums)       # [11,22]
test()
test2()
print(num)       # 200
print(nums)      # [11,22,33]

结论:如果函数中会修改变量的引用,那么就需要使用global进行声明;如果没有修改变量的引用,就不需要使用global进行声明

g_num = 100
def test1():
    global g_num
    g_num += 1
    print("-------- in test1 g_num = %s " % g_num)
def test2():
    print("-------- in test2 g_num = %d " % g_num)
def main():
    t1 = threading.Thread(target = test1)
    t2 = threading.Thread(target = test2)
    t1.start()
    time.sleep(1)    # 让test1子线程先执行
    t2.start()
    time.sleep(1)
    print('------- in main thread g_num = %d ' % g_num)
if __name__ == '__main__':
    main()

执行结果:

-------- in test1 g_num = 101 
-------- in test2 g_num = 101 
------- in main thread g_num = 101

结论:从以上代码可以看出,两个子线程间共享全局变量

2.5 多线程的参数传递

import threading
import time
g_num = 100
g_nums = [11, 22]
def test1(tmp):
    tmp.append(33)
    print("-------- in test1 tmp = %s " % str(tmp))
def test2(tmp):
    print("-------- in test2 tmp = %s " % str(tmp))
def main():
    t1 = threading.Thread(target = test1, args = (g_nums,))  # 必须以元祖的方式传递
    t2 = threading.Thread(target = test2, args = (g_nums,))
    t1.start()
    time.sleep(1)
    t2.start()
    time.sleep(1)
    print('------- in main thread g_nums = %s ' % g_nums)
if __name__ == '__main__':
    main()

执行结果

-------- in test1 tmp = [11, 22, 33] 
-------- in test2 tmp = [11, 22, 33] 
------- in main thread g_nums = [11, 22, 33]

总结:
在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)

共享全局变量带来的问题就是资源的竞争,如:

import threading
import time
g_num = 0
def test1(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("-------- in test1 g_num = %d " % g_num)
def test2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("-------- in test2 g_num = %d " % g_num)
def main():
    t1 = threading.Thread(target = test1, args=(1000000,))
    t2 = threading.Thread(target = test2, args=(1000000,))
    t1.start()
    t2.start()
    time.sleep(5)   # 等待上面的两个线程执行完毕
    print('------- in main thread g_num = %d ' % g_num)
if __name__ == '__main__':
    main()

执行结果

-------- in test2 g_num = 979681 
-------- in test1 g_num = 1059525 
------- in main thread g_num = 1059525

结论:如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确

2.6 同步

同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
"同"字从字面上容易理解为一起动作,其实不是,"同"字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B执行,再将结果给A;A再继续操作。

解决线程同时修改全局变量的方式:对于上一小节提出的那个计算错误的问题,可以通过线程同步来进行解决

思路,如下:

  1. 系统调用t1,然后获取到g_num的值为0,此时上一把锁,即不允许其他线程操作g_num
  2. t1对g_num的值进行+1
  3. t1解锁,此时g_num的值为1,其他的线程就可以使用g_num了而且是g_num的值不是0而是1
  4. 同理其他线程在对g_num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性

2.7 互斥锁(解决资源竞争)

当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性

import threading
import time
g_num = 0
mutex = threading.Lock()    # 创建一个互斥锁
def test1(num):
    global g_num
    mutex.acquire()
    # 上锁,如果之前没有被上锁,此时上锁成功
    # 如果上锁之前已经被上锁,那么此时会堵塞在这里,直到这个锁被解开为止
    for i in range(num):
        g_num += 1
    mutex.release()     # 解锁
    print("-------- in test1 g_num = %d " % g_num)
def test2(num):
    global g_num
    mutex.acquire()
    for i in range(num):
        g_num += 1
    mutex.release()
    print("-------- in test2 g_num = %d " % g_num)
def main():
    t1 = threading.Thread(target = test1, args=(1000000,))
    t2 = threading.Thread(target = test2, args=(1000000,))
    t1.start()
    t2.start()
    time.sleep(5)   # 等待上面的两个线程执行完毕
    print('------- in main thread g_num = %d ' % g_num)
if __name__ == '__main__':
    main()

执行结果

-------- in test1 g_num = 1000000 
-------- in test2 g_num = 2000000 
------- in main thread g_num = 2000000

上锁的原则:上锁的代码越少越好
上面的代码上锁的代码是锁定了整个for循环语句,也就是说当一个for循环语句要执行100秒,那么下一个for循环就要等待100秒

import threading
import time
g_num = 0
mutex = threading.Lock()    # 创建一个互斥锁
def test1(num):
    global g_num
    for i in range(num):
        mutex.acquire()
        g_num += 1
        mutex.release()     # 解锁
    print("-------- in test1 g_num = %d " % g_num)
def test2(num):
    global g_num
    for i in range(num):
        mutex.acquire()
        g_num += 1
        mutex.release()
    print("-------- in test2 g_num = %d " % g_num)
def main():
    t1 = threading.Thread(target = test1, args=(1000000,))
    t2 = threading.Thread(target = test2, args=(1000000,))
    t1.start()
    t2.start()
    time.sleep(5)   # 等待上面的两个线程执行完毕
    print('------- in main thread g_num = %d ' % g_num)
if __name__ == '__main__':
    main()

执行结果(注意上面代码中上锁的位置,锁放到了for循环的里面)

-------- in test1 g_num = 1955418 
-------- in test2 g_num = 2000000 
------- in main thread g_num = 2000000

上锁解锁过程
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。

GIL全局解释器锁
python自带的解释器是CPython。cpython解释器的多线程实际上是一个假的多线程(在多核CPU中,只能利用一核,不能利用多核)。同一时刻只有一个线程在执行,为了保证同一时刻只有一个线程在执行,在CPYTHON解释器中有一个东西叫做 GIL(global intepreter lock),叫做全局解释器锁。这个解释器锁是有必要的。因为cpython解释的内存管理不是线程安全的。当然除了cpython解释器,还有其他的解释器,有些解释器是没有GIL锁的。
1:jypython 用JAVA实现的python解释器,不存在GIL锁
2:ironpython 用.net实现的python解释器,不存在GIL锁
3:pypy 用python实现的python解释器,存在GIL锁
GIL虽然是一个假的多线程,但在处理一些IO操作(比如文件读写和网络请求)还是可以在很大程度上提高效率的。在IO操作上建议使用多线程提高效率。在一些CPU计算操作上不建议使用多线程,而建议使用多进程。

2.8 死锁

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子

import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        # 对mutexA上锁
        mutexA.acquire()
        # mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
        print(self.name+'----do1---up----')
        time.sleep(1)
        # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
        mutexB.acquire()
        print(self.name+'----do1---down----')
        mutexB.release()
        # 对mutexA解锁
        mutexA.release()
class MyThread2(threading.Thread):
    def run(self):
        # 对mutexB上锁
        mutexB.acquire()
        # mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
        print(self.name+'----do2---up----')
        time.sleep(1)
        # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
        mutexA.acquire()
        print(self.name+'----do2---down----')
        mutexA.release()
        # 对mutexB解锁
        mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

Python基础之多任务
此时已经进入到了死锁状态,可以使用ctrl-c退出
如何避免死锁:

  1. 程序设计时要尽量避免(银行家算法)
  2. 添加超时时间等

2.9 semaphore(信号量)

互斥锁同时允许一个线程更改数据,而semaphore是同时允许一定数量的线程更改数据

def run(n):
    semaphore.acquire()
    time.sleep(1)
    print('run the thread: %s\n' % n)
    semaphore.release()

if __name__ == '__main__':
    num = 0
    semaphore = threading.BoundedSemaphore(5)   # 最多允许5个线程同时运行
    for i in range(20):
        t = threading.Thread(target = run, args = (i,))
        t.start()

while threading.active_count() != 1:
    # print(threading.active_count())  # 打印活跃的线程数
    pass
else:
    print('---- all threads done ----')

2.10 案例-多任务版udp聊天器

import socket
import threading
def recv_msg(udp_socket):
    while True:
        recv_data, sender_addr = udp_socket.recvfrom(1024)
        print(recv_data.decode('gbk'))
def send_msg(udp_socket):
    while True:
        send_data = input('Please input send data: ')
        udp_socket.sendto(send_data.encode('utf-8'), ('172.17.2.82', 9090))
def main():
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    local_addr = ('', 8080)
    udp_socket.bind(local_addr)
    t_recv = threading.Thread(target=recv_msg, args=(udp_socket,))
    t_send = threading.Thread(target=send_msg, args=(udp_socket,))
    t_recv.start()
    t_send.start()
if __name__ == '__main__':
    main()

也就是既能发信息也能收信息

2.11 join()方法

假如现在有个程序中开启了10个线程,如何计算这个程序运行所运行的时间?

def run(n):
    print('task', n)
    print('当前线程为:',threading.current_thread())
    time.sleep(2)
    print('task done', n)
start_time = time.time()
t_objs = []
for i in range(10):
    t = threading.Thread(target = run, args = (i,))
    t.start()
    t_objs.append(t)  # 为了不阻塞后面线程的启动,不在这里join,而是先放到一个列表中
for t in t_objs:
    t.join()  # 等待10个子线程全部结束,再运行下面的主线程代码
# 下面的代码由主线程执行,该脚本就是主线程    
print('------- all threads has finished')
print(threading.current_thread())  # 打印当前线程
print('cost : ', time.time() - start_time)

2.12 守护线程

def run(n):
    print('task', n)
    time.sleep(2)
    print('task done', n)
start_time = time.time()
for i in range(10):
    t = threading.Thread(target = run, args = (i,))
    t.setDaemon(True)  # 把当前线程设置为守护线程,必须定义在 t.start() 之前
    t.start()
print('------- all threads has finished')
print('当前线程为:',threading.current_thread())
print('cost : ', time.time() - start_time)

如果你设置一个线程为守护线程,就表示你在说这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。
如果你的主线程在退出的时候,不用等待那些子线程完成,那就设置这些线程的daemon属性。即在线程开始(thread.start())之前,调用setDeamon()函数,设定线程的daemon标志。(thread.setDaemon(True))就表示这个线程“不重要”。

2.13 Events

Python提供了Event对象用于线程间通信,它是由线程设置的信号标志,如果信号标志位真,则其他线程等待直到信号接触。
Event对象实现了简单的线程通信机制,它提供了设置信号,清除信号,等待等用于实现线程间的通信。

event = threading.Event()
def lighter():
    count = 0
    event.set()
    while True:
        if count > 5 and count < 10:
            event.clear()   # 清空标志位
            print("\033[41;1m red light is on ... \033[0m")
        elif count > 10:
            event.set()     # 设置标志位
            count = 0
        else:
            print("\033[42;1m green light is on ... \033[0m")
        time.sleep(1)
        count += 1
def car(name):
    while True:
        if event.is_set():
            print('[%s] running ...' % name)
            time.sleep(1)
        else:
            print('[%s] sees red light, waitting ...' % name)
            event.wait()
            print('[%s] green light is on, start going ...' % name)
t = threading.Thread(target = lighter,)
t.start()
car1 = threading.Thread(target = car, args = ('BMW',))
car1.start()

2.13 queue队列

作用:
   解耦:使程序直接实现松耦合,修改一个函数,不会有串联关系。
   提高处理效率:FIFO = 现进先出,LIFO = 后入先出。
队列:
  队列可以并发的派多个线程,对排列的线程处理,并切每个需要处理线程只需要将请求的数据放入队列容器的内存中,线程不需要等待,当排列完毕处理完数据后,线程在准时来取数据即可。请求数据的线程只与这个队列容器存在关系,处理数据的线程down掉不会影响到请求数据的线程,队列会派给其他线程处理这分数据,它实现了解耦,提高效率。队列内会有一个有顺序的容器,列表与这个容器是有区别的,列表中数据虽然是排列的,但数据被取走后还会保留,而队列中这个容器的数据被取后将不会保留。当必须在多个线程之间安全地交换信息时,队列在线程编程中特别有用。

相关参数:

# 先入先出 maxsize 可设置大小,设置block=False抛异常
class queue.Queue(maxsize=0)  
# 后进先出 
class queue.LifoQueue(maxsize=0)
# 存储数据时可设置优先级的队列
# 优先级设置数越小等级越高
class queue.PriorityQueue(maxsize=0) 
# 放入数据
Queue.put(item, block=True, timeout=None)
# 取出数据 #没有数据将会等待
Queue.get(block=True, timeout=None)
# 如果1秒后没取到数据就退出
Queue.get(timeout = 1)
# 取数据,如果没数据抛queue.Empty异常
Queue.get_nowait()
# 查看队列大小
Queue.qsize()
# 返回True,如果空
Queue.empty() #return True if empty  
# 设置队列大小
Queue.full() 
# 后续调用告诉队列,任务的处理是完整的。
Queue.task_done()

生产者消费者模型:

q = queue.Queue(10)
def producer(name):
    count = 1
    while True:
        q.put('Bone-%s' % str(count))
        print('[%s] make bone-%s' % (name, str(count)))
        count += 1
        time.sleep(1)
def consumer(name):
    while True:
        print('[%s] get [%s] and eated ...' %(name, q.get()))
p = threading.Thread(target = producer, args = ('Andy',))
c = threading.Thread(target = consumer, args = ('Bruce',))
w = threading.Thread(target = consumer, args = ('water',))
p.start()
c.start()
w.start()

三:进程

3.1 进程以及状态

程序:例如xxx.py这是程序,是一个静态的
进程:一个程序运行起来后,代码+用到的资源 称之为进程,它是操作系统分配资源的基本单元。
不仅可以通过线程完成多任务,进程也是可以的

进程的状态
工作中,任务数往往大于cpu的核数,即一定有一些任务正在执行,而另外一些任务在等待cpu进行执行,因此导致了有了不同的状态
Python基础之多任务
就绪态:运行的条件都已经就绪,正在等待cpu执行
执行态:cpu正在执行其功能
等待态:等待某些条件满足,例如一个程序sleep了,此时就处于等待态

Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。

3.2 进程的创建

multiprocessing模块就是跨平台版本的多进程模块,提供了一个Process类来代表一个进程对象,这个对象可以理解为是一个独立的进程,可以执行另外的事情

import time
import multiprocessing
def test1():
    while True:
        print('1-------------')
        time.sleep(1)
def test2():
    while True:
        print('2-------------')
        time.sleep(1)
def main():
    t1 = multiprocessing.Process(target=test1)
    t2 = multiprocessing.Process(target=test2)
    t1.start()
    t2.start()
if __name__ == '__main__':
    main()

可以看到,多进程的创建与多线程的创建形式基本一致

3.3 进程线程的对比

进程,能够完成多任务,比如 在一台电脑上能够同时运行多个QQ
线程,能够完成多任务,比如 一个QQ中的多个聊天窗口

进程与线程定义的不同:
进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

进程与线程的区别:
一个程序至少有一个进程,一个进程至少有一个线程.
线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
Python基础之多任务
线线程不能够独立执行,必须依存在进程中
可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人

优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。

3.4 进程间的通信-Queue

Process之间有时需要通信,操作系统提供了很多机制来实现进程间的通信。
可以使用multiprocessing模块的Queue实现多进程之间的数据传递,Queue本身是一个消息列队程序,首先用一个小实例来演示一下Queue的工作原理:

import multiprocessing
def download(q):    # 模拟写入数据到Queue
    data = [11, 22, 33, 44]
    for tmp in data:
        q.put(tmp)
    print('---下载器已经下载完了所有数据并存入到队列中')
def analysis_data(q):    # 模拟从Queue读数据
    w_data = []
    while True:
        data = q.get()
        w_data.append(data)
        if q.empty():
            break
    print(w_data)
def main():
    q = multiprocessing.Queue()
    p1 = multiprocessing.Process(target=download, args=(q,))
    p2 = multiprocessing.Process(target=analysis_data, args=(q,))
    p1.start()
    p2.start()
if __name__ == '__main__':
    main()

初始化Queue()对象时(例如:q=Queue()),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限(直到内存的尽头);
Queue.qsize():返回当前队列包含的消息数量;
Queue.empty():如果队列为空,返回True,反之False ;
Queue.full():如果队列满了,返回True,反之False;
Queue.get([block[, timeout]]):获取队列中的一条消息,然后将其从列队中移除,block默认值为True;

  1. 如果block使用默认值,且没有设置timeout(单位秒),消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出"Queue.Empty"异常;
  2. 如果block值为False,消息列队如果为空,则会立刻抛出"Queue.Empty"异常;

Queue.get_nowait():相当Queue.get(False);
Queue.put(item,[block[, timeout]]):将item消息写入队列,block默认值为True;

  1. 如果block使用默认值,且没有设置timeout(单位秒),消息列队如果已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,如果设置了timeout,则会等待timeout秒,若还没空间,则抛出"Queue.Full"异常;
  2. 如果block值为False,消息列队如果没有空间可写入,则会立刻抛出"Queue.Full"异常;
    Queue.put_nowait(item):相当Queue.put(item, False);

3.5 进程池

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

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

import os, time, random
from multiprocessing.dummy import Pool
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-----")

multiprocessing.Pool常用函数解析:
apply_async(func[, args[, kwds]]) :使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
close():关闭Pool,使其不再接受新的任务;
terminate():不管任务是否完成,立即终止;
join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;

执行结果

----start----
0开始执行,进程号为22512
1开始执行,进程号为22512
2开始执行,进程号为22512
2 执行完毕,耗时0.25
3开始执行,进程号为22512
1 执行完毕,耗时0.39
4开始执行,进程号为22512
3 执行完毕,耗时0.59
5开始执行,进程号为22512
5 执行完毕,耗时0.16
6开始执行,进程号为22512
0 执行完毕,耗时1.54
7开始执行,进程号为22512
7 执行完毕,耗时0.27
8开始执行,进程号为22512
4 执行完毕,耗时1.48
9开始执行,进程号为22512
8 执行完毕,耗时0.79
6 执行完毕,耗时1.70
9 执行完毕,耗时0.95
-----end-----

3.6 多进程案例(文件夹copy器)

进程池中的Queue
如果要使用Pool创建进程,就需要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否则会得到一条如下的错误信息:

RuntimeError: Queue objects should only be shared between processes through inheritance.
import os
import multiprocessing
def copy_file(q, file_name, old_folder_name, new_folder_name):
    old_f = open(old_folder_name + '/' + file_name, 'rb')
    content = old_f.read()
    old_f.close()
    new_f = open(new_folder_name + '/' + file_name, 'wb')
    new_f.write(content)
    new_f.close()
    # 复制完成一个文件后向队列写入一个消息,表示已经完成
    q.put(file_name)
def main():
    old_folder_name = input('Please input want to copy directory name: ')
    new_folder_name = old_folder_name + '[copy]'
    try:
        os.mkdir(new_folder_name)
    except Exception as e:
        pass
    file_names = os.listdir(old_folder_name)
    # print(file_names)
    # 创建进程池
    po = multiprocessing.Pool(5)
    # 创建一个队列
    q = multiprocessing.Manager().Queue()
    for file_name in file_names:
        po.apply_async(copy_file, args=(q, file_name, old_folder_name, new_folder_name))
    po.close()
    # po.join()
    all_file_num = len(file_names)
    count = 0
    while True:
        file_name = q.get()
        # print('已经完成拷贝:%s' % file_name)
        count += 1
        print('\r拷贝的进度为:%.2f%%' % (count / all_file_num * 100), end = '')
        # end=''表示打印完后不换行;%%两个%号为输出一个%;\r表示本次打印完后回到行首继续打印
        if count >= all_file_num:
            break
    print()
if __name__ == '__main__':
    main()

四:协程

4.1 迭代器

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

可迭代对象:
我们已经知道可以对list、tuple、str等类型的数据使用for…in…的循环语法从其中依次拿到数据进行使用,我们把这样的过程称为遍历,也叫迭代。
但是,是否所有的数据类型都可以放到for…in…的语句中,然后让for…in…每次从中取出一条数据供我们使用,即供我们迭代吗?

>>> for i in 100:
...     print(i)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>>
# int整型不是iterable,即int整型不是可以迭代的

# 我们自定义一个容器MyList用来存放数据,可以通过add方法向其中添加数据
>>> class MyList(object):
...     def __init__(self):
...             self.container = []
...     def add(self, item):
...             self.container.append(item)
...
>>> mylist = MyList()
>>> mylist.add(1)
>>> mylist.add(2)
>>> mylist.add(3)
>>> for num in mylist:
...     print(num)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'MyList' object is not iterable
>>>
# MyList容器的对象也是不能迭代的

我们自定义了一个容器类型MyList,在将一个存放了多个数据的MyList对象放到for…in…的语句中,发现for…in…并不能从中依次取出一条数据返回给我们,也就说我们随便封装了一个可以存放多条数据的类型却并不能被迭代使用。
我们把可以通过for…in…这类语句迭代读取一条数据供我们使用的对象称之为可迭代对象(Iterable)。

如何判断一个对象是否可以迭代
可以使用 isinstance() 判断一个对象是否是 Iterable 对象

from collections import Iterable
print(isinstance([11,22], Iterable))    # True

可迭代对象的本质
我们分析对可迭代对象进行迭代使用的过程,发现每迭代一次(即在for…in…中每循环一次)都会返回对象中的下一条数据,一直向后读取数据直到迭代了所有数据后结束。那么,在这个过程中就应该有一个“人”去记录每次访问到了第几条数据,以便每次迭代都可以返回下一条数据。我们把这个能帮助我们进行数据迭代的“人”称为迭代器(Iterator)。
可迭代对象的本质就是可以向我们提供一个这样的中间“人”即迭代器帮助我们对其进行迭代遍历使用。
可迭代对象通过__iter__方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据.
那么也就是说,一个具备了__iter__方法的对象,就是一个可迭代对象。

import time
from collections import Iterable, Iterator
class Classmate(object):
    def __init__(self):
        self.names = list()
        self.current_num = 0
    def add(self, name):
        self.names.append(name)
    def __iter__(self):
        '''如果想要一个对象成为一个可以迭代的对象,即可以使用for,那么必须实现__iter__方法'''
        return self
    def __next__(self):
        if self.current_num < len(self.names):
            ret = self.names[self.current_num]
            self.current_num += 1
            return ret
        else:
            raise StopIteration
            # 抛异常告诉外面的for循环没有数据可取了,可结束for循环
classmate = Classmate()
classmate.add('Bruce Lee')
classmate.add('Jet Lee')
classmate.add('Jackie Chan')
print("判断classmate是否可以迭代的对象:", isinstance(classmate, Iterable))
classmate_iterator = iter(classmate)
print("判断classmate_iterator是否是迭代器:", isinstance(classmate_iterator, Iterator))
print(next(classmate_iterator))
for i in classmate:
    print(i)
    time.sleep(1)

执行结果:

判断classmate是否可以迭代的对象: True
判断classmate_iterator是否是迭代器: True
Bruce Lee
Jet Lee
Jackie Chan

for 循环的本质

for i in classmate:
    print(i)
# 1:判断classmate是否是可迭代对象
# 2:在第一步成立的前提下,调用iter函数,得到classmate对象的__next__方法的返回值
# 3:__iter__方法的返回值是一个迭代器

迭代器的应用场景
我们发现迭代器最核心的功能就是可以通过next()函数的调用来返回下一个数据值。如果每次返回的数据值不是在一个已有的数据集合中读取的,而是通过程序按照一定的规律计算生成的,那么也就意味着可以不用再依赖一个已有的数据集合,也就是说不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,这样可以节省大量的存储(内存)空间。
举个例子,比如,数学中有个著名的斐波拉契数列(Fibonacci),数列中第一个数为0,第二个数为1,其后的每一个数都可由前两个数相加得到:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

现在我们想要通过for…in…循环来遍历迭代斐波那契数列中的前n个数。那么这个斐波那契数列我们就可以用迭代器来实现,每次迭代都通过数学计算来生成下一个数。

class FibIterator():
    def __init__(self, num):
        self.num = num
        self.current_num = 0
        self.a = 0
        self.b = 1
    def __iter__(self):
        return self
    def __next__(self):
        if self.current_num < self.num:
            ret = self.a
            self.a, self.b = self.b, self.a + self.b
            self.current_num += 1
            return ret
        else:
            raise StopIteration
fibo = FibIterator(10)
for i in fibo:
    print(i)

除了for循环能接收可迭代对象,list、tuple等也能接收。

li = list(FibIterator(15))
print(li)
tp = tuple(FibIterator(6))
print(tp)

4.2 生成器

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

创建生成器方法1
要创建一个生成器,有很多种方法。第一种方法很简单,只要把一个列表生成式的 [ ] 改成 ( )

In [15]: L = [ x*2 for x in range(5)]

In [16]: L
Out[16]: [0, 2, 4, 6, 8]

In [17]: G = ( x*2 for x in range(5))

In [18]: G
Out[18]: <generator object <genexpr> at 0x7f626c132db0>

创建 L 和 G 的区别仅在于最外层的 [ ] 和 ( ) , L 是一个列表,而 G 是一个生成器。我们可以直接打印出列表L的每一个元素,而对于生成器G,我们可以按照迭代器的使用方法来使用,即可以通过next()函数、for循环、list()等方法使用。

In [19]: next(G)
Out[19]: 0

In [20]: next(G)
Out[20]: 2

In [21]: next(G)
Out[21]: 4

In [22]: next(G)
Out[22]: 6

In [23]: next(G)
Out[23]: 8

In [24]: next(G)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-24-380e167d6934> in <module>()
----> 1 next(G)

StopIteration:

In [25]:
In [26]: G = ( x*2 for x in range(5))

In [27]: for x in G:
   ....:     print(x)
   ....:     
0
2
4
6
8

创建生成器方法2
generator非常强大。如果推算的算法比较复杂,用类似列表生成式的 for 循环无法实现的时候,还可以用函数来实现。
我们仍然用上一节提到的斐波那契数列来举例,回想我们在上一节用迭代器的实现方式:

class FibIterator(object):
    """斐波那契数列迭代器"""
    def __init__(self, n):
        """
        :param n: int, 指明生成数列的前n个数
        """
        self.n = n
        # current用来保存当前生成到数列中的第几个数了
        self.current = 0
        # num1用来保存前前一个数,初始值为数列中的第一个数0
        self.num1 = 0
        # num2用来保存前一个数,初始值为数列中的第二个数1
        self.num2 = 1

    def __next__(self):
        """被next()函数调用来获取下一个数"""
        if self.current < self.n:
            num = self.num1
            self.num1, self.num2 = self.num2, self.num1+self.num2
            self.current += 1
            return num
        else:
            raise StopIteration

    def __iter__(self):
        """迭代器的__iter__返回自身即可"""
        return self

注意,在用迭代器实现的方式中,我们要借助几个变量(n、current、num1、num2)来保存迭代的状态。现在我们用生成器来实现一下。

In [30]: def fib(n):
   ....:     current = 0
   ....:     num1, num2 = 0, 1
   ....:     while current < n:
   ....:         num = num1
   ....:         num1, num2 = num2, num1+num2
   ....:         current += 1
   ....:         yield num
   ....:     return 'done'
   ....:

In [31]: F = fib(5)

In [32]: next(F)
Out[32]: 1

In [33]: next(F)
Out[33]: 1

In [34]: next(F)
Out[34]: 2

In [35]: next(F)
Out[35]: 3

In [36]: next(F)
Out[36]: 5

In [37]: next(F)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-37-8c2b02b4361a> in <module>()
----> 1 next(F)

StopIteration: done

在使用生成器实现的方式中,我们将原本在迭代器__next__方法中实现的基本逻辑放到一个函数中来实现,但是将每次迭代返回数值的return换成了yield,此时新定义的函数便不再是函数,而是一个生成器了。简单来说:只要在def中有yield关键字的 就称为 生成器
此时按照调用函数的方式( 案例中为F = fib(5) )使用生成器就不再是执行函数体了,而是会返回一个生成器对象( 案例中为F ),然后就可以按照使用迭代器的方式来使用生成器了。

In [38]: for n in fib(5):
   ....:     print(n)
   ....:     
1
1
2
3
5

但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中:

In [39]: g = fib(5)

In [40]: while True:
   ....:     try:
   ....:         x = next(g)
   ....:         print("value:%d"%x)      
   ....:     except StopIteration as e:
   ....:         print("生成器返回值:%s"%e.value)
   ....:         break
   ....:     
value:1
value:1
value:2
value:3
value:5
生成器返回值:done

In [41]:

总结
1:使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)
2:yield关键字有两点作用:
保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用
3:可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)
4:Python3中的生成器可以使用return返回最终运行的返回值,而Python2中的生成器不允许使用return返回一个返回值(即可以使用return从生成器中退出,但return后不能有任何表达式)。

使用send唤醒
我们除了可以使用next()函数来唤醒生成器继续执行外,还可以使用send()函数来唤醒执行。使用send()函数的一个好处是可以在唤醒的同时向断点处传入一个附加数据。

def create_num(all_num):
    a, b = 0, 1
    current_num = 0
    while current_num < all_num:
        ret = yield a
        print('>>ret:', ret)
        a, b = b, a+b
        current_num += 1
obj = create_num(10)
ret = next(obj)
print(ret)
# 注意:生成器将(yield a)的值返回过来,而ret=的赋值操作此时还没执行
ret = obj.send('hello')  # 注意:如果第一次就使用send,那么可以传None值,否则会报错
# 注意:此时我们send('hello')时,才会将hello赋值给ret,然后代码继续向下走,走到yield a将1返回出来,代码再次暂停执行
print(ret)

执行结果:

0
>>ret: hello
1

使用next函数:

In [11]: f = gen()

In [12]: next(f)
Out[12]: 0

In [13]: next(f)
None
Out[13]: 1

In [14]: next(f)
None
Out[14]: 2

In [15]: next(f)
None
Out[15]: 3

In [16]: next(f)
None
Out[16]: 4

In [17]: next(f)
None
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-17-468f0afdf1b9> in <module>()
----> 1 next(f)

StopIteration:

使用__next__()方法(不常使用)

In [18]: f = gen()

In [19]: f.__next__()
Out[19]: 0

In [20]: f.__next__()
None
Out[20]: 1

In [21]: f.__next__()
None
Out[21]: 2

In [22]: f.__next__()
None
Out[22]: 3

In [23]: f.__next__()
None
Out[23]: 4

In [24]: f.__next__()
None
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-24-39ec527346a9> in <module>()
----> 1 f.__next__()

StopIteration:

4.3 协程-yield

协程,又称微线程,纤程。英文名Coroutine。

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

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

协程和线程差异
在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。

简单实现协程:

import time
def task_1():
    while True:
        print('---1---')
        time.sleep(0.1)
        yield
def task_2():
    while True:
        print('---2---')
        time.sleep(0.1)
        yield
def main():
    t1 = task_1()
    t2 = task_2()
    while True:
        next(t1)
        next(t2)
if __name__ == '__main__':
    main()

注意:上面的代码是并发而不是并行,两个函数交替执行,也就task_1执行到yield暂停,然后task_2执行到yield暂停,再执行task_1,如此反复

4.4 协程-greenlet

为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单
greenlet需要单独安装:

pipenv install greenlet
from greenlet import greenlet
import time
def test1():
    while True:
        print('---test1---')
        gr2.switch()  # 代码执行到这里,暂停,切到test2中执行
        time.sleep(0.5)
def test2():
    while True:
        print('---test1---')
        gr1.switch()  # 代码执行到这里,暂停,切到test1中执行
        time.sleep(0.5)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()  # 切换到gr1中运行

其运行效果与上面的yield相同

4.5 协程-gevent

greenlet已经实现了协程,但是这个还得人工切换,是不是觉得太麻烦了,不要着急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent
其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO

pipenv install gevent

gevent简单使用:

import gevent
def f1(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        #time.sleep(0.5)
        gevent.sleep(0.5)   # 遇到耗时操作自动切换到下一个任务
def f2(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        gevent.sleep(0.5)
def f3(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        gevent.sleep(0.5)
print('--1--')
g1 = gevent.spawn(f1, 5)
print('--2--')
g2 = gevent.spawn(f2, 5)
print('--3--')
g3 = gevent.spawn(f3, 5)
print('--4--')
g1.join()
g2.join()
g3.join()

执行结果

--1--
--2--
--3--
--4--
<Greenlet at 0x1cdf38f4648: f1(5)> 0
<Greenlet at 0x1cdf38f4a48: f2(5)> 0
<Greenlet at 0x1cdf38f4b48: f3(5)> 0
<Greenlet at 0x1cdf38f4648: f1(5)> 1
<Greenlet at 0x1cdf38f4a48: f2(5)> 1
<Greenlet at 0x1cdf38f4b48: f3(5)> 1
<Greenlet at 0x1cdf38f4648: f1(5)> 2
<Greenlet at 0x1cdf38f4a48: f2(5)> 2
<Greenlet at 0x1cdf38f4b48: f3(5)> 2
<Greenlet at 0x1cdf38f4648: f1(5)> 3
<Greenlet at 0x1cdf38f4a48: f2(5)> 3
<Greenlet at 0x1cdf38f4b48: f3(5)> 3
<Greenlet at 0x1cdf38f4648: f1(5)> 4
<Greenlet at 0x1cdf38f4a48: f2(5)> 4
<Greenlet at 0x1cdf38f4b48: f3(5)> 4

给程序打补丁

import gevent
from gevent import monkey
import time
monkey.patch_all()
# gevent不认time.sleep(),只认gevent.sleep(),只有遇到gevent.sleep()才会自动切换任务
# monkey.patch_all()的作用就是只要遇到代码中的time.sleep()全部替换为gevent.sleep()
def f1(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        time.sleep(0.5)
def f2(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        time.sleep(0.5)
def f3(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        time.sleep(0.5)
gevent.joinall([
        gevent.spawn(f1, 5),
        gevent.spawn(f2, 5),
        gevent.spawn(f3, 5)
])

五:进程,线程,协程对比

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

有一个老板想要开个工厂进行生产某件商品(例如剪子)
他需要花一些财力物力制作一条生产线,这个生产线上有很多的器件以及材料这些所有的 为了能够生产剪子而准备的资源称之为:进程
只有生产线是不能够进行生产的,所以老板的找个工人来进行生产,这个工人能够利用这些材料最终一步步的将剪子做出来,这个来做事情的工人称之为:线程

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

简单总结:

  1. 进程是资源分配的单位
  2. 线程是操作系统调度的单位
  3. 进程切换需要的资源很最大,效率很低
  4. 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
  5. 协程切换任务资源很小,效率高
  6. 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值