day35---并发编程之协程

1. GIL(Global Interpreter Lock)

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

  • 描述:GIL的本质是互斥锁,将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全;保护不同的数据的安全,就应该加不同的锁

  • 介绍:所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的;所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这会导致同一个数据正在被运行的同时也会被回收,解决这种问题的方式就是枷锁处理。GIL,就是保证Python解释器在同一时间只能执行一个任务的代码

  • GIL与Lock的区别:

    • GIL保护的是解释器级的数据
    • Lock保护的是文件级的数据
  • GIL的应用场景:

    • 每一个Cpython进程内都有一个GIL
    • GIL导致同一个进程内的多个线程同一时间只能有一个运行
    • GIL是存在是因为Cpython的内存管理不是线程安全的
    • 对于计算型密集型使用多进程
    • 对于I/O密集型使用多线程

2. 多线程性能测试

  • 单核情况下:

    • 计算密集型的任务,使用多线程的方式效率高
    • I/O密集型的任务,使用多线程的方式效率高
  • 多核情况下:

    • 计算密集型的任务,使用多进程的方式效率高
    • I/O密集型的任务,使用多线程的方式效率高
# 计算密集型
import os
import time
from multiprocessing import Process
from threading import Thread
def task():
    res = 0
    for i in range(10000000):
        res *= i
if __name__ == '__main__':
    print('本机cpu核心数是[%s]核' % os.cpu_count())
    print('计算密集型...')
    p_list = []
    t_list = []
    p_start_time = time.time()
    for i in range(10):
        p = Process(target=task)
        p.start()
        p_list.append(p)
    for j in p_list:
        p.join()
    print('多进程的运行时间:%s' % (time.time() - p_start_time))
    t_start_time = time.time()
    for i in range(10):
        t = Thread(target=task)
        t.start()
        t_list.append(t)
    for j in t_list:
        t.join()
    print('多线程的运行时间:%s' % (time.time() - t_start_time))
    print('yan...')
>>>
本机cpu核心数是[4]核
计算密集型...
多进程的运行时间:4.771674394607544
多线程的运行时间:6.147441625595093
yan...
# I/O密集型
import os
import time
from multiprocessing import Process
from threading import Thread
def task():
    time.sleep(2)
if __name__ == '__main__':
    print('本机cpu核心数是[%s]核' % os.cpu_count())
    print('I/O密集型...')
    p_list = []
    t_list = []
    p_start_time = time.time()
    for i in range(10):
        p = Process(target=task)
        p.start()
        p_list.append(p)
    for j in p_list:
        p.join()
    print('多进程的运行时间:%s' % (time.time() - p_start_time))
    t_start_time = time.time()
    for i in range(10):
        t = Thread(target=task)
        t.start()
        t_list.append(t)
    for j in t_list:
        t.join()
    print('多线程的运行时间:%s' % (time.time() - t_start_time))
    print('yan...')
>>>
本机cpu核心数是[4]核
I/O密集型...
多进程的运行时间:2.6464638710021973
多线程的运行时间:2.0020933151245117
yan...

3. 死锁现象和递归锁

  • 死锁:两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,这些永远在互相等待的进程或线程称为死锁

# 死锁的例子
from threading import Thread, Lock
mutex1 = Lock()
mutex2 = Lock()
class Mythread(Thread):
        def run(self):
            self.task1()
            self.task2()
        def task1(self):
            mutex1.acquire()
            print('%s ---> 锁1' % self.name)
            mutex2.acquire()
            print('%s ---> 锁2' % self.name)
            mutex2.release()
            mutex1.release()
        def task2(self):
            mutex2.acquire()
            print('%s ---> 锁2' % self.name)
            mutex1.acquire()
            print('%s ---> 锁1' % self.name)
            mutex1.release()
            mutex2.release()
if __name__ == '__main__':
        for i in range(10):
            t = Mythread()
            t.start()
