Python 多进/线程 协程 的实现:一

1.多任务:就是操作系统可以同时运行多个任务。

并发/并行

  • 并行:多个CPU核心,不同的程序就分配给不同的CPU来运行。可以让多个程序同时执行。
  • 并发:单个CPU核心,在一个时间切片里一次只能运行一个程序,如果需要运行多个程序,则串行执行。

同步/异步

  • 同步:当进程执行IO(等待外部数据)的时候,-----等。同步(例如打电话的时候必须等)
  • 异步:当进程执行IO(等待外部数据)的时候,-----不等,去执行其他任务,一直等到数据接收成功,再回来处理。异步(例如发短信)

2.进程,线程,协程

1.概念

  • 进程:对于操作系统来说,一个任务就是一个进程
  • 线程:在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程
  • 协程:微线程--在单线程上执行多个任务

2.应用场景

  • 多进程:密集CPU任务,需要充分使用多核CPU资源(服务器,大量的并行计算)的时候,用多进程。multiprocessing
    缺陷:多个进程之间通信成本高,切换开销大。
  • 多线程:密集I/O任务(网络I/O,磁盘I/O,数据库I/O)使用多线程合适。threading
    缺陷:同一个时间切片只能运行一个线程,不能做到高并行,但是可以做到高并发。
  • 协程:又称微线程,在单线程上执行多个任务,用函数切换,开销极小。不通过操作系统调度,没有进程、线程的切换开销。 多线程请求返回是无序的,哪个线程有数据返回就处理那个线程,而协程返回的数据是有序的。
    缺陷:单线程执行,处理密集CPU和本地磁盘IO的时候,性能较低。处理网络I/O性能还是比较高.

3.进程与线程的比较

关系

