文章目录
前言
为了开发一个属于自己的量化框架,于是深入学习python高级知识。本文主要讲解进程、线程概念和python实现过程,最后给出事件驱动引擎框架,方便后续开发。
1. 理解多任务和进程
1.1 下面通过一个范例理解单任务和多任务的概念。
from time import sleep
def sing():
for i in range(3):
print("正在唱歌%d" % i)
sleep(0.5)
def dance():
for i in range(3):
print("正在跳舞%d" % i)
sleep(0.5)
if __name__ == '__main__':
sing()
dance()
上面两个函数按照时间顺序调用执行,不能同时运行,这就是单任务。多任务就是多个任务同时进行,即两个函数并发执行。
一个程序是静态的,通常存放在外存中,而当程序被调入内存中运行后,就变成了进程,进程是动态的概念,是系统进行资源分配和调度的基本单位。
1.2 Multiprocessing模块
使用Multiprocessing模块创建进程对象有两种方式:直接实例化Process和通过继承实现进程。
下面介绍如何使用这个模块对进程进行管理。
- 直接实例化Process
from multiprocessing import Process
import os
import time
def work():
print('子进程pid=%s,work...' % os.getpid())
if __name__ == '__main__':
print('begin')
process = Process(target=work)
print(process.name)
process.start()
time.sleep(1)
print('end')
代码中 process = Process(target=work)
创建一个进程对象,将进程实例化,指定进程在运行时需要完成的任务,使用target参数指定要执行的work()函数;不过这时进程只是创建了,还没有执行;而process.start()表示进程就绪,此时等待获取cpu的使用权;因为有sleep(1)语句,暂停主进程的执行,因此先执行子进程,如果这条语句去掉,就是这个结果。
可见,结果取决于CPU分配进程的先后顺序。
- 进程实例中传递参数
import os
import time
def func(a, b):
print(a, b)
print("子进程%s" % os.getpid())
if __name__ == '__main__':
print('begin')
process = Process(target=func, args=(1, 2))
process.start()
time.sleep(1)
print('end')
3. 守护进程的使用
守护进程也称为后台进程。主进程时非守护型的,进程创建默认也是非守护的。如果一个程序中所有非守护进程都停止了,只有守护进程,那么程序就会停止。守护进程可以通过实例对象daemon属性进行设置的,如果设置为True,就会守护进程·。
守护进程(daemon)是一类在后台运行的特殊进程,是一个在后台运行并且不受任何终端控制的进程。用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。守护进程最重要的特性是后台运行。
from multiprocessing import Process
import os
import time
def func(a, b):
print(a, b)
time.sleep(1)
print("子进程%s" % os.getpid())
if __name__ == '__main__':
print('begin')
process = Process(target=func, args=(1, 2))
process.daemon = True
process.start()
time.sleep(3)
print('end')
守护进程是伴随主进程运行的,如果主进程结束,守护进程也结束,读者可以更改两个sleep的时间,让主进程时间小于守护进程时间,看看会发生什么情况?
1.3 通过继承来实现进程
前面知道Process是multprocessing的一个子类,并且可以定义自己的子类,继承process类,下面介绍如何通过继承实现进程。
from multiprocessing import Process
import os
import time
class Myprocess(Process):
def __init__(self, pname, a=1, b=2):
Process.__init__(self, name=pname)
self.a = a
self.b = b
def run(self) -> None:
print(self.a, self.b)
print('子进程:%s,父进程%s' % (os.getpid(), os.getppid()))
for i in range(3):
print(i)
time.sleep(0.5)
if __name__ == '__main__':
print("主进程%s" % os.getpid())
myprocess = Myprocess("myprocess")
print(myprocess.name)
myprocess.start()
myprocess2 = Myprocess("myprocess2")
print(myprocess2.name)
myprocess2.start()
myprocess.join()
myprocess2.join()
print("主进程结束%s" % os.getpid())
自定义Myprocess类中init定义了自己的参数,重写run方法,当进程调用的时候,会自动执行run方法。join()方法可以保证子进程运行结束才继续执行父进程。
1.4 进程池
python中,进程池内部会维护一个进程序列。当需要时,程序会去进程池中获取一个进程。如果进程池序列中没有可以使用的进程,那么程序就会等待,直到进程池中有可用进程为止。当进程较少时,占用系统资源较少时,就不需要进程池,直接用Multiprocessing中的Process动态生成多个进程;当系统进程较多时,成百上千目标,手动创建进程工作量大,如何合理调度进程,就需要使用进程池了。通过系统合理分配任务,来提高系统性能,尽量实现真正的并行处理,提示系统处理效率,此时就可以用到Multiprocessing模块提供的pool()方法。
- 进程池的使用
from multiprocessing import Pool
import os, time, random
def worker(num):
print("begin %s进程号:%s" % (num,os.getpid()))
time.sleep(random.random())
print("end %s进程号:%s" % (num,os.getpid()))
if __name__ == '__main__':
process_pool = Pool(4)
print("begin,主进程pid:%s"%os.getpid())
for i in range(1,11):
process_pool.apply_async(worker,(i,))
process_pool.close()
process_pool.join()
print("end,主进程pid:%s"%os.getpid())
可以看出4个进程执行顺序是1,2,3,4开始执行。上面代码使用的是非阻塞式执行方式,如果 process_pool.apply_async(worker,(i,))
改为
process_pool.apply(worker,(i,))
就称为阻塞式执行方式。
程序是一个进程执行完毕,执行另外一个进程。
- 进程间通讯
前面介绍进程的概念时,提到进程时不共享全局变量的,每个进程拥有自己独立的资源和私有空间,那么进程间的通讯如何实现呢?
from multiprocessing import Process, Queue
import os, time, random
def write(q):
for v in ['a', 'b', 'c', 'd']:
print('put %s to queue' % v)
# 往队列放数据
q.put(v)
time.sleep(random.random())
def read(q):
while True:
# 判断队列是否为空
if not q.empty():
v = q.get()
print('get %s from queue' % v)
time.sleep(random.random())
else:
break
if __name__ == '__main__':
q = Queue()
p_write = Process(target=write, name='p_write', args=(q,))
p_write.start()
p_write.join()
p_read = Process(target=read, name='p_write', args=(q,))
p_read.start()
p_read.join()
print("所有数据写入完毕,读取完毕")
这段代码运行结果如图:
使用进程池,进行通信,代码如下:
if __name__ == '__main__':
p_pool = Pool(2)
q = Manager().Queue()
p_pool.apply(write, args=[q])
p_pool.apply(read, args=[q])
p_pool.close()
p_pool.join()
print("所有数据写入完毕,读取完毕")
运行结果和上面实例一样。
2.线程
当进程执行时,每个进程至少包含一个线程,所有的线程运行在同一个进程中,共享相同的运行资源和环境,线程一般是并发执行的,实现了多任务的并行和数据共享。
2.1 使用threading创建线程
- 直接实例化Thread
from threading import Thread, current_thread
import os
import time
def work():
print("子线程(name=%s) work..." % (current_thread().name))
time.sleep(4)
print(123)
if __name__ == '__main__':
print("主线程(name=%s) work..." % (current_thread().name))
# 创建线程对象
thread = Thread(target=work, name="my_thread")
print(thread.name)
thread.start()
time.sleep(1)
print("主线程%s...end" % (current_thread().name))
2. 继承实现
import random
from threading import Thread, current_thread
import os
import time
class MyThread(Thread):
def __init__(self, pname, a=1, b=2):
Thread.__init__(self, name=pname)
self.a = a
self.b = b
def run(self) -> None:
print(self.a, self.b)
print("子线程(name=%s) work..." % (current_thread().name))
for i in range(3):
print(i)
time.sleep(random.random())
if __name__ == '__main__':
print("主线程(name=%s) work..." % (current_thread().name))
# 创建线程对象
thread = MyThread(pname="my_thread")
print(thread.name)
thread.start()
time.sleep(3)
print("主线程%s...end" % (current_thread().name))
2.2 进程和线程的区别
前面的介绍进程知识中,每个进程都有自己独立的资源,进程之间互不影响,那么线程之间呢?
- 使用线程修改全局变量
import random
from threading import Thread, current_thread
import os
import time
num = 100
def f1():
global num
num += 1
print("%s...num=%s\n" % (num, current_thread().name))
def f2():
global num
num += 1
print("%s...num=%s\n" % (num, current_thread().name))
if __name__ == '__main__':
print("主线程(name=%s) work..." % (current_thread().name))
t1 = Thread(target=f1,name="f1")
t2 = Thread(target=f2,name="f2")
t1.start()
t2.start()
t1.join()
t2.join()
print("%s主线程%s...end" % (num,current_thread().name))
结果如下:
可以发现,两个线程都修改了全局变量的值,说明在线程中,资源是共享的。
一个进程内部可能包含了很多顺序执行流,每个顺序执行流就是一个线程。现在的操作系统大多使用抢占式多任务操作策略,以支持多进程的并发性,而多线程是多进程的扩展,使得一个进程也可以进行多项任务并发处理。线程是进程中并发执行操作的基本单位,因此被称为轻量级进程。一个进程包含多个线程,每个线程都有自己的父进程。
线程开销小,但不利于资源的维护,而进程恰恰相反,可以根据以下原则进行选择。
- 如果是需要共享资源的,建议使用线程。
- 如果是计算密集型。如算法运算,建议使用多进程,并且进程的数量可以等于CPU的数量。
- 如果是I/O密集型的数据处理,如果web、文件传输等,运行会阻塞,速度远小于cpu熟读。可以多开辟一些线程来完成任务,即便一个任务阻塞了,其他任务也可以照常运行。
2.3 同步
当一个线程修改变量时,其他线程在读取这个变量的值时就可能看到不一致的数据。因此必须保证数据的一致性,下面介绍同步的概念。
- 同步的概念
为了解释同步的概念,先看一段代码:
import threading
import time
g_num = 0
def func1():
global g_num
for i in range(1000000):
g_num = g_num + 1
print("----test----g_num=%d" % g_num)
def func2():
global g_num
for i in range(1000000):
g_num = g_num + 1
print("----test----g_num=%d" % g_num)
if __name__ == '__main__':
t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)
t1.start()
t2.start()
time.sleep(3)
print("g_num=%d"% g_num)
运行代码:
发现最后的值不是2000000,这是为什么呢?
主要是因为线程共享全局变量,cpu进行资源调度时,某一时刻时线程1访问全局变量,另一时刻时线程2访问全局变量,这样就会造成数据混乱,为什么会出现上面的那个值呢?原因在于,假如某一时刻,g_num=100,func2开始操作+1,这时候时101,func1拿到这个数据是100,但还没进行计算,func2计算完成后,func1开始计算,最后g_num的值=101,所以最终g_num不可能到2000000。
2.4 互斥锁
那么如何解决数据共享混乱问题呢?这时候需要线程的同步,通过协同步调,让程序按照约定的顺序执行,可以使用threading的Lock类。
Lock类使用Lock()创建一个同步锁对象,使用acquire()方法获取锁并进行上锁,如果上锁成功返回true,使用release()释放锁。在一个锁已经上锁还没有解锁的情况下,如果在上锁就会报错。
import threading
import time
from threading import Lock
g_num = 0
def func1():
global g_num
if my_lock.acquire():
for i in range(1000000):
g_num = g_num + 1
my_lock.release()
print("----test----g_num=%d" % g_num)
def func2():
global g_num
if my_lock.acquire():
for i in range(1000000):
g_num = g_num + 1
my_lock.release()
print("----test----g_num=%d" % g_num)
if __name__ == '__main__':
my_lock = Lock()
t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)
t1.start()
t2.start()
time.sleep(3)
print("g_num=%d" % g_num)
可以看出,两个线程达到了同步的效果,实现了每个线程的循环叠加。
2.5 死锁
如果两个线程,每个线程都对自己的资源进行加锁,然后又想使用对方的资源,企图对另一个资源加锁,就会产生死锁。即线程A等待线程B释放锁,线程B等待线程A释放锁。下面通过实例来理解:
import threading
import time
from threading import Lock
class Mythread1(threading.Thread):
def run(self) -> None:
if mutexA.acquire():
print(self.name + "do1---up")
time.sleep(1)
if mutexB.acquire():
print(self.name + "do----down")
mutexB.release()
mutexA.release()
class Mythread2(threading.Thread):
def run(self) -> None:
if mutexB.acquire():
print(self.name + "do1---up")
time.sleep(1)
if mutexA.acquire():
print(self.name + "do----down")
mutexA.release()
mutexB.release()
if __name__ == '__main__':
mutexA = threading.Lock()
mutexB = threading.Lock()
t1 = Mythread1()
t2 = Mythread2()
t1.start()
t2.start()
运行结果:
程序卡死在这个位置。
2.6 ThreadLocal用法
假设一个进程中有多个线程分别执行不同的任务,这些任务有可能在执行过程中需要调用某个变量或对象,此时可以将变量或对象以方法实参的形式传给其它方法或函数,但这种方式存在传递参数过多,代码复杂的缺点。如果定义成全局变量,又需要锁同步。现在就使用ThreadLocal来实现,即把局部变量绑定到当前线程,此时只要获取当前线程,即可使用这个线程中的所有局部变量。
import threading
thread_local = threading.local()
def f1(name):
print("f1...")
thread_local.name = name
f2()
def f2():
print("f2...")
name = thread_local.name
print("hello:%s...tname...%s" % (name, threading.current_thread().name))
if __name__ == '__main__':
t1 = threading.Thread(target=f1, args=["小明"], name="t1")
t2 = threading.Thread(target=f1, args=["小红"], name="t2")
t1.start()
t2.start()
f2和f1实现name局部变量共享。
2.7 异步
前面介绍的同步概念是保持线程之间协同步调,按照约定好的顺序执行,实现过程需要借助锁才可以实现。而异步是指在处理过程中不用阻塞当前进程等待处理完成,而是运行后续操作,直到其它线程处理完成,并返回调用通知该线程。
实际上,异步I/O是一种单线程的编程模式,换句话说,尽管在单个进程中使用了一个线程,但异步I/O给人一种并发的效果。协同调用不是并行。 异步IO是一种并发编程风格。意味着不要将宝贵的CPU时间浪费在一个被I/O等待的任务,事件循环通过不断轮询任务队列,以确保立即调度并运行一个处于非I/O等待的任务。
通过上述机制,异步代码有助于并发执行。细说Python异步编程模式
import asyncio
import time
import random
start=time.time()
def take_time():
return "%1.2f秒" % (time.time()-start)
async def task_A():
print("运行task_A")
await asyncio.sleep(random.uniform(1.0,8.0)/10)
print(f"task_A结束!!耗时{take_time()}")
async def task_B():
print("运行task_B")
await asyncio.sleep(random.uniform(1.0,8.0)/10)
print(f"task_B结束!!耗时{take_time()}")
async def task_C():
print("运行task_C")
await asyncio.sleep(random.uniform(1.0,8.0)/10)
print(f"task_C结束!!耗时{take_time()}")
async def task_exect():
tasks=[task_A(),task_B(),task_C()]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(task_exect())
3. 基于多线程实现事件驱动引擎
# 系统模块
import datetime
from queue import Queue, Empty
from threading import Thread
from time import sleep
from collections import defaultdict
EVENT_TIMER = 'eTimer' # 计时器事件,每隔1秒发送一次
########################################################################
class EventEngine(object):
"""
计时器使用python线程的事件驱动引擎
"""
# ----------------------------------------------------------------------
def __init__(self):
"""初始化事件引擎"""
# 事件队列
self.__queue = Queue()
# 事件引擎开关
self.__active = False
# 事件处理线程
self.__thread = Thread(target=self.__run)
# 计时器,用于触发计时器事件
self.__timer = Thread(target=self.__runTimer)
self.__timerActive = False # 计时器工作状态
self.__timerSleep = 1 # 计时器触发间隔(默认1秒)
# 这里的__handlers是一个字典,用来保存对应的事件调用关系
# 其中每个键对应的值是一个列表,列表中保存了对该事件进行监听的函数功能
self.__handlers = defaultdict(list)
# __generalHandlers是一个列表,用来保存通用回调函数(所有事件均调用)
self.__generalHandlers = []
# ----------------------------------------------------------------------
def __run(self):
"""引擎运行"""
while self.__active == True:
try:
event = self.__queue.get(block=True, timeout=1) # 获取事件的阻塞时间设为1秒
self.__process(event)
except Empty:
pass
# ----------------------------------------------------------------------
def __process(self, event):
"""处理事件"""
# 检查是否存在对该事件进行监听的处理函数
if event.type_ in self.__handlers:
# 若存在,则按顺序将事件传递给处理函数执行
# [handler(event) for handler in self.__handlers[event.type_]]
# 以上语句为Python列表解析方式的写法,对应的常规循环写法为:
for handler in self.__handlers[event.type_]:
handler(event)
# 调用通用处理函数进行处理
if self.__generalHandlers:
for handler in self.__generalHandlers:
handler(event)
# ----------------------------------------------------------------------
def __runTimer(self):
"""运行在计时器线程中的循环函数"""
while self.__timerActive:
# 创建计时器事件
event = Event(type_=EVENT_TIMER)
# 向队列中存入计时器事件
self.put(event)
# 等待
sleep(self.__timerSleep)
# ----------------------------------------------------------------------
def start(self, timer=True):
"""
引擎启动
timer:是否要启动计时器
"""
# 将引擎设为启动
self.__active = True
# 启动事件处理线程
self.__thread.start()
# 启动计时器,计时器事件间隔默认设定为1秒
if timer:
self.__timerActive = True
self.__timer.start()
# ----------------------------------------------------------------------
def stop(self):
"""停止引擎"""
# 将引擎设为停止
self.__active = False
# 停止计时器
self.__timerActive = False
self.__timer.join()
# 等待事件处理线程退出
self.__thread.join()
# ----------------------------------------------------------------------
def register(self, type_, handler):
"""注册事件处理函数监听"""
# 尝试获取该事件类型对应的处理函数列表,若无defaultDict会自动创建新的list
handlerList = self.__handlers[type_]
# 若要注册的处理器不在该事件的处理器列表中,则注册该事件
if handler not in handlerList:
handlerList.append(handler)
# ----------------------------------------------------------------------
def unregister(self, type_, handler):
"""注销事件处理函数监听"""
# 尝试获取该事件类型对应的处理函数列表,若无则忽略该次注销请求
handlerList = self.__handlers[type_]
# 如果该函数存在于列表中,则移除
if handler in handlerList:
handlerList.remove(handler)
# 如果函数列表为空,则从引擎中移除该事件类型
if not handlerList:
del self.__handlers[type_]
# ----------------------------------------------------------------------
def put(self, event):
"""向事件队列中存入事件"""
self.__queue.put(event)
# ----------------------------------------------------------------------
def registerGeneralHandler(self, handler):
"""注册通用事件处理函数监听"""
if handler not in self.__generalHandlers:
self.__generalHandlers.append(handler)
# ----------------------------------------------------------------------
def unregisterGeneralHandler(self, handler):
"""注销通用事件处理函数监听"""
if handler in self.__generalHandlers:
self.__generalHandlers.remove(handler)
########################################################################
class Event:
"""事件对象"""
# ----------------------------------------------------------------------
def __init__(self, type_=None, dict_=None):
"""Constructor"""
self.type_ = type_ # 事件类型
self.dict_ = dict_ # 字典用于保存具体的事件数据
# 测试事件监听
def test():
LogEvent = Event(type_="log", dict_={"交易时间": [2019, 2020, 2021], "交易金额": [100, 101, 102]})
def fun(event):
print(event.dict_["交易时间"])
print(datetime.datetime.now())
def fun2(event):
print(event.dict_["交易金额"])
print(datetime.datetime.now())
gg = EventEngine()
gg.put(LogEvent)
gg.register(type_="log", handler=fun)
gg.register(type_="log", handler=fun2)
gg.start()
# 测试通用事件处理函数监听
def test2():
# 模拟系统启动流程
# 行情获取
def get_data(event):
print(event.type_)
print(datetime.datetime.now())
print("行情获取")
# 调用日志事件
def record(event):
print(event.type_)
test()
print("日志记录完成")
print("-" * 40)
gg = EventEngine()
gg.registerGeneralHandler(get_data)
gg.registerGeneralHandler(record)
gg.start()
if __name__ == '__main__':
test2()
这篇博文详细分析了该框架的各个部分。vn.py源码解读(三、事件驱动引擎代码分析)