py17day10

 

操作系统的作用:
    1.把硬件丑陋复杂的接口隐藏起来,为应用程序提供良好接口
    2.管理,调用进程,并且把进程之间对硬件的竞争变得有序化

多道技术:
    1.产生背景:为了实现单cpu下的并发效果
    2.分为两部分:
        1:空间上的复用(必须实现硬件层面的隔离)
        2:时间上的复用(复用cpu的时间片)
            什么切换?
                1:正在执行的任务遇到的阻塞
                2:正在执行的任务运行时间过长

进程:正在运行的一个过程/一个任务,由操作系统负责调用,然后由cpu负责执行
程序:就是程序员写的代码
并发:伪并行,单核+多道
并行:只有多核才能实现真正的并行

同步:打电话,一个进程在执行某个任务时,另外一个进程必须等待其执行完毕,才能继续执行
异步:发短信,一个进程在执行某个任务时,另外一个进程无需等待其执行完毕,就可以继续执行,当有消息返回时,系统会通知后者进行处理,这样可以提高执行效率

进程的创建:
    1. 系统初始化
    2. 与用户交互
    3. 在执行一个进程的过程中调用(Popen,os.fork)
    4.批处理任务

系统的调用:
    linux:fork
    win:CreateProcess

linux的下的进程与windows下的区别:
    1:linux的进程有父子关系,是一种树形结构,windows没有这种关系
    2:linux创建新的进程需要copy父进程的地址空间,win下从最开始创建进程,两个进程之间就是不一样
上节内容复习

 

一、Python并发编程之多线程

  线程:一条流水线的执行过程是一个线程,一条流水线必须属于一个车间,一个车间的运行过程就是一个进程(一个进程内至少一个线程)

  进程是资源单位,而线程才是cpu上的执行单位。

   多线程:一个车间内有多条流水线,多个流水线共享该车间的资源(多线程共享一个进程的资源)

   线程创建的开销要远远小于进程

为何要创建多线程?
    1. 共享资源
    2. 创建开销小

  进程之间是竞争关系,线程之间是协助关系

1.开启线程的两种方式(同Process):

 

#方式一:
from threading import Thread

def work(name):
    print('%s say hi!'%name)


if __name__ == '__main__':
    t = Thread(target=work,args=('Dylan',))
    t.start()
    print('这是主进程')

 

 

#方式二:
from threading import Thread

class Work(Thread):
    def run(self):
        print('%s say hi!'%self.name)

if __name__ == '__main__':
    t = Work()
    t.start()
    print('这是主进程')

2.创建线程与进程开销对比:

#先来看线程:
from threading import Thread

def work(name):
    print('%s say hi!'%name)


if __name__ == '__main__':
    t = Thread(target=work,args=('Dylan',))
    t.start()
    print('这是主进程')

运行结果:
Dylan say hi!
这是主进程       # 主进程在后执行

 

#再看进程:
from multiprocessing import  Process

def work(name):
    print('%s say hi!'%name)


if __name__ == '__main__':
    p = Process(target=work,args=('Dylan',))
    p.start()
    print('这是主进程')

运行结果:
这是主进程    # 主进程在创建进程前执行
Dylan say hi!

  可见创建进程的开销远大于线程。

练习:

from socket import *
from threading import Thread

def server(ip,port):
    s = socket(AF_INET,SOCK_STREAM)
    s.bind((ip,port))
    s.listen(5)
    while True:
        conn,addr = s.accept()
        t = Thread(target=talk,args=(conn,addr))
        t.start()
        print('Client:%s:%s connect successful.'%(addr[0],addr[1]))



def talk(conn,addr):
    while True:
        try:
            msg = conn.recv(1024)
            if not msg : break
            conn.send(msg.upper())
        except Exception:
            break


if __name__ == '__main__':
    server('127.0.0.1',8080)
多线程并发socket_server
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if not msg:continue
    c.send(msg.encode('utf-8'))
    res=c.recv(1024)
    print(res.decode('utf-8'))
客户端
from threading import Thread
msg_l=[]
format_l=[]

def msg():
    while True:
        msg = input(">>:")
        if not msg:continue
        msg_l.append(msg)

def format():
    while True:
        if not msg_l:continue
        f_msg = msg_l.pop().upper()
        format_l.append(f_msg)

def save():
    while True:
        if not format_l:continue
        s_msg = format_l.pop()
        with open('db.txt','a',encoding='utf-8') as f:
            f.write('%s\n'%s_msg)