(1) 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)处理机分给线程,即真正在处理机上运行的是线程
(4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
	线程是指进程内的一个执行单元,也是进程内的可调度实体.

进程与线程的区别:

(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
(4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

不同点
(1)定义:

进程是操作系统分配资源的最小单元, 线程是操作系统调度的最小单元。
一个应用程序至少包括1个进程,而1个进程包括1个或多个线程,线程的尺度更小。
每个进程在执行过程中拥有独立的内存单元,而一个进程的多个线程在执行过程中共享内存。

(2)实现进程与线程的模块:

multiprocessing/threading*

(3)CPU利用率(url):

进程:调用多个CPU
线程:一个CPU调用多个线程,多线程之间切换

(4)功能:

进程:调用多个CPU,能够完成多任务,比如 在一台电脑上能够同时运行多个QQ
线程:一个CPU调用多个线程,多线程之间切换,能够完成多任务,比如 一个QQ中的多个聊天窗口

(5)I/O密集型/计算密集型(url)

在IO密集型任务中使用多线程,
	进行耗时的IO操作的时候,能释放GIL
在计算密集型任务中使用多进程。
	调度多个CPU。

(6)优缺点:

线程执行开销小,但不利于资源的管理和保护;而进程正相反。
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。
多进程模式的缺点是创建进程的代价大,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的。
多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。

进程间通信的方式

(1)管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道
	所具有的功能外,它还允许无亲缘关系进程间的通信。
(2)信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,
	一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
(3)消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限的
	进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
(4)共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,
	不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
(5)信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
(6)套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。

锁(线程锁)

锁(多线程)的由来:

多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。
 Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。
在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据缺点就是,线程是对
全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)
 如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确(线程同步(lock))

GIL相关问题

问题1: 什么时候会释放Gil锁,

1 遇到像 i/o操作这种 会有时间空闲情况 造成cpu闲置的情况会释放Gil
2 会有一个专门ticks进行计数 一旦ticks数值达到100 这个时候释放Gil锁 线程之间开始竞争Gil锁(说明:
	ticks这个数值可以进行设置来延长或者缩减获得Gil锁的线程使用cpu的时间)

问题2: 互斥锁和Gil锁的关系

Gil锁  : 保证同一时刻只有一个线程能使用到cpu
互斥锁 : 多线程时,保证修改共享数据时有序的修改,不会产生数据修改混乱

假设只有一个进程,这个进程中有两个线程 Thread1,Thread2, 要修改共享的数据date, 并且有互斥锁

执行以下步骤:

(1)多线程运行,假设Thread1获得GIL可以使用cpu,这时Thread1获得 互斥锁lock,Thread1可以改date
	数据(但并没有开始修改数据)
(2)Thread1线程在修改date数据前发生了 i/o操作 或者 ticks计数满100 (注意就是没有运行到修改data数据),
这个时候 Thread1 让出了Gil,Gil锁可以被竞争
(3) Thread1 和 Thread2 开始竞争 Gil (注意:如果Thread1是因为 i/o 阻塞 让出的Gil Thread2必定拿到Gil,如果
Thread1是因为ticks计数满100让出Gil 这个时候 Thread1 和 Thread2 公平竞争)
(4)假设 Thread2正好获得了GIL, 运行代码去修改共享数据date,由于Thread1有互斥锁lock,所以Thread2无法更改共享
数据date,这时Thread2让出Gil锁 , GIL锁再次发生竞争 
(5)假设Thread1又抢到GIL,由于其有互斥锁Lock所以其可以继续修改共享数据data,
	当Thread1修改完数据释放互斥锁lock,Thread2在获得GIL与lock后才可对data进行修改

锁的分类:

Lock 互斥锁: 同一时刻只有一个线程可以访问共享的数据。


#未加锁
# -*- coding: utf-8 -*
import threading

class Account:
    def __init__(self):
        self.balance = 0

    def add(self, lock):
        # 获得锁
        #lock.acquire()
        for i in range(0, 10000000):
            self.balance += 1
        # 释放锁
        #lock.release()

    def delete(self, lock):
        # 获得锁
        #lock.acquire()
        for i in range(0, 10000000):
            self.balance -= 1
            # 释放锁
        #lock.release()

if  __name__ == "__main__":
    account = Account()
    lock = threading.Lock()
    # 创建线程
    thread_add = threading.Thread(target=account.add, args=(lock,), name='Add')
    thread_delete = threading.Thread(target=account.delete, args=(lock,), name='Delete')

    # 启动线程
    thread_add.start()
    thread_delete.start()

    # 等待线程结束
    thread_add.join()
    thread_delete.join()

    print('The final balance is: {}'.format(account.balance))
    #不为0/随机(-1566075)
#加锁
# -*- coding: utf-8 -*
import threading

class Account:
    def __init__(self):
        self.balance = 0

    def add(self, lock):
        # 获得锁
        lock.acquire()
        for i in range(0, 10000000):
            self.balance += 1
        # 释放锁
        lock.release()

    def delete(self, lock):
        # 获得锁
        lock.acquire()
        for i in range(0, 10000000):
            self.balance -= 1
            # 释放锁
        lock.release()

if  __name__ == "__main__":
    account = Account()
    lock = threading.Lock()
    # 创建线程
    thread_add = threading.Thread(target=account.add, args=(lock,), name='Add')
    thread_delete = threading.Thread(target=account.delete, args=(lock,), name='Delete')

    # 启动线程
    thread_add.start()
    thread_delete.start()

    # 等待线程结束
    thread_add.join()
    thread_delete.join()

    print('The final balance is: {}'.format(account.balance)) 
    #为0

RLock 可重入锁: 在同一个线程中,RLock.acquire()可以被多次调用当所有RLock被release后,其他线程才能获取资源。
Semaphore 信号: 这种锁允许一定数量的线程同时更改数据,它不是互斥锁。

import time
import threading

s1=threading.Semaphore(5)   #添加一个计数器

def foo():
    s1.acquire()    #计数器获得锁
    time.sleep(2)   #程序休眠2秒
    print("ok",time.ctime())
    s1.release()    #计数器释放锁

for i in range(20):
    t1=threading.Thread(target=foo,args=()) #创建线程
    t1.start()  #启动线程   

Event 事件:

clear()方法会将事件的Flag设置为False。
set()方法会将Flag设置为True。
wait()方法将等待信号。
is_set():判断当前状态 

import time, threading

def lighter():  #
    t = 0
    while True:
        if t < 5:
            F1.clear()
            print("红灯亮")
        elif t >= 5 and t < 10:
            F1.set()
            print("绿灯亮")
        else:
            t = 0
            continue
        # print(t)
        t += 1
        time.sleep(1)

def car(*args):
    F1.clear()  # 最开始要清除标志位
    while True:
        if F1.is_set():  # 判断是否设置了标志位
            print("宝马走")
            time.sleep(1)

        else:
            print("等待")
            F1.wait()  # 会不断查找标志是否存在。如果没有标志位,就一直卡在这里,不会执行下面的
            print("准备")

F1 = threading.Event()  # 设置一个Event实例
L = threading.Thread(target=lighter, )
C = threading.Thread(target=car, args=("宝马",))

L.start()
C.start()

Condition 条件

wait([timeout])方法将使线程进入Condition的
等待池等待通知,并释放锁。
使用前线程必须已获得锁定,否则将抛出异常。
notify()方法将从等待池挑选一个线程并通知,收到通知的线程将自动
调用acquire()尝试获得锁定

运行原理:

1. 某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;
2. 直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。
3. 互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

注意:

如果这个锁之前是没有上锁的,那么acquire不会堵塞
如果在调用acquire对这个锁上锁之前,它已经被其他线程上了锁,
那么此时acquire会堵塞,直到这个锁被解锁为止

优缺点:

锁的好处:确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处: 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
死锁:在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

锁的应用

当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定

多线/进程间的数据共享与通信

进/线程池,队列,管道(pipe)
分布式消息队列(Queue):生产者和消费者彼此之间不直接通讯,而是通过阻塞队列来进行通讯, 所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列, 消费者不直接找生产者要数据,而是从阻塞队列里取, 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力, 解耦了生产者和消费者。
线程池:在切换线程的时候,需要切换上下文环境,线程很多的时候,依然会造成CPU的大量开销。


#使用进程池实现数据共享
from multiprocessing import Pool,Manager
import os
import time

def writeFs(q):
    print("当前子进程{}".format(os.getpid()))
    for i in ["A","B","C"]:
        print('Put %s to queue'%i)
        q.put(i)
        time.sleep(1)

def readFs(q):
    print("当前子进程{}".format(os.getpid()))
    for _ in range(4):
        value = q.get()
        print("get value:%s from queue "%value)
        time.sleep(2)

if  __name__== '__main__':
    print("当前父进程{}".format(os.getpid()))
    q = Manager().Queue() # Manger().Queue()  消息队列可用于进程池

    my_pool = Pool(2)
    my_pool.apply_async(writeFs,args=(q,))
    my_pool.apply_async(readFs, args=(q,))

    my_pool.close()
    my_pool.join()
    #.start()/.join()位置的不同,实现的结构也不同
    q = Queue() # Queue() 只能用于父子进程
    p1 = Process(target=writeFs,args=(q,))
    p2 = Process(target=readFs, args=(q,))
    p1.start()#按顺序执行进程
    p1.join()
    
    p2.start()
    p2.join()
    '''
    '''
    当前子进程4768
    Put A to queue
    Put B to queue
    Put C to queue
    当前子进程6648
    get value:A from queue 
    get value:B from queue 
    get value:C from queue
    '''
    '''
    p1.start()#同时开启进程
    p2.start()

    p1.join()#等待进程结束
    p2.join()
    '''
    '''
    当前子进程5916
    Put A to queue
    当前子进程4344
    get value:A from queue 
    Put B to queue
    Put C to queue
    get value:B from queue 
    get value:C from queue

#线程实现数据共享
from threading import Thread
from queue import  Queue
import threading
import os
import time


def writeFs(q):
    print("当前子线程{}".format(threading.current_thread()))
    for i in ["A", "B", "C"]:
        print('Put %s to queue' % i)
        q.put(i)
        time.sleep(1)

def readFs(q):
    print("当前子线程{}".format(threading.current_thread()))
    for _ in range(4):
        value = q.get()
        print("get value:%s from queue " % value)
        time.sleep(2)

if __name__ == '__main__':
    print("当前父线程{}".format(threading.current_thread()))

    q = Queue() # Queue() 只能用于父子线程
    p1 = Thread(target=writeFs,args=(q,))
    p2 = Thread(target=readFs, args=(q,))
    p1.start()
    p1.join()

    p2.start()
    p2.join()

#管道实现4个进程信息交互
from multiprocessing import Process,Pipe,Pool
import time

def send_pip1(pipe):
    while True:
        for i in range(10):
            print("pip1 will send:%d"%i)
            pipe.send(i)
            time.sleep(1)

def send_pip2(pipe):
    while True:
        for j in range(20,40,2):
            print("pip2 will send:%d"%j)
            pipe.send(j)
            time.sleep(1)

def recv_pip1(pipe):
    while True:
        print("pip1 will recv the :%s"%(pipe.recv()))
        time.sleep(1)

def recv_pip2(pipe):
    while True:
        print("pip2 will recv the :%s"%pipe.recv())
        time.sleep(1)


if  __name__=='__main__':
    pipes = Pipe()#全双工

    my_pool = Pool(4)

    my_pool.apply_async(send_pip1, args=(pipes[0],))#connect1 发包
    my_pool.apply_async(recv_pip2, args=(pipes[1],))#connect2 收包

    my_pool.apply_async(send_pip2, args=(pipes[1],))#connect2 发包
    my_pool.apply_async(recv_pip1, args=(pipes[0],))#connect1 收包

    my_pool.close()
    my_pool.join()

参考博客:
python 线程的Event
Python中进程的理解
一文看懂Python多进程
Python中进程和线程总体区别
python 学习笔记 - Queue & Pipes
python面试不得不知道的点——GIL

猜你喜欢

转载自blog.csdn.net/qq_36652517/article/details/86614776