7.并发编程

多进程和多线程

Windows下使用multiprocessing实现多进程。

from multiprocessing import Process
import os

# 子进程的函数
def run_proc(name):
    # os.getpid()获取进程号
    print('Run child process %s (%s)...' % (name, os.getpid()))

if __name__ == '__main__':
    # 父进程即main函数
    print('Parent process %s.' % os.getpid())
    # 创建子进程
    p = Process(target = run_proc, args = ('test',))
    # 开启子进程
    p.start()
    # 等待子进程的结束
    p.join()
    # End标志父进程结束
    print('End')
    
"""
Parent process 5852.
Run child process test (8536)...
End
"""

Python中有两个多线程库:_threadthreading模块,但是建议不要直接使用_thread,因为_thread过于接近底层,不方便移植。

import threading

def loop():
    # 获取当前线程的name属性
    thread_name = threading.current_thread().name
    print('Thread %s is running...' % thread_name)
    n = 0
    # 执行一个计算任务
    while n < 5:
        n = n + 1
        print('Thread %s >>> %d' % (thread_name, n))
    print('Thread %s ends.' % thread_name)

# 获取当前线程
thread_name = threading.current_thread().name
print('Thread %s is running...' % thread_name)
# 创建子线程
t = threading.Thread(target = loop, name = 'loopThread')
# 启动子线程
t.start()
# 等待子线程结束
t.join()
# 主线程结束
print('Thread %s ends.' % thread_name)

"""
Thread MainThread is running... # 主线程MainThread
Thread loopThread is running... # 子线程loopThread
Thread loopThread >>> 1
Thread loopThread >>> 2
Thread loopThread >>> 3
Thread loopThread >>> 4
Thread loopThread >>> 5
Thread loopThread ends.
Thread MainThread ends.
"""

多线程往往面临资源的竞争,以存款操作为例:

"""
多线程操作银行存款
"""
import threading

balance = 0

def change_it(n):
    """
    操作存款, 先存后取, 0*n=0
    """
    global balance
    balance = balance + n
    balance = balance - n
    balance = balance * n

# 理论上, 如果在run_thread中, 确保每次都完整执行完一遍change_it函数, 存款应当是不变的
# 即不论操作多少次, 存款应该是不变的
# 换言之, 如果没有线程的切换, balance最后还是0
def run_thread(n):
    for i in range(10000):
        change_it(n)

# 创建子线程t1: 每次存取5元, 创建子线程t2: 每次存取8元
t1 = threading.Thread(target = run_thread, args = (5, ))
t2 = threading.Thread(target = run_thread, args = (8, ))

# 开启子线程t1
t1.start()
# 开启子线程t2
t2.start()
# 等待子线程t1结束
t1.join()
# 等待子线程t2结束
t2.join()

# 打印最后的存款, 顺序执行情况下(线程顺序执行, 没有切换)应该是0
# 由于现在是多线程, 可能存在线程的切换, 存款最后可能不再是0
print(balance)

balance就是一个公共资源,由于多线程的切换,可能导致操作资源后出现错误的结果。为了避免这个错误,我们需要在脚本中控制资源的操作,比如加锁,使用run_thread_lock代替run_thread

lock = threading.Lock()

def run_thread_lock(n):
    for i in range(10000):
    	# 先获得锁
        lock.acquire()
        try:
        	# 再进行计算修改
            change_it(n)
        finally:
        	# 最后释放锁
            lock.release()

使用tryfinally的意义在于:不管哪个线程执行change_it出现问题,最后都能把锁释放出来留给其他线程。如果没有finally,很可能某个线程在执行change_it时出现异常不再执行,而不能进行下一步释放锁,其他线程也得不到锁,就会出现死锁。

为了更方便地保证线程的安全(线程安全就是多线程访问同一资源,不会产生不确定的结果),Python提供了队列结构(使得线程对资源的操作是遵循规则的),队列分为三种:

  • FIFO,先进先出;
import queue
import threading

# FIFO
q = queue.Queue()
for i in range(5):
    q.put(i)

while not q.empty():
    print(q.get())
"""
0
1
2
3
4
"""
  • LIFO:后进先出,即堆栈;
# LIFO
q = queue.LifoQueue()
for i in range(5):
    q.put(i)

while not q.empty():
    print(q.get())
"""
4
3
2
1
0
"""
  • Priority Queue:优先队列,数据进入后进行排序,再根据优先级pop;
class Task:
    def __init__(self, priority, description):
        self.priority = priority # 任务的优先级
        self.description = description # 任务的描述信息

    # 该魔法方法用于实现对象之间直接比较, 而不需要采用方法调用
    def __lt__(self, other):
        return self.priority < other.priority

q = queue.PriorityQueue()
# 在元素(Task的实例)put到优先队列时, 自动调用__lt__比较各个实例, 并排序
q.put(Task(1, 'Important task'))
q.put(Task(10, 'Normal task'))
q.put(Task(100, 'Lazy task'))

# 从优先队列中pop元素
def job(q):
    while True:
        task = q.get()
        print('Task: %s\n' % task.description)
        q.task_done()

# 创建两个线程同时操作q中的资源
threads = [threading.Thread(target = job, args = (q, )),
           threading.Thread(target = job, args = (q, ))]
