• Python之线程、进程


    一、进程、线程

    1、进程

      进程就是一个程序在一个数据集上的一次动态执行过程;

      进程一般由程序、数据集、进程控制块三部分组成;

      我们编写的程序用来描述进程要完成哪些功能以及如何完成;

      数据集则是程序在执行过程中所需要使用的资源;

      进程控制块用来记录程序的外部特征,描述进程执行变化过程,系统可以利用他来控制和管理进程,他是系统感知进程存在的唯一标志;

    2、进程和线程的关系区别

      (1)、一个程序至少有一个进程,一个进程至少有一个线程(进程可以理解成线程的容器);

      (2)、0进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大的提高了程序的运行效率;

      (3)、线程在执行过程中与进程还是有区别。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能独立运行,必须依存在应用程序中,由应用程序提供多个线程执行控制;

      (4)、进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是cpu调度和分配的基本单位,它是比进程更小的

      能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源;

      一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行;

    3、线程的调用

    (1)、直接调用

    import  time,threading
    def hi(num):
        print("hello%s" %num)
        time.sleep(3)
    
    if __name__ == "__main__":
        ###开辟线程1,2
        t1 = threading.Thread(target=hi, args=(10,))
        t1.start()
        t2 = threading.Thread(target=hi,args=(9,))
        t2.start()
        print("程序运行结束")
    

     (2)、继承式调用

    import threading
    import time
    
    
    class MyThread(threading.Thread):
        def __init__(self,num):
            threading.Thread.__init__(self)
            self.num = num
    
        def run(self):#定义每个线程要运行的函数,函数名必须定义为run,重写run方法
            print("running on number:%s" %self.num)
    
            time.sleep(3)
    
    if __name__ == '__main__':
    
        t1 = MyThread(1)
        t2 = MyThread(2)
        t1.start()   ###子线程1
        t2.start()   ###子线程2
    
        print("ending......")  ##主线程的代码块
    

     4、threading.Thread实例的方法

    (1)、join方法

    在子线程完成运行之前,这个子线程的父线程将一直被阻塞。

    import  time,threading
    def hi(num):
        print("hello%s" %num,time.ctime())
        time.sleep(3)
        print("运行结束",time.ctime())
    if __name__ == "__main__":
        ###开辟子线程1,2
        t1 = threading.Thread(target=hi, args=(10,))
        t1.start()  ##子线程1
        t2 = threading.Thread(target=hi,args=(9,))
        t2.start() ###子线程2
        t1.join()
        t2.join()
        print("主程序运行结束")  ##主线程的代码块
    """
    运行结果:
    hello10 Wed Dec 25 11:13:26 2019
    hello9 Wed Dec 25 11:13:26 2019
    运行结束 Wed Dec 25 11:13:29 2019
    运行结束 Wed Dec 25 11:13:29 2019
    主程序运行结束
    """
    

     (2)、setDaemon方法

       将线程声明为守护进程,必须在start()方法之前设置,如果不设置为守护进程程序会被无限挂起。这个方法基本和join是相反的。当我们在程序运行中,执行一个主线程,

       如果主线程又创建了一个子线程,主线程和子线程就兵分两路,分别运行,那么当主进程完成想退出的时,会检验子线程是否完成。如果子线程未完成,则主线程会等待

    子线程完成后退出。但是有时候我们需要的是只要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这是就可以用setDaemon()方法了。

    import threading
    from time import ctime,sleep
    import time
    
    ###定义方法
    def ListenMusic(name):
    
            print ("Begin listening to %s. %s" %(name,ctime()))
            sleep(3)
            print("end listening %s"%ctime())
    def RecordBlog(title):
    
            print ("Begin recording the %s! %s" %(title,ctime()))
            sleep(5)
            print('end recording %s'%ctime())
    threads = []
    ###实例两个线程对象
    t1 = threading.Thread(target=ListenMusic,args=('水手',))
    t2 = threading.Thread(target=RecordBlog,args=('python线程',))
    
    threads.append(t1)
    threads.append(t2)
    
    if __name__ == '__main__':
        # t2.setDaemon(True)   ##3第二种情况:
        for t in threads:
            t.setDaemon(True)  ###第一种情况:
            t.start()  ###启动线程
            # t.join()
        # t1.join()
        print ("all over %s" %ctime())  ###主线程代码块
    """"
    第一种情况:是将子线程t1,t2都设置为守护进程,在主线程运行完成后直接就结束程序了
    Begin listening to 水手. Wed Dec 25 11:34:32 2019
    Begin recording the python线程! Wed Dec 25 11:34:32 2019
    all over Wed Dec 25 11:34:32 2019
    
    第二种情况执行结果:将子线程t2设置为守护进程
    Begin listening to 水手. Wed Dec 25 11:33:28 2019
    Begin recording the python线程! Wed Dec 25 11:33:28 2019
    all over Wed Dec 25 11:33:28 2019
    end listening Wed Dec 25 11:33:31 2019
    
        """
    

     (3)、其他方法

    # run():  线程被cpu调度后自动执行线程对象的run方法
    # start():启动线程活动。
    # isAlive(): 返回线程是否活动的。
    # getName(): 返回线程名。
    # setName(): 设置线程名。
    
    #threading模块提供的一些方法:
    # threading.currentThread(): 返回当前的线程变量。
    # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
    # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
    

     5、同步锁

    import threading,time
    num =100
    lock = threading.Lock()
    l =[]
    def sub():
        global num
        # num-=1
        """
        tem = num     这种情况下得到的结果是错误的,并不能得到正确的结果:
        time.sleep(0.001)  ##因为sleep等同于IO操作,线程切换的情况:IO操作的时候,时间轮询;
        num = tem-1     ###无法保证每个线程在切换的时候拿到的unm是最新的num,
        """
    
        lock.acquire()   ###将该代码块设置为串行运行,保证每个线程拿到的都是最新的num
        tem = num      ###多个线程都在同时操作同一个共享资源,所以造成了资源破坏,怎么办呢?
        time.sleep(0.1)
        num = tem-1
        lock.release()
    
    
    for i in range(100):
        t = threading.Thread(target=sub)
        t.start()
        l.append(t)
    for i in l:
        i.join()
    print(num)   ###运行结果为0
    

     多进程运行原理图

    6、死锁和递归锁

    在多个线程间共享多个资源的时候,如果两个线程分别占用一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都在使用,

    所有这两个线程在无外力作用下将一直等待下去。

    死锁:

    ####死锁
    import threading,time
    
    class myThread(threading.Thread):   ###通过继承方式实例化
        def doA(self):   ###定义了两个方法
            lockA.acquire()
            print(self.name,"gotlockA",time.ctime())
            time.sleep(3)
            lockB.acquire()
            print(self.name,"gotlockB",time.ctime())
            lockB.release()
            lockA.release()
    
        def doB(self):
            lockB.acquire()
            print(self.name,"gotlockB",time.ctime())
            time.sleep(2)
            lockA.acquire()
            print(self.name,"gotlockA",time.ctime())
            lockA.release()
            lockB.release()
     
        def run(self):   ###重写run方法
            self.doA()
            self.doB()
    if __name__=="__main__":
    
        lockA=threading.Lock()  
        lockB=threading.Lock()
        threads=[]
        for i in range(5):
            threads.append(myThread())  ###实例化并将实例添加到李彪
        for t in threads:
            t.start()   
        for t in threads:
            t.join()#
    

     使用递归锁避免死锁的情况

    为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter变量记录了acquire的次数,从而使得

    资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

     

    ####死锁
    import threading,time
    
    class myThread(threading.Thread):
        def doA(self):
            r_lock.acquire()   ###
            print(self.name,"gotr_lock",time.ctime())
            time.sleep(3)
            r_lock.acquire()
            print(self.name,"gotr_lock",time.ctime())
            r_lock.release()
            r_lock.release()
    
        def doB(self):
            r_lock.acquire()
            print(self.name,"gotr_lock",time.ctime())
            time.sleep(2)
            r_lock.acquire()
            print(self.name,"gotr_lock",time.ctime())
            r_lock.release()
            r_lock.release()
    
        def run(self):
            self.doA()
            self.doB()
    if __name__=="__main__":
        # 
        # r_lock=threading.Lock()
        # r_lock=threading.Lock()
        r_lock = threading.RLock()
        threads=[]
        for i in range(5):
            threads.append(myThread())
        for t in threads:
            t.start()
        for t in threads:
            t.join()#等待线程结束,后面再讲。
    

    7、同步条件:event

     

    import threading,time
    def boss():
        print("BOSS:今晚大家都要加班到22:00。")
        print(event.isSet())
        event.set()
        time.sleep(5)
        print("BOSS:<22:00>可以下班了。")
        print(event.isSet())
        event.set()
    def worker():
        event.wait()
        print("Worker:哎……命苦啊!")
        time.sleep(1)
        event.clear()
        event.wait()
        print("Worker:OhYeah!")
    if __name__ == "__main__":
        event = threading.Event()
        boss1= threading.Thread(target=boss)
        boss1.start()
        worker_list =[]
        for i in range(5):
            worker_obj= threading.Thread(target=worker)
            worker_obj.start()
            worker_list.append(worker_obj)
        for i in worker_list:
            i.join()
        boss1.join()
    

     8、信号量semaphore

    信号量用来控制线程并发数的,BounderSemaphore或Semaphore管理一个内置的计数器,每当调用acquire()时-1,调用release()时+1。计数器不能小于0,当计数器为0的时候,acquire()将

    阻塞线程至同步锁定状态,直到其他线程调用release()。BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数器的值是否超过了计数器的初始值,如果超过了将抛出异常。

    import threading,time
    class myThread(threading.Thread):
        def run(self):
            if semaphore.acquire():
                print(self.name)
                time.sleep(5)
                semaphore.release()
    if __name__=="__main__":
        semaphore=threading.Semaphore(5)
        thrs=[]
        for i in range(100):
            thrs.append(myThread())
        for t in thrs:
            t.start()
    

     9、队列queue

      (1)、队列常用格式

    import queue,time
    #先进后出
    
    q=queue.LifoQueue()  ###后进先出
    
    q.put(34)
    q.put(56)
    q.put(12)
    
    #优先级
    # q=queue.PriorityQueue()
    # q.put([5,100])
    # q.put([7,200])
    # q.put([3,"hello"])
    # q.put([4,{"name":"alex"}])
    
    while 1:
    
      data=q.get()
      print(data)
      if q.empty():
          time.sleep(5)
          if q.empty():
              break
    

     (2)、queue常用方法

    # 创建一个“队列”对象
    # import Queue
    # q = Queue.Queue(maxsize = 10)
    # Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。
    # 
    # 将一个值放入队列中
    # q.put(10)
    # 调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为
    # 1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0,put方法将引发Full异常。
    # 
    # 将一个值从队列中取出
    # q.get()
    # 调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True,
    # get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。
    # 
    # Python Queue模块有三种队列及构造函数:
    # 1、Python Queue模块的FIFO队列先进先出。   class queue.Queue(maxsize)
    # 2、LIFO类似于堆,即先进后出。               class queue.LifoQueue(maxsize)
    # 3、还有一种是优先级队列级别越低越先出来。        class queue.PriorityQueue(maxsize)
    # 
    # 此包中的常用方法(q = Queue.Queue()):
    # q.qsize() 返回队列的大小
    # q.empty() 如果队列为空,返回True,反之False
    # q.full() 如果队列满了,返回True,反之False
    # q.full 与 maxsize 大小对应
    # q.get([block[, timeout]]) 获取队列,timeout等待时间
    # q.get_nowait() 相当q.get(False)
    # 非阻塞 q.put(item) 写入队列,timeout等待时间
    # q.put_nowait(item) 相当q.put(item, False)
    # q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号
    # q.join() 实际上意味着等到队列为空,再执行别的操作
    

     (3)、使用队列完成生产者消费者模型

      

    import time,random
    import queue,threading
    ####
    q = queue.Queue()
    
    def Producer(name):
      count = 0
      while count <10:
        print("making........")
        time.sleep(random.randrange(3))
        q.put(count)
        print('Producer %s has produced %s baozi..' %(name, count))
        count +=1
        #q.task_done()
        #q.join()
        print("ok......")
    def Consumer(name):
      count = 0
      while count <10:
        time.sleep(random.randrange(4))
        if not q.empty():
            data = q.get()
            #q.task_done()
            #q.join()
            print(data)
            print('33[32;1mConsumer %s has eat %s baozi...33[0m' %(name, data))
        else:
            print("-----no baozi anymore----")
        count +=1
    
    p1 = threading.Thread(target=Producer, args=('A',))
    c1 = threading.Thread(target=Consumer, args=('B',))
    # c2 = threading.Thread(target=Consumer, args=('C',))
    # c3 = threading.Thread(target=Consumer, args=('D',))
    p1.start()
    c1.start()
    # c2.start()
    # c3.start()
    

     备注:

      a、并发、并行

      并发:指系统具有处理多个任务(动作)的能力;

      并行:指系统具有同时处理多个任务(动作)0的能力;

      并行是并发的一个子集;

      b、同步、异步

      同步:当进程执行到一个IO(等待外部数据)操作的时候,----等就是同步;

      异步:当进程执行到一个IO(等待外部数据)操作的时候,----不等,等到数据接收成功,再进行处理,就是异步;

      c、任务:IO密集型、计算密集型

      sleep就相当于IO操作。

      对于IO密集型任务:python的多线程就有意义;或者多线程+协程;

      对于计算密集型任务:python的多线程就不推荐了,python就不适用了;

      d、GIL全局解释锁

      无论你有多少个线程,有多少个cpu,python在执行的时候就会淡定的在同一时刻只允许一个线程在运行;因为有GIL的存在,

      所以在同一时刻,只有一个线程在同一时刻在被执行;

      下图为程序执行过程的原理图-简单版

      上图中的下方方框为进程,进程有三个线程:1个主线程,2个子线程。两个子线程在一个cpu中交叉执行,由于IO操作或者时间轮询进行交叉执行;

    二、多进程

      由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分使用多核CPU的资源,在python中大部分情况需要使用多进程。

       multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.process对象来创建一个进程。该进程可以运行在

      python程序内部编写的函数。该process对象与Thread对象的用法相同,也有start(),run(),join()方法,此外multiprossing包中也有Lock/Event/semaphore/Condition类

      (这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。所以multiprocessing的很大一部分与threading共用

      一套API。

     1、调用方式

      a、第一种方式:

    from multiprocessing import Process
    import time
    p_list =[]
    def f(name):
        time.sleep(1)
        print("My name is %s" %name,time.ctime())
    
    if __name__ =="__main__":
        p_list =[]
        name_list = ["heaton","potti","赵四"]
        for i in range(len(name_list)):
            p = Process(target=f,args=(name_list[i],))
            p_list.append(p)
            p.start()
        for i in p_list:
            i.join()
        print("end")
    

     b、继承式调用

    from multiprocessing import Process
    import time
    import  os
    
    class MyProcess(Process):
        def __init__(self):
            super(MyProcess, self).__init__()
            print(self.__dict__)
            #self.name = name  就是进程的属性字典
        def run(self):
            time.sleep(1)
            print ('hello', self.name,time.ctime(),"=======>>父进程:",os.getppid(),"=======>>子进程:",os.getpid())
    if __name__ == '__main__':
        p_list=[]
        for i in range(3):
            p = MyProcess()
            p.start()
            p_list.append(p)
        for p in p_list:
            p.join()
        print('end')
    

     2、Process类的方法
     构造方法:

      

    #     Process([group [, target [, name [, args [, kwargs]]]]])
    #   group: 线程组,目前还没有实现,库引用中提示必须是None; 
    #   target: 要执行的方法; 
    #   name: 进程名; 
    #   args/kwargs: 要传入方法的参数。
    

      

     实例方法:

     """
        is_alive():返回进程是否在运行;
        join([timeout]):阻塞当前上下文环境的进程,知道调用此方法的进程终止或到达指定的timeout
        start():进程准备就绪,等待cpu调度
        run():start()调用run方法,如果实例进程时未指定传入target,start默认执行run()方法;
        terminate():不管任务是否完成,立即停止功能进程;
        """
    

     属性

    """
    daemon:设置进程为守护进程;
    name:进程名字
    pid:进程号
    """
    

    3、进程间的通讯

    (1)、进程队列queue

    from multiprocessing import Process,Queue
    import queue
    def f(q,n):
        name_list=["heaton","potti","赵四"]
        q.put(name_list[n])
        print("=======>>子进程",id(q))
    
    if __name__ =="__main__":
        q = Queue()
        print("====>>主进程id",id(q))
        p_lsit  = []
        for i in  range(3):
            p = Process(target=f,args=(q,i))
            p.start()
            p_lsit.append(p)
        for i in p_lsit:
            i.join()   
            print(q.get())
            print(q.get())
            print(q.get())
            """
            ====>>主进程id 2630197274032
            =======>>子进程 2955385109088
            heaton
            =======>>子进程 2237267311200
            potti
            =======>>子进程 1403175696992
            赵四
            """
    

     (2)、Pipe管道

    from multiprocessing import Process, Pipe
    
    def f(conn):
        conn.send([12, {"name":"yuan"}, 'hello'])
        response=conn.recv()
        print("response",response)
        conn.close()
        print("q_ID2:",id(conn))
    
    if __name__ == '__main__':
    
        parent_conn, child_conn = Pipe()
        print("q_ID1:",id(parent_conn))
        p = Process(target=f, args=(child_conn,))
        p.start()
        print(parent_conn.recv())   # prints "[42, None, 'hello']"
        parent_conn.send("儿子你好!")
        p.join()
    

     (3)、managers

      Queue和Pipe只是实现了数据交互,并没实现数据共享,即一个进程去更改另一个进程的数据;

    from multiprocessing import Process, Manager
    
    def f(d, l,n):
        d[n] = '1'
        d['2'] = 2
        d[0.25] = None
        l.append(n)
        #print(l)
    
        print("son process:",id(d),id(l))
    
    if __name__ == '__main__':
    
        with Manager() as manager:
    
            d = manager.dict()
            print(d)
            print(manager.__dict__)
    
            l = manager.list(range(5))
            print(l)
    
            print("main process:",id(d),id(l))
    
            p_list = []
    
            for i in range(10):
                p = Process(target=f, args=(d,l,i))
                p.start()
                p_list.append(p)
    
            for res in p_list:
                res.join()
    
            print(d)
            print(l)
    

     4、进程同步

    from multiprocessing import Process, Lock
    
    def f(l, i):
    
        with l:
            print('hello world %s'%i)
    
    if __name__ == '__main__':
        lock = Lock()
    
        for num in range(10):
            Process(target=f, args=(lock, num)).start()
    

     5、进程池 

      进程池内部维护着一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池列中没有可供使用的进程,那么程序就会等待,知道进程池中有可用的进程为止;

    from multiprocessing import  Process,Pool
    import time,os
    
    def Foo(i):
        time.sleep(1)
        print(i)
        return i+100
    
    def Bar(arg):
        print("=======>>pid:",os.getpid())
        print("=======>>ppid:",os.getppid())
        print("======>>>",arg)
    
    if __name__ == "__main__":
    
        pool = Pool(5)
        Bar(1)
        print("=================================>>>>>")
    
        for i in range(10):
            # res = pool.apply(func=Foo,args=(i,))
            # res = pool.apply_async(func=Foo,args=(i,))
            res = pool.apply_async(func=Foo,args=(i,),callback=Bar) #callback回调函数,回调函数的参数就是子进程执行函数的return返回值;
    
            print("程序运行结果====>>:",res)
    
        pool.close()
        pool.join()
        print("======>>进程结束!")
    

     三、协程

      协程,又叫微线程,纤程。

      协程有点:

      a、协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势越明显。

      b、不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多,

      因为协程是一个线程执行,那怎么利用多核cpu呢?最简单的方法就是:多进程+协程,即充分利用多核,也充分发挥了协程的高效率,可获得极高的性能。

     1、通过yeild简单实现

      

    import time,queue
    def consumer(name):
        print("======>>ready to eat baozi======>>")
        while True:
            new_baozi = yield   ###
            print("%s is eating baozi %s" %(name,new_baozi))
    def producer():
        r = con.__next__()
        r2 = con2.__next__()
        n = 0
        while n<=10:
            time.sleep(3)
            print("======>>making baozi %s and %s " %(n,n+1))
            con.send(n)
            con2.send(n+1)
            n+=2
    if __name__ =="__main__":
        con  = consumer("c1")
        con2 = consumer("c2")
        p = producer()
    

     2、greenlet模块

    greenlet是一个用C实现的协程模块,相比与python自带的yeild,他可以使你在任意函数之间随意切换,而不需要把这个函数先声明为ggenerator

    from greenlet import greenlet
     
     
    def test1():
        print(12)
        gr2.switch()
        print(34)
        gr2.switch()
     
     
    def test2():
        print(56)
        gr1.switch()
        print(78)
     
     
    gr1 = greenlet(test1)
    gr2 = greenlet(test2)
    gr1.switch()
    

     3、gevent

    import gevent
    
    import requests,time
    
    
    start=time.time()
    
    def f(url):
        print('GET: %s' % url)
        resp =requests.get(url)
        data = resp.text
        print('%d bytes received from %s.' % (len(data), url))
    
    gevent.joinall([
    
            gevent.spawn(f, 'https://www.python.org/'),
            gevent.spawn(f, 'https://www.yahoo.com/'),
            gevent.spawn(f, 'https://www.baidu.com/'),
            gevent.spawn(f, 'https://www.sina.com.cn/'),
    
    ])
    
    # f('https://www.python.org/')
    #
    # f('https://www.yahoo.com/')
    #
    # f('https://baidu.com/')
    #
    # f('https://www.sina.com.cn/')
    
    print("cost time:",time.time()-start)
    

      

  • 相关阅读:
    人在年轻的时候,最需要的能力--吃药的能力
    查分单词-Python
    关于NLP算法工程师的几点思考
    找出只出现一次的数字-Python
    最长连续序列
    二叉树中的最大路径和-Python
    windows:查找端口所对应的进程
    vue项目路由模式为history时打包后部署在nginx 配置访问
    用navicat连接数据库报错:1130-host ... is not allowed to connect to this MySql server如何处理
    mysql误删root用户
  • 原文地址:https://www.cnblogs.com/tengjiang/p/12095835.html
Copyright © 2020-2023  润新知