>>>
Thread-1 ---> 锁1
Thread-1 ---> 锁2
Thread-1 ---> 锁2
Thread-2 ---> 锁1
  • 递归锁:解决死锁的方式,可重入锁(RLock)

  • RLock:可重入锁;内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源;上例中RLock代替Lock,则不会发生死锁

from threading import Thread, RLock
mutex1 = mutex2 = RLock()
class Mythread(Thread):
        def run(self):
            self.task1()
            self.task2()
        def task1(self):
            mutex1.acquire()
            print('%s ---> 锁1' % self.name)
            mutex2.acquire()
            print('%s ---> 锁2' % self.name)
            mutex2.release()
            mutex1.release()
        def task2(self):
            mutex2.acquire()
            print('%s ---> 锁2' % self.name)
            mutex1.acquire()
            print('%s ---> 锁1' % self.name)
            mutex1.release()
            mutex2.release()
if __name__ == '__main__':
        for i in range(10):
            t = Mythread()
            t.start()
>>>
Thread-1 ---> 锁1
Thread-1 ---> 锁2
Thread-1 ---> 锁2
Thread-1 ---> 锁1
Thread-2 ---> 锁1
Thread-2 ---> 锁2
Thread-2 ---> 锁2
Thread-2 ---> 锁1
Thread-3 ---> 锁1
Thread-3 ---> 锁2
Thread-3 ---> 锁2
Thread-3 ---> 锁1
Thread-5 ---> 锁1
Thread-5 ---> 锁2
Thread-5 ---> 锁2
Thread-5 ---> 锁1
Thread-6 ---> 锁1
Thread-6 ---> 锁2
Thread-6 ---> 锁2
Thread-6 ---> 锁1
Thread-7 ---> 锁1
Thread-7 ---> 锁2
Thread-7 ---> 锁2
Thread-7 ---> 锁1
Thread-4 ---> 锁1
Thread-4 ---> 锁2
Thread-4 ---> 锁2
Thread-4 ---> 锁1
Thread-10 ---> 锁1
Thread-10 ---> 锁2
Thread-10 ---> 锁2
Thread-10 ---> 锁1
Thread-9 ---> 锁1
Thread-9 ---> 锁2
Thread-8 ---> 锁1
Thread-8 ---> 锁2
Thread-8 ---> 锁2
Thread-8 ---> 锁1
Thread-9 ---> 锁2
Thread-9 ---> 锁1

4. 定时器

  • 描述:指定时间后执行的操作;类似time.sleep(n)休眠n秒的时间后在运行下面的程序代码

  • 使用定时器需要导入模块:from threading import Timer

  • 参数:Timer(n, func, args)

    • n秒后运行函数func
    • args和kwargs指定传入func的参数,元组的形式指定位置传参,字典的形式指定关键字传参
  • 方法:

    • start():启动定时器线程
    • join():等待定时器线程运行结束
    • cancel():取消定时器线程
import time
from threading import Timer
def task(name, age, sex='girl', job='IT'):
    print('name ---> %s' % name)
    print('age ---> %s' % age)
    print('sex ---> %s' % sex)
    print('job ---> %s' % job)
print('yan...')
t = Timer(2, task, args=('yy', 18), kwargs={'job': 'Admin'})
start_time = time.time()
t.start()
t.join()
print(time.time() - start_time)
>>>
yan...
name ---> yy
age ---> 18
sex ---> girl
job ---> Admin
2.0005106925964355

5. 协程

  • 引子:cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长。对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程
  • 本质:在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:
    • 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行
    • 可以检测io操作,在遇到io操作的情况下才发生切换
  • 说明:单线程下实现并发,又叫微线程、纤程,英文名Coroutine。协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的

  • 实现方式:切换+保存状态

  • 强调:

    • python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
    • 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(非io操作的切换与效率无关)
  • 优点:

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

    • 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
    • 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
  • 特点:

    • 必须在只有一个单线程里实现并发
    • 修改共享数据不需加锁
    • 用户程序里自己保存多个控制流的上下文栈
    • 附加特点:一个协程遇到I/O操作自动切换到其它协程(yield和greenlet都无法实现检测I/O,可以使用gevent模块(select机制))