for t in threads:
    # setDaemon(true)来设置线程为“守护线程”, 将一个用户线程设置为守护线程的方式是: 在线程对象创建之前, 调用线程对象的setDaemon方法
    # 守护线程也称“服务线程”,在没有用户线程可服务时会自动离开, 守护线程的优先级比较低, 用于为系统中的其它对象和线程提供服务。
    t.setDaemon(True)
    t.start()
# 等待线程q结束, 即q内元素为空
q.join()
"""
可见, 两个线程能够按照优先级合理执行Task

Task: Important task

Task: Normal task

Task: Lazy task
"""

关于GIL
多线程可以运行在单核上,也可以运行在多核上。一个线程可以某一时间段在一个核心上运行,下一刻在另一个核心上运行。线程是内核调度的最小单位。 一个进程可以有多个线程,它们共同完成某个任务。 线程是被包裹在进程中的,进程提供了线程运行的资源。 进程之间互不影响,一个进程挂掉,并不影响其它进程,然而一个进程内的一个线程出现问题 ,其它线程也无法正常运行。

为了方便开发人员保护公共资源,Python在使用多线程利用多核时,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个,这使得Python的多线程几乎就是单线程。

而上面对于多线程的编程实例,主要目的是让我们了解多线程编程。在Python中,还是使用多进程才能真正提高效率。


我们使用线程局部变量也可以确保线程安全:

import threading

# 线程的局部变量
local_school = threading.local()

def process_student():
    std = local_school.student
    print('Hello %s (%s)\n' % (std, threading.current_thread().name))

def process_thread(name):
    local_school.student = name
    process_student()

t1 = threading.Thread(target = process_thread, args = ('Tom', ), name = 'TA')
t2 = threading.Thread(target = process_thread, args = ('Jack', ), name = 'TB')
t1.start()
t2.start()
t1.join()
t2.join()
"""
Hello Tom (TA)

Hello Jack (TB)
"""

我们创建了两个线程,并且分别向两个线程传递不同的参数,两个线程共同操作了线程变量local_school,如果不定义local_school是线程局部的,将会使得线程不安全,因为此时local_school是物理上共用的。然而,由于local_school是线程局部的,该变量将在每个线程内保持"独立",其实是为两个线程开辟了两个资源。

线程池

线程和进程的上下文切换,创建和销毁都需要成本。线程池可以重用已存在的线程,降低线程创建和销毁造成的消耗:

import time
import threadpool

def long_op(n):
    print('%d' % n)
    time.sleep(2)

# 创建线程池, 线程池内的线程数量小于单个CPU的核数
pool = threadpool.ThreadPool(2)
# 创建了10个任务
tasks = threadpool.makeRequests(long_op, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
print("len of task: ",len(tasks))
# 把任务扔进线程池
[pool.putRequest(task) for task in tasks]
pool.wait()
"""
中间的空行代表线程池重新创建线程
在同时使用线程池内所有线程后, 就销毁并重建, 因为此时线程池内没有空线程可切换
len of task:  10
12

34

56

78

910
"""

异步IO

假设现在只有一个单核CPU,存在三个线程,一个调度线程S,两个工作线程W1和W2,时间是在三个线程上分配的:

  • W1有5个时间长度XXX'X'X,其中X'代表等待外部设备的时间,本身不占用CPU时间;
  • W2有7个时间长度YY'Y'Y'YYY

在普通的情况下,时间进度可能为SXXYY'SSX'...,S最开始分配到时间,然后执行W1,又切换到执行W2,此时W2开始访问外部设备,S先询问W2,W2还在等待外部设备,S询问W1的进度,W1不需要等待外部设备,然后执行W1,此时已经轮到W1访问外部设备,然后S将不停询问W1和W2的进度,已确认已经访问完外部设备,才继续执行W1或W2。

可发现,S总是把时间花在询问W1和W2上,这造成了一些CPU时间的浪费。因此,需要异步IO,当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。

异步IO可以提高单个单核CPU上的利用率。

股票信息获取的并发编程

Python都已经有了不同方式的相关封装,我们只需调用它们即可。

直接开辟多线程方式

import requests
import threading

def get_stock(code):
    url = 'http://hq.xxx.cn/list=' + code
    resp = requests.get(url).text
    print('%s\n' % resp)

codes = ['sz000878', 'sh600993', 'sz000002', 'sh600153', 'sz002230', 'sh600658']
threads = [threading.Thread(target = get_stock, args = (code, )) for code in codes]
for t in threads:
    t.start()
for t in threads:
    t.join()

线程池方式

import requests
import threadpool

def get_stock(code):
    url = 'http://hq.xxx.cn/list=' + code
    resp = requests.get(url).text
    print('%s\n' % resp)

codes = ['sz000878', 'sh600993', 'sz000002', 'sh600153', 'sz002230', 'sh600658']
pool = threadpool.ThreadPool(2)
tasks = threadpool.makeRequests(get_stock, codes)
[pool.putRequest(task) for task in tasks]
pool.wait()

异步IO方式

import aiohttp
import asyncio

# 该装饰器可以使任务在执行时是异步IO的方式
@asyncio.coroutine
def get_stock(code):
    url = 'http://hq.xxx.cn/list=' + code
    resp = yield from aiohttp.request('GET', url)
    body = yield from resp.read()
    print(body.decode('gb2312'))

codes = ['sz000878', 'sh600993', 'sz000002', 'sh600153', 'sz002230', 'sh600658']
# 使用异步IO的消息循环机制
tasks = [get_stock(code) for code in codes]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值