【python】基于多线程实现事件驱动引擎


前言

为了开发一个属于自己的量化框架,于是深入学习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和通过继承实现进程。
下面介绍如何使用这个模块对进程进行管理。

  1. 直接实例化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分配进程的先后顺序。

  1. 进程实例中传递参数
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()方法。

  1. 进程池的使用
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,))就称为阻塞式执行方式。
在这里插入图片描述
程序是一个进程执行完毕,执行另外一个进程。

  1. 进程间通讯
    前面介绍进程的概念时,提到进程时不共享全局变量的,每个进程拥有自己独立的资源和私有空间,那么进程间的通讯如何实现呢?
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创建线程

  1. 直接实例化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 进程和线程的区别

前面的介绍进程知识中,每个进程都有自己独立的资源,进程之间互不影响,那么线程之间呢?

  1. 使用线程修改全局变量
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 同步

当一个线程修改变量时,其他线程在读取这个变量的值时就可能看到不一致的数据。因此必须保证数据的一致性,下面介绍同步的概念。

  1. 同步的概念
    为了解释同步的概念,先看一段代码:
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源码解读(三、事件驱动引擎代码分析)

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Python提供了多线程模块`threading`,可以方便地实现多线程抓取数据并存入数据库的方法。 首先,我们需要导入必要的库和模块。常用的库包括`requests`用于发送HTTP请求,`BeautifulSoup`用于解析HTML内容,`threading`用于启动和管理多线程,以及数据库库比如`mysql-connector-python`用于与数据库交互等。 接下来,我们可以定义一个函数来实现数据的抓取和存入数据库的逻辑。这个函数会接受一些参数,比如抓取数据的URL,要存入数据库的表名等。 在函数内部,我们会使用`requests`库发送HTTP请求并获取响应内容。然后,使用`BeautifulSoup`解析响应内容,提取需要的数据。 将提取得到的数据存入数据库之前,需要先与数据库建立连接。连接数据库可以使用`mysql-connector-python`或者其他合适的数据库库。之后,可以使用SQL语句将数据插入到指定的表中。 在主函数中,我们可以创建多个线程来并发执行数据抓取和存储的任务。每个线程负责一个特定的URL或者一组URL的抓取。可以通过创建线程对象并调用其`start()`方法来启动线程。 最后,我们需要在主函数中等待所有线程的完成,可以通过调用`join()`方法实现。 通过上述方法,我们就可以实现基于多线程的数据抓取并存入数据库的功能。多线程的特点可以提高抓取和存储的效率,加快整个过程的执行速度。当然,我们还需要考虑多线程的并发性和同步问题,比如避免多个线程同时操作数据库的同一部分等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

m 宽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值