多线程操作 协程

  • 进程和线程的比较
  • GIL全局解释器锁(重要理论)
  • 互斥锁
  • 线程队列(线程里使用队列)
  • 进程池和线程池的用法
  • 协程理论
  • 如何使用协程
  • 基于协程的高并发城程序

进程和线程的比较

1. 进程的开销比线程的开销大很多
2. 进程之间的数据是隔离的,但是,线程之间的数据不隔离
3. 多个进程之间的线程数据不共享----->还是让进程通信(IPC)------->进程下的线程也通信了---->队列

GIL全局解释器锁(重要理论)

首先需要明确的一点是 GIL 并不是 Python 的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比 C++ 是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码

GIL全局解释器锁是存在于CPython中

GIL保护的是解释器级的数据

了解前提

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

python解释器的种类

CPython
这个解释器是用C语言开发的 
当前市场使用的最多(95%)的解释器就是CPython解释器
GIL全局解释器锁是存在于CPython中
IPython
IPython是基于CPython之上的一个交互式解释器
IPython只是在交互方式上有所增强,但是执行Python代码的功能和CPython是完全一样的,好比很多国产浏览器虽然外观不同,但内核其实是调用了IE。
PyPy
PyPy是另一个Python解释器,它的目标是执行速度,PyPy采用JIT技术,对Python代码进行动态编译,所以可以显著提高Python代码的执行速度
Jython
Jython是运行在Java平台上的Python解释器,可以直接把Python代码编译成Java字节码执行。
IronPython
IronPython和Jython类似,只不过IronPython是运行在微软.Net平台上的Python解释器,可以直接把Python代码编译成.Net的字节码。
对Python解释器的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线
程在运行"

介绍

GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。
可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。
首先确定一点:每次执行python程序,都会产生一个独立的进程。
在一个python的进程内,不仅有由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程
总之,所有线程都运行在这一个进程内,毫无疑问
 想避免的问题是,出现多个线程抢夺资源的情况
怎么避免的这个问题,那就是在Python这门语言设计之处,就直接在解释器上添加了一把锁,这把锁就是为了让统一时刻只有一个线程在执行,言外之意就是哪个线程想执行,就必须先拿到这把锁(GIL), 只有等到这个线程把GIL锁释放掉,别的线程才能拿到,然后具备了执行权限.
得出结论:GIL锁就是保证在统一时刻只有一个线程执行,所有的线程必须拿到GIL锁才有执行权限

互斥锁

在多线程的情况下,同时执行一个数据,会发生数据错乱的问题

原理

拿时间换空间,空间换时间 时间复杂度

创建互斥锁

# 导入线程threading模块
import threading
 
# 创建互斥锁
mutex = threading.Lock()

锁定资源/解锁资源

acquire() — 锁定资源,此时资源是锁定状态,其他线程无法修改锁定的资源,直到等待锁定的资源释放之后才能操作;
release() — 释放资源,也称为解锁操作,对锁定的资源解锁,解锁之后其他线程可以对资源正常操作;

互斥锁例子


from threading import Thread
from threading import Lock
import time
n = 10
def task(lock):
    lock.acquire()
    global n
    temp = n
    time.sleep(0.5)#停顿0.5s
    n = temp - 1
    lock.release()

if __name__ == '__main__':
    tt = []#存放线程
    lock=Lock()
    for i in range(10):#10多线程
        t = Thread(target=task, args=(lock, ))
        t.start()
        tt.append(t)#添加到tt列表
    for j in tt:
        j.join()

    print("主", n)

注意:互斥锁一旦锁定之后要记得解锁,否则资源会一直处于锁定状态;

面试题:既然有了GIL锁,为什么还要互斥锁? (多线程下)
	   比如:我起了2个线程,来执行a=a+1,a一开始是0
       1. 第一个线程来了,拿到a=0,开始执行a=a+1,这个时候结果a就是1了
       2. 第一个线程得到的结果1还没有赋值回去给a,这个时候,第二个线程来了,拿到的a是0,继续执行
    		a=a+1结果还是1
       3. 加了互斥锁,就能够解决多线程下操作同一个数据,发生错乱的问题
        

线程队列

为什么线程中还有使用队列?

同一个进程下多个线程数据是共享的
为什么先同一个进程下还会去使用队列呢
因为队列是
    管道 + 锁
所以用队列还是为了保证数据的安全

线程队列:
    1. 先进先出
    2. 后进先出
    3. 优先级的队列
  1. 先进先出

import queue
#先进先出
q=queue.Queue() # 理论无限大、
q.put('first')
q.put('second')
q.put('third')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
  1. 后进先出(queue.LifoQueue())

import queue

# Lifo:last in first out
q=queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
  1. 优先级的队列(queue.PriorityQueue())

## 优先级队列
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())

进程池和线程池的用法

池的概念

池是保证计算机能够正常运行的前提下,能最大限度开采的资源的量,他降低的程序的运行效率,但是保证了计算机硬件的安全,从而让你写的程序可以正常运行。池就是一个量的单位,代表了你所能开采的资源的度,超过这个度,身体就跨了,首先要用,其次要有度。

进程池