if __name__ == '__main__':
    t1 = Thread(target=msg)
    t2 = Thread(target=format)
    t3 = Thread(target=save)
    t1.start()
    t2.start()
    t3.start()
模拟文本编辑工具

3.线程的一些方法:

  与进程的方法都是类似的,其实是multiprocessing模仿threading的接口

join与setdaemon:

from threading import Thread
import time

def work(name):
    time.sleep(1)
    print('%s say hi!'%name)


if __name__ == '__main__':
    t = Thread(target=work,args=('Dylan',))
    t.setDaemon(True) # 设置t为守护线程
    t.start()
    t.join() # 等待t结束
    print('这是主线程程')
    print(t.is_alive())

Thread实例对象的方法:
  # isAlive(): 返回线程是否活动的。

  # getName(): 返回线程名。

  # setName(): 设置线程名。

threading模块提供的一些方法:

  # threading.currentThread(): 返回当前的线程变量。

  # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。

  # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

from threading import Thread
import threading
import time

def work(name):
    time.sleep(1)
    print('%s say hi!'%name)
    print(threading.current_thread().getName()) # 打印当前线程名:Thread-1


if __name__ == '__main__':
    t = Thread(target=work,args=('Dylan',))
    t.start()
    print(threading.current_thread()) #打印当前线程:主线程
    print(threading.enumerate()) # 当前运行的所有线程:主线程和t
    print(threading.active_count()) # 当前运行所有线程数量
    print('这是主线程程')

运行结果:
<_MainThread(MainThread, started 222676)>
[<Thread(Thread-1, started 222696)>, <_MainThread(MainThread, started 222676)>]
2
这是主线程程
Dylan say hi!
Thread-1

 

 

4. Python GIL(Global Interpreter Lock):

GIL:全局解释器锁

'''

定义:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple 
native threads from executing Python bytecodes at once. This lock is necessary mainly 
because CPython’s memory management is not thread-safe. (However, since the GIL 
exists, other features have grown to depend on the guarantees that it enforces.)

'''
关于GIL

  因为有GIL的存在,在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势

  首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