6. gevent模块

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

  • 作用:

    • 切换+保存状态
    • 检测单线程下任务的IO,实现遇到IO自动切换
  • 安装:pip install gevent

  • 安装后导入模块:import gevent

  • 实例化的参数:

    • gevent.spawn(func, args/kwargs):指定运行的函数,后跟传入的参数
    • value:得到函数的返回值
    • join():等待实例运行结束
    • gevent.joinall():同时等待多个实例运行结束(多个实例保存在一个列表中)
    • gevent.sleep(n):遇到I/O阻塞n秒钟;gevent无法直接识别其他阻塞(time.sleep()),需要打补丁才可以,补丁必须在文件的开头,放在导入其他模块之前
    • 查看每一个实例的名字,使用current_thread().getName(),结果是DummyThread-n,即假线程(虚拟线程),实际上只有一个线程
    from gevent import monkey;monkey.patch_all()
    
from gevent import monkey;monkey.patch_all()
import time
import random
import gevent
from threading import current_thread
def eat():
    print('eat ---> food...')
    time.sleep(random.randint(1, 3))
    print('eat ---> fruit...')
    return current_thread().getName()
def drink():
    print('drink ---> water...')
    time.sleep(random.randint(1, 3))
    print('drink ---> Cola...')
    return current_thread().getName()
def play():
    print('play ---> games...')
    time.sleep(random.randint(1, 3))
    print('play ---> balls...')
    return current_thread().getName()
g1 = gevent.spawn(eat)
g2 = gevent.spawn(drink)
g3 = gevent.spawn(play)
# g1.join()
# g2.join()
# g3.join()
gevent.joinall([g1, g2, g3])
gevent.sleep(2)
print(g1.value)
print(g2.value)
print(g3.value)
time.sleep(1)
print('yan...')
>>>
eat ---> food...
drink ---> water...
play ---> games...
drink ---> Cola...
play ---> balls...
eat ---> fruit...
DummyThread-3
DummyThread-1
DummyThread-2
yan...

7. 应用

  • 协程的同步与异步

from gevent import monkey; monkey.patch_all()
import time
import gevent
from threading import current_thread
def task():
        time.sleep(1)
        print('[%s] is done...' % current_thread().getName())
def synchronous():
        synchronous_start_time = time.time()
        for i in range(10):
            g = gevent.spawn(task)
            g.join()
        print('用时:%s' % (time.time() - synchronous_start_time))
def asynchronous():
        asynchronous_start_time = time.time()
        g_list = []
        for i in range(10):
            g = gevent.spawn(task)
            g_list.append(g)
        gevent.joinall(g_list)
        print('用时:%s' % (time.time() - asynchronous_start_time))
if __name__ == '__main__':
        print('synchronous:')
        synchronous()
        print('-' * 20)
        print('asynchronous:')
        asynchronous()
>>>
synchronous:
[DummyThread-1] is done...
[DummyThread-2] is done...
[DummyThread-3] is done...
[DummyThread-4] is done...
[DummyThread-5] is done...
[DummyThread-6] is done...
[DummyThread-7] is done...
[DummyThread-8] is done...
[DummyThread-9] is done...
[DummyThread-10] is done...
用时:10.008307695388794
--------------------
asynchronous:
[DummyThread-11] is done...
[DummyThread-12] is done...
[DummyThread-13] is done...
[DummyThread-14] is done...
[DummyThread-15] is done...
[DummyThread-16] is done...
[DummyThread-17] is done...
[DummyThread-18] is done...
[DummyThread-19] is done...
[DummyThread-20] is done...
用时:1.0008456707000732
  • 使用协程实现简单web页面爬虫

from gevent import monkey; monkey.patch_all()
import time
import gevent
import requests
def get_page(url):
        print('get %s' % url)
        response = requests.get(url)
        if response.status_code == 200:
            print('%s的页面大小是:%s' % (url, len(response.text)))
if __name__ == '__main__':
        urls = [
            'http://www.baidu.com',
            'http://www.tmall.com',
            'http://www.jd.com',
            'http://www.iqiyi.com',
            'http://www.youku.com'
        ]
        start_time = time.time()
        g_list = []
        for i in urls:
            g = gevent.spawn(get_page, i)
            g_list.append(g)
        gevent.joinall(g_list)
        print('用时:%s' %(time.time() - start_time))
>>>
get http://www.baidu.com
get http://www.tmall.com
get http://www.jd.com
get http://www.iqiyi.com
get http://www.youku.com
http://www.baidu.com的页面大小是:2381
http://www.iqiyi.com的页面大小是:214584
http://www.youku.com的页面大小是:686324
http://www.tmall.com的页面大小是:215146
http://www.jd.com的页面大小是:129123
用时:0.8404057025909424
  • 使用携程实现socket套接字并发

# 服务端
from gevent import monkey; monkey.patch_all()
import gevent
import subprocess
import json
import struct
from socket import *
def server_socket(ip, port):
        server = socket(AF_INET, SOCK_STREAM)
        server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        server.bind((ip, port))
        server.listen(5)
        while True:
            conn, add = server.accept()
            print('客户端[%s]成功连接,端口是[%s]' % (add[0], add[1]))
            gevent.spawn(shell_result, conn)
        server.close()
def shell_result(conn):
        while True:
            try:
                shell = conn.recv(1024)
                if not shell:
                    break
                obj = subprocess.Popen(shell.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                stdout = obj.stdout.read()
                stderr = obj.stderr.read()
                header_dict = {'total_size': len(stdout) + len(stderr)}
                header_json = json.dumps(header_dict)
                header_bytes = header_json.encode('utf-8')
                header_size = struct.pack('i', len(header_bytes))
                conn.send(header_size)
                conn.send(header_bytes)
                conn.send(stdout)
                conn.send(stderr)
            except ConnectionResetError:
                break
        conn.close()
if __name__ == '__main__':
        ip = '127.0.0.1'
        port = 8080
        server_socket(ip, port)
# 客户端1
import json
import struct
from socket import *
ip = '127.0.0.1'
port = 8080
client = socket(AF_INET, SOCK_STREAM)
client.connect((ip, port))
def shell_run(shell):
        client.send(shell.encode('utf-8'))
        header_len = client.recv(4)
        header_size = struct.unpack('i', header_len)[0]
        header_bytes = client.recv(header_size)
        header_json = header_bytes.decode('utf-8')
        header_dict = json.loads(header_json)
        total_size = header_dict['total_size']
        recv_size = 0
        data = b''
        while recv_size < total_size:
            recv_data = client.recv(1024)
            data += recv_data
            recv_size += len(recv_data)
        print(data.decode('gbk'))
if __name__ == '__main__':
        while True:
            shell = input('请输入需要执行的系统命令:').strip()
            if not shell:
                continue
            shell_run(shell)
        client.close()
# 客户端2
import json
import struct
from socket import *
ip = '127.0.0.1'
port = 8080
client = socket(AF_INET, SOCK_STREAM)
client.connect((ip, port))
def shell_run(shell):
        client.send(shell.encode('utf-8'))
        header_len = client.recv(4)
        header_size = struct.unpack('i', header_len)[0]
        header_bytes = client.recv(header_size)
        header_json = header_bytes.decode('utf-8')
        header_dict = json.loads(header_json)
        total_size = header_dict['total_size']
        recv_size = 0
        data = b''
        while recv_size < total_size:
            recv_data = client.recv(1024)
            data += recv_data
            recv_size += len(recv_data)
        print(data.decode('gbk'))
if __name__ == '__main__':
        while True:
            shell = input('请输入需要执行的系统命令:').strip()
            if not shell:
                continue
            shell_run(shell)
        client.close()
posted @ 2017-12-05 20:01  _岩哥  阅读(115)  评论(0)    收藏  举报