提前定义好一个池子,然后,往这个池子里面添加进程,以后,只需要往这个进程池里面丢任务就行了,然后,有这个进程池里面的任意一个进程来执行任务
"""开进程池"""
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
def task(n, m):
    return n+m

def task1():
    return {'username':'kevin', 'password':123}

def callback(res):
    print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
    print(res.result()) # 3

def callback1(res):
    print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
    print(res.result()) # {'username': 'kevin', 'password': 123}
    print(res.result().get('username'))
if __name__ == '__main__':
    pool=ProcessPoolExecutor(3) # 定义一个进程池,里面有3个进程
    ## 2. 往池子里面丢任务

    pool.submit(task, m=1, n=2).add_done_callback(callback)#将task丢到callbak
    pool.submit(task1).add_done_callback(callback1)
    pool.shutdown()  # join + close
    print(123)

线程池

提前定义好一个池子,然后,往这个池子里面添加线程,以后,只需要往这个线程池里面丢任务就行了,然后,有这个线程池里面的任意一个线程来执行任务

"""开进程/线池"""
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
def task(n, m):
    return n+m

def task1():
    return {'username':'kevin', 'password':123}

def callback(res):#回调
    print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
    print(res.result()) # 3

def callback1(res):
    print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
    print(res.result()) # {'username': 'kevin', 'password': 123}
    print(res.result().get('username'))
if __name__ == '__main__':
    # pool=ProcessPoolExecutor(3) # 定义一个进程池,里面有3个进程
    pool=ThreadPoolExecutor(3)#定义一个线程池,里面有3个进程
    ## 2. 往池子里面丢任务

    pool.submit(task, m=1, n=2).add_done_callback(callback)#将task丢到callbak
    pool.submit(task1).add_done_callback(callback1)
    pool.shutdown()  # join + close
    print(123)
多线程爬取网页
import requests

def get_page(url):
    res=requests.get(url)
    name=url.rsplit('/')[-1]+'.html'
    return {'name':name,'text':res.content}

def call_back(fut):
    print(fut.result()['name'])
    with open(fut.result()['name'],'wb') as f:
        f.write(fut.result()['text'])


if __name__ == '__main__':
    pool=ThreadPoolExecutor(2)
    urls=['http://www.baidu.com','http://www.cnblogs.com','http://www.taobao.com']
    for url in urls:
        pool.submit(get_page,url).add_done_callback(call_back)

我们的宗旨是:保证硬件能够正常工作的情况下,最大限度的利用资源。

协程理论

介绍

协程是基于单线程来实现并发,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发,为此我们需要先回顾下并发的本质:切换+保存状态
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
强调:
#1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)

#2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)

协程:

单线程实现并发
    在应用程序里控制多个任务的切换+保存状态
 优点:
        应用程序级别速度要远远高于操作系统的切换
缺点:
        多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地
        该线程内的其他的任务都不能执行了

        一旦引入协程,就需要检测单线程下所有的IO行为,
        实现遇到IO就切换,少一个都不行,因为一旦一个任务阻塞了,整个线程就阻塞了,
        其他的任务即便是可以计算,但是也无法运行了
协程特点:

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

目的

 想要在单线程下实现并发
    并发指的是多个任务看起来是同时运行的
    并发 = 切换 + 保存状态
协程是最节省资源的,进程是最消耗资源的,其次是线程
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。

如何使用协程

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

# 用法
g1=gevent.spawn(func,1,,2,3,x=4,y=5) 创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的

g2=gevent.spawn(func2)

g1.join() # 等待g1结束
 
g2.join() # 等待g2结束

# 或者上述两步合作一步:gevent.joinall([g1,g2])

g1.value # 拿到func1的返回值
from gevent import monkey;
monkey.patch_all()

import gevent
import time
def eat():
    print('eat food 1')
    time.sleep(2)
    print('eat food 2')

def play():
    print('play 1')
    time.sleep(1)
    print('play 2')

start_time = time.time()
g1=gevent.spawn(eat)
g2=gevent.spawn(play)
# gevent.joinall([g1,g2])
g1.join()
g2.join()

print('主', time.time() - start_time)

协程实现高并发

服务端:
from gevent import monkey;

monkey.patch_all()
import gevent
from socket import socket
# from multiprocessing import Process
from threading import Thread


def talk(conn):
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0: break
            print(data)
            conn.send(data.upper())
        except Exception as e:
            print(e)
    conn.close()


def server(ip, port):
    server = socket()
    server.bind((ip, port))
    server.listen(5)
    while True:
        conn, addr = server.accept()
        # t=Process(target=talk,args=(conn,))
        # t=Thread(target=talk,args=(conn,))
        # t.start()
        gevent.spawn(talk, conn)


if __name__ == '__main__':
    g1 = gevent.spawn(server, '127.0.0.1', 8080)
    g1.join()
客户端:
	import socket
from threading import current_thread, Thread


def socket_client():
    cli = socket.socket()
    cli.connect(('127.0.0.1', 8080))
    while True:
        ss = '%s say hello' % current_thread().getName()
        cli.send(ss.encode('utf-8'))
        data = cli.recv(1024)
        print(data)


for i in range(5000):
    t = Thread(target=socket_client)
    t.start()