(关于GIL透彻剖析:http://www.dabeaz.com/python/UnderstandingGIL.pdf

 

 

   有了GIL的存在,同一时刻统一进程中只有一个线程被执行

  进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势

  要解决这个问题,我们需要在几个点上达成一致:

    1. cpu到底是用来做计算的,还是用来做I/O的?

    1. 多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能

    2. 每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处

 

 

  有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:

    方案一:开启四个进程

    方案二:一个进程下,开启四个线程

 

  单核情况下,分析结果:

  如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜

  如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜

 

  多核情况下,分析结果:

  如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜

  如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜

 

  结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

# 计算密集型
from threading import Thread
from multiprocessing import Process
import os
import time

def work():
    res = 0
    for i in range(1000000):
        res+=1

if __name__ == '__main__':
    t_l = []
    start_time = time.time()
    for i in range(100):
        # t = Thread(target=work) # runtime:22.27627396583557
        t = Process(target=work) # runtime:20.163153409957886
        t.start()
        t_l.append(t)

    for t in t_l:
        t.join()
    stop_time = time.time()

    print('runtime:%s'%(stop_time-start_time))

 

# IO密集型
from threading import Thread
from multiprocessing import Process
import os
import time

def work():
    time.sleep(2)

if __name__ == '__main__':
    t_l = []
    start_time = time.time()
    for i in range(100):
        t = Thread(target=work) # runtime:2.033116340637207
        # t = Process(target=work) # runtime:12.702726602554321
        t.start()
        t_l.append(t)

    for t in t_l:
        t.join()
    stop_time = time.time()

    print('runtime:%s'%(stop_time-start_time))

应用:

  多线程用于IO密集型,如socket,爬虫,web
  多进程用于计算密集型,如金融分析

5.线程的互斥锁:

 

from threading import Thread
import time

num = 100 #设定一个共享变量
def work():
    global num #在每个线程中都获取这个全局变量
    temp = num
    time.sleep(0.1)
    num = temp - 1 # 对此公共变量进行-1操作

if __name__ == '__main__':
    t_l = []
    for i in range(100):
        t = Thread(target=work)
        t.start()
        t_l.append(t)
    for t in t_l: #等待所有线程执行完毕
        t.join()

    print(num)

运行结果:
99
不加锁
#加锁
from threading import Thread,Lock
import time

num = 100 #设定一个共享变量
def work():
    with mutex:
        global num #在每个线程中都获取这个全局变量
        temp = num
        time.sleep(0.1)
        num = temp - 1 # 对此公共变量进行-1操作

if __name__ == '__main__':
    t_l = []
    mutex = Lock()
    for i in range(100):
        t = Thread(target=work)
        t.start()
        t_l.append(t)
    for t in t_l: #等待所有线程执行完毕
        t.join()

    print(num)

运行结果:
0

  锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁:

import threading

R=threading.Lock()

R.acquire()
'''
对公共数据的操作
'''
R.release()

GIL VS Lock

  Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 

  首先我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据

  然后,我们可以得出结论:保护不同的数据就应该加不同的锁

  最后,问题就很明朗了,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock    

  详细的:

  因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题,  这可以说是Python早期版本的遗留问题。 

 6.死锁与递归锁:

  所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

  如下就是死锁:

from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()

class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('\033[41m%s 拿到A锁\033[0m' %self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B锁\033[0m' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B锁\033[0m' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[45m%s 拿到A锁\033[0m' %self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(10):
        t=MyThread()
        t.start()

 

  使用递归锁解决死锁问题:

  递归锁:在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

  这个RLock内部维护着一个Lock和一个counter变量,counter(相当于计数器)记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

'''
mutexA=mutexB=threading.RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
'''
from threading import Thread,RLock
import time
mutexA=mutexB=RLock()

class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('\033[41m%s 拿到A锁\033[0m' %self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B锁\033[0m' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B锁\033[0m' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[45m%s 拿到A锁\033[0m' %self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(10):
        t=MyThread()
        t.start()

 

7.信号量Semahpore

  Semaphore管理一个内置的计数器,每当调用acquire()时内置计数器-1;调用release() 时内置计数器+1;

  计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

 

  实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):

 

 

from threading import Thread,Semaphore
import time
def work(id):
    with sem:
        time.sleep(2)
        print('%s working !'%id)

if __name__ == '__main__':
    sem = Semaphore(5)
    for i in range(20):
        t = Thread(target=work,args=(i,))
        t.start()

#运行结果:
2 working !
1 working !
0 working !
4 working !
3 working !

5 working !
6 working !
7 working !
8 working !
9 working !

10 working !
12 working !
11 working !
13 working !
14 working !

15 working !
17 working !
18 working !
16 working !
19 working !

 

与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程

 

 

8.Event事件:

  线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就 会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将这个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

 

event.isSet():返回event的状态值;

event.wait():如果 event.isSet()==False将阻塞线程;

event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;

event.clear():恢复event的状态值为False。

 

  可以考虑一种应用场景(仅仅作为说明),例如,我们有多个线程从Redis队列中读取数据来处理,这些线程都要尝试去连接Redis的服务,一般情况下,如果Redis连接不成功,在各个线程的代码中,都会去尝试重新连接。如果我们想要在启动时确保Redis服务正常,才让那些工作线程去连接Redis服务器,那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作:主线程中会去尝试连接Redis服务,如果正常的话,触发事件,各工作线程会尝试连接Redis服务。

  这里用mysql举例:

from threading import Event,Thread
import threading
import time

e = Event() # 设置时间对象

def conn_mysql():
    print('%s waiting for connect mysql...'%threading.current_thread().getName())
    e.wait() # 等待事件状态变为True
    time.sleep(1)
    print('%s connected!'%threading.current_thread().getName())

def check_mysql():
    print('%s checking connect...'%threading.current_thread().getName())
    time.sleep(3)
    print('%s mysql connect is alive.'%threading.current_thread().getName())
    e.set() # 将时间状态设置为True

if __name__ == '__main__':
    c1 = Thread(target=conn_mysql)
    c2 = Thread(target=conn_mysql)

    c3 = Thread(target=check_mysql)

    c1.start()
    c2.start()
    c3.start()

运行结果:
Thread-1 waiting for connect mysql...
Thread-2 waiting for connect mysql...
Thread-3 checking connect...
Thread-3 mysql connect is alive.
Thread-1 connected!
Thread-2 connected!

  threading.Event的wait方法还接受一个超时参数,默认情况下如果事件一致没有发生,wait方法会一直阻塞下去,而加入这个超时参数之后,如果阻塞时间超过这个参数设定的值之后,wait方法会返回。对应于上面的应用场景,如果Redis服务器一致没有启动,我们希望子线程能够打印一些日志来不断地提醒我们当前没有一个可以连接的Redis服务,我们就可以通过设置这个超时参数来达成这样的目的:

from threading import Event,Thread
import threading
import time

e = Event() # 设置时间对象

def conn_mysql():
    print('%s waiting for connect mysql...'%threading.current_thread().getName())
    e.wait(0.5) # 等待事件状态变为True,超时时间为0.5,超时则不阻塞,继续执行
    time.sleep(1)
    print('%s connected!'%threading.current_thread().getName())

def check_mysql():
    print('%s checking connect...'%threading.current_thread().getName())
    time.sleep(3)
    print('%s mysql connect is alive.'%threading.current_thread().getName())
    e.set() # 将时间状态设置为True

if __name__ == '__main__':
    c1 = Thread(target=conn_mysql)
    c2 = Thread(target=conn_mysql)

    c3 = Thread(target=check_mysql)

    c1.start()
    c2.start()
    c3.start()

运行结果:
Thread-1 waiting for connect mysql...
Thread-2 waiting for connect mysql...
Thread-3 checking connect...
Thread-2 connected!
Thread-1 connected!
Thread-3 mysql connect is alive.
修改上述mysql版

 

9.定时器:

 定时器,指定n秒后执行某操作:

from threading import Timer
 
 
def hello():
    print("hello, world")
 
t = Timer(1, hello)
t.start()  # after 1 seconds, "hello, world" will be printed

 

10.线程Queue:

  queue队列 :使用import queue,用法与进程Queue一样

class queue.Queue(maxsize=0) #先进先出

#
import queue

q=queue.Queue() #先进先出--->队列
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
运行结果:
first
second
third
'''

class queue.LifoQueue(maxsize=0) #last in fisrt out 

import queue

q=queue.LifoQueue() #后进先出--->堆栈
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
'''
运行结果(后进先出):
third
second
first
'''

class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列

import queue

q=queue.PriorityQueue()
#put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))

print(q.get())
print(q.get())
print(q.get())
'''
运行结果(数字越小优先级越高,优先级高的优先出队):
(10, 'b')
(20, 'a')
(30, 'c')
'''

 

 

 

 

 

二、协程

  协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

需要强调的是:

  1. python的线程属于内核级别的,即由操作系统控制调度(如单线程一旦遇到io就被迫交出cpu执行权限,切换其他线程运行)

  2. 单线程内开启协程,一旦遇到io,从应用程序级别(而非操作系统)控制切换

对比操作系统控制线程的切换,用户在单线程内控制协程的切换,优点如下:

  • 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
  • 单线程内就可以实现并发的效果,最大限度地利用cpu

 

  要实现协程,关键在于用户程序自己控制程序切换,切换之前必须由用户程序自己保存协程上一次调用时的状态,如此,每次重新调用时,能够从上次的位置继续执行

(详细的:协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈)

 

  为此,我们之前已经学习过一种在单线程下可以保存程序运行状态的方法,即yield,我们来简单复习一下:

  1. yiled可以保存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级
  2. send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换
#不用yield:每次函数调用,都需要重复开辟内存空间,即重复创建名称空间,因而开销很大
import time

def consumer(item):
    a = 11111111111
    b = 22222222222
    c = 33333333333
    d = 44444444444
    e = 55555555555
    f = 66666666666
    pass


def producer(target):
    for i in range(10000000):
        target(i)  #每次调用函数,会临时产生名称空间,调用结束则释放,循环10000000次,则重复这么多次的创建和释放,开销非常大

start_time = time.time()
producer(consumer)
stop_time = time.time()

print('run time: %s'%(stop_time-start_time)) # run time: 5.060288906097412

 

#使用yield:无需重复开辟内存空间,即重复创建名称空间,因而开销小
import time

def consumer():
    a = 11111111111
    b = 22222222222
    c = 33333333333
    d = 44444444444
    e = 55555555555
    f = 66666666666
    while True:
         item = yield
    pass


def producer(target):
    for i in range(10000000):
        target.send(i) #无需重新创建名称空间,从上一次暂停的位置继续,相比上例,开销小

g = consumer()
next(g) # 初始化生成器函数

start_time = time.time()
producer(g)
stop_time = time.time()

print('run time: %s'%(stop_time-start_time)) # run time: 3.6112060546875

缺点:

  协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程

  协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

 

协程的定义(满足1,2,3就可称为协程):

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁
  3. 用户程序里自己保存多个控制流的上下文栈
  4. 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))

  yield切换在没有io的情况下或者没有重复开辟内存空间的操作,对效率没有什么提升,甚至更慢,为此,可以用greenlet

1.greenlet:

  greenlet是一个用C实现的协程模块,相比与python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator

 

from greenlet import greenlet

def test1():
    print('test1,first')
    gr2.switch()
    print('test1,sencod')
    gr2.switch()
def test2():
    print('test2,first')
    gr1.switch()
    print('test2,sencod')


gr1=greenlet(test1)
gr2=greenlet(test2)
gr1.switch()

运行结果:
test1,first
test2,first
test1,sencod
test2,sencod

 

  greenlet只是提供了一种比generator更加便捷的切换方式,仍然是没有解决遇到IO自动切换的问题

 

2.Gevent:

  Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

g1=gevent.spawn()创建一个协程对象g1,

spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的

 

# 遇到IO阻塞时会自动切换任务
import gevent
import time

def eat(name):
    print('%s eat 1'%name)
    gevent.sleep(5) # 阻塞
    print('%s eat 2'%name)

def play(name):
    print('%s play 1'%name)
    gevent.sleep(3) # 阻塞
    print('%s play 2'%name)

g1 = gevent.spawn(eat,name='Dylan')
g2 = gevent.spawn(play,name='Dylan')
g1.join()  # 需要join方法,主线程才不会先执行
g2.join()    
print('这是主线程')

  上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,

  而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了

  from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前

  或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头

from gevent import monkey;monkey.patch_all()
import gevent
import time

def eat(name):
    print('%s eat 1'%name)
    time.sleep(5)
    print('%s eat 2'%name)

def play(name):
    print('%s play 1'%name)
    time.sleep(3)
    print('%s play 2'%name)

g1 = gevent.spawn(eat,name='Dylan')
g2 = gevent.spawn(play,name='Dylan')
g1.join()
g2.join()
print('这是主线程')

运行结果:
Dylan eat 1
Dylan play 1
Dylan play 2
Dylan eat 2
这是主线程

 

from gevent import monkey;monkey.patch_all()
import requests
import gevent
import time

def get_url(url):
    print('get page:%s'%url)
    response = requests.get(url)
    if response.status_code == 200:
        print(response.text)

start_time = time.time()
g1=gevent.spawn(get_url,url='https://www.python.org')
g2=gevent.spawn(get_url,url='https://www.yahoo.com')
g3=gevent.spawn(get_url,url='https://www.github.com')
gevent.joinall([g1,g2,g3])
stop_time = time.time()

print('run time:%s'%(stop_time-start_time)) # run time:4.201239109039307
协程爬取网页内容

 

 

from gevent import monkey;monkey.patch_all()
from socket import *
import gevent

def server(ip,port):
    s = socket(AF_INET,SOCK_STREAM)
    s.bind((ip,port))
    s.listen(5)
    while True:
        conn,addr = s.accept()
        gevent.spawn(talk,conn,addr)
        print('Client:%s:%s connect successful.'%(addr[0],addr[1]))



def talk(conn,addr):
    while True:
        try:
            msg = conn.recv(1024)
            if not msg : break
            print('msg:%s form %s:%s'%(msg,addr[0],addr[1]))
            conn.send(msg.upper())
        except Exception:
            break

if __name__ == '__main__':
    server('127.0.0.1',8080)
协程实现并发socket_server
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if not msg:continue
    c.send(msg.encode('utf-8'))
    res=c.recv(1024)
    print(res.decode('utf-8'))
客户端
from threading import Thread
from socket import *
import threading

def client(server_ip,port):
    c=socket(AF_INET,SOCK_STREAM)
    c.connect((server_ip,port))

    count=0
    while True:
        c.send(('%s say hello %s' %(threading.current_thread().getName(),count)).encode('utf-8'))
        msg=c.recv(1024)
        print(msg.decode('utf-8'))
        count+=1
if __name__ == '__main__':
    for i in range(500):
        t=Thread(target=client,args=('127.0.0.1',8080))
        t.start()
利用多线程模拟多个客户端并发请求服务端

 

 

三、socketserver

 

 

 

import socketserver

class Socket_demo(socketserver.BaseRequestHandler):
    def handle(self):
        msg = self.request.recv(1024)
        print('client<%s:%s> msg:%s'%(self.client_address[0],self.client_address[1],msg))
        self.request.send(msg.upper())


if __name__ == '__main__':
    socketserver.TCPServer.allow_reuse_address = True
    # 相当于setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s = socketserver.ThreadingTCPServer(('127.0.0.1',8084),Socket_demo)
    s.serve_forever()

 

 

 

 

 

 

 

 

 

 

 

四、基于udp的socket

 

# udp 服务端

from socket import *

s = socket(AF_INET,SOCK_DGRAM)
s.bind(('127.0.0.1',8080))
# udp服务端不需要建立连接

while True:
    msg,addr = s.recvfrom(1024)
    print(msg,addr)
    s.sendto(msg.upper(),addr)

 

# udp 客户端

from socket import *

c = socket(AF_INET,SOCK_DGRAM)

while True:
    msg = input('>>:').strip()
    # 由于udp发送的是数据报文,会包含报头等信息,即使msg为空,也能发送
    c.sendto(msg.encode('utf-8'),('127.0.0.1',8080))
    # 相应的,客户端也不需要连接,直接向指定地址发消息就行
    server_msg,server_addr = c.recvfrom(1024)
    print(server_msg.decode('utf-8'),server_addr)

关于发送空值:

  tcp是基于数据流的,而udp是基于数据报的:

  1. send(bytes_data):发送数据流,数据流bytes_data若为空,自己这段的缓冲区也为空,操作系统不会控制tcp协议发空包
  2. sendinto(bytes_data,ip_port):发送数据报,bytes_data为空,还有ip_port,所以即便是发送空的bytes_data,数据报其实也不是空的,自己这端的缓冲区收到内容,操作系统就会控制udp协议发包。

关于粘包问题:

  将上述的客户端接收缓冲区大小改为(1)

# udp client
from socket import *

c = socket(AF_INET,SOCK_DGRAM)

while True:
    msg = input('>>:').strip()
    c.sendto(msg.encode('utf-8'),('127.0.0.1',8081))
    server_msg,server_addr = c.recvfrom(1)
    # recvfrom()改为1,查看是否会发生粘包现象
    print(server_msg.decode('utf-8'),server_addr)

 

# windows上运行结果:
>>:Hello Elaine!How are you!
Traceback (most recent call last):
  File "D:/python/py17/code/py17day10/test_code/socket_udp_client.py", line 19, in <module>
    server_msg,server_addr = c.recvfrom(1)
OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小
# linux上运行结果
>>:Hello!Elaine!How are you!
H ('127.0.0.1', 8081)
>>:Good day!
GOOD DAY! ('127.0.0.1', 8081) # 不会发生粘包

  可见,recvfrom收的数据小于sendinto发送的数据时,在mac和linux系统上数据直接丢失,在windows系统上发送的比接收的大直接报错,所以不会发生粘包现象

1.tcp协议:

(1)如果收消息缓冲区里的数据为空,那么recv就会阻塞(阻塞很简单,就是一直在等着收)

(2)只不过tcp协议的客户端send一个空数据就是真的空数据,客户端即使有无穷个send空,也跟没有一个样。

(3)tcp基于链接通信

  • 基于链接,则需要listen(backlog),指定半连接池的大小
  • 基于链接,必须先运行的服务端,然后客户端发起链接请求
  • 对于mac系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端在收消息后加上if判断,空消息就break掉通信循环)
  • 对于windows/linux系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端通信循环内加异常处理,捕捉到异常后就break掉通讯循环)

 

 

2.udp协议:

(1)如果如果收消息缓冲区里的数据为“空”,recvfrom也会阻塞

(2)只不过udp协议的客户端sendinto一个空数据并不是真的空数据(包含:空数据+地址信息,得到的报仍然不会为空),所以客户端只要有一个sendinto(不管是否发送空数据,都不是真的空数据),服务端就可以recvfrom到数据。

(3)udp无链接

  • 无链接,因而无需listen(backlog),更加没有什么连接池之说了
  • 无链接,udp的sendinto不用管是否有一个正在运行的服务端,可以己端一个劲的发消息,只不过数据丢失
  • recvfrom收的数据小于sendinto发送的数据时,在mac和linux系统上数据直接丢失,在windows系统上发送的比接收的大直接报错
  • 只有sendinto发送数据没有recvfrom收数据,数据丢失

 

3.socketserver实现并发udp服务端:

import socketserver

class Udphandler(socketserver.BaseRequestHandler):
    def handle(self):
        client_msg,conn = self.request
        print('msg:%s from<%s>'%(client_msg,self.client_address))
        conn.sendto(client_msg.upper(),self.client_address)
if __name__ == '__main__':
    s = socketserver.ThreadingUDPServer(('127.0.0.1',8082),Udphandler)
    s.serve_forever()

 

posted @ 2017-07-02 18:49  Dylan_Wu  阅读(165)  评论(0编辑  收藏  举报