线程、进程和协程是什么

线程、进程和协程的详细概念解释和原理剖析不是本文的重点,本文重点讲述在Python中怎样实际使用这三种东西

参考: 进程、线程、协程之概念理解

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。
协程:一个程序可以包含多个协程,可以对比于一个进程包含多个线程,因而下面我们来比较协程和线程:我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。

准备工作

磨刀不误砍柴工,在用实例讲解线程、进程和协程怎么使用之前,先准备一些工具:

实际上多线程、多进程和协程,都属于并发编程,并发编程的最重要的目标就是提高程序运行的效率,那么我们需要一个计算一个函数耗时长度的工具,用于对比不同方式程序的运行时间,这里我们写一个函数计时装饰器fn_timer来完成这件事:

def fn_timer(function):
    '''
    函数计时装饰器
    :param function: 函数对象
    :return: 装饰器
    '''
    @wraps(function)
    def function_timer(*args,**kwargs):
        # 起始时间
        t0 = time.time()
        # 调用函数
        result = function(*args,**kwargs)
        # 结束时间
        t1 = time.time()
        # 打印函数耗时
        print '[finished function:{func_name} in {time:.2f}s]'.format(func_name = function.__name__,time = t1 - t0)
        return result
    return function_timer

该装饰器的用法示例:

# 测试
@fn_timer
def add(x,y):
    time.sleep(1.22)
    return x + y

if __name__ == '__main__':
    # 测试
    sum = add(1,2)
    print sum

运行代码输出:

[finished function:test in 1.23s]
3

实际使用中,大规模爬虫程序很适合用并发来实现,所以我们再准备一些网址放在urls列表中,用于测试爬虫程序的效率(都是百度百科的一些词条页面):

# 20个网页
urls = ['https://baike.baidu.com/item/%E8%87%AA%E7%94%B1%E8%BD%AF%E4%BB%B6',
        'https://baike.baidu.com/item/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%E8%AF%AD%E8%A8%80',
        'https://baike.baidu.com/item/%E5%9F%BA%E9%87%91%E4%BC%9A',
        'https://baike.baidu.com/item/%E5%88%9B%E6%96%B02.0',
        'https://baike.baidu.com/item/%E5%95%86%E4%B8%9A%E8%BD%AF%E4%BB%B6',
        'https://baike.baidu.com/item/%E5%BC%80%E6%94%BE%E6%BA%90%E4%BB%A3%E7%A0%81',
        'https://baike.baidu.com/item/OpenBSD',
        'https://baike.baidu.com/item/%E8%A7%A3%E9%87%8A%E5%99%A8',
        'https://baike.baidu.com/item/%E7%A8%8B%E5%BA%8F/71525',
        'https://baike.baidu.com/item/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80',
        'https://baike.baidu.com/item/C%2B%2B',
        'https://baike.baidu.com/item/%E8%B7%A8%E5%B9%B3%E5%8F%B0',
        'https://baike.baidu.com/item/Web/150564',
        'https://baike.baidu.com/item/%E7%88%B1%E5%A5%BD%E8%80%85',
        'https://baike.baidu.com/item/%E6%95%99%E5%AD%A6',
        'https://baike.baidu.com/item/Unix%20shell',
        'https://baike.baidu.com/item/TIOBE',
        'https://baike.baidu.com/item/%E8%AF%BE%E7%A8%8B',
        'https://baike.baidu.com/item/MATLAB',
        'https://baike.baidu.com/item/Perl']

整合:把函数计时装饰器和urls列表封装在一个类:utils.py中:

# coding:utf-8
from functools import wraps
import time

# 20个网页
urls = ['https://baike.baidu.com/item/%E8%87%AA%E7%94%B1%E8%BD%AF%E4%BB%B6',
        'https://baike.baidu.com/item/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%E8%AF%AD%E8%A8%80',
        'https://baike.baidu.com/item/%E5%9F%BA%E9%87%91%E4%BC%9A',
        'https://baike.baidu.com/item/%E5%88%9B%E6%96%B02.0',
        'https://baike.baidu.com/item/%E5%95%86%E4%B8%9A%E8%BD%AF%E4%BB%B6',
        'https://baike.baidu.com/item/%E5%BC%80%E6%94%BE%E6%BA%90%E4%BB%A3%E7%A0%81',
        'https://baike.baidu.com/item/OpenBSD',
        'https://baike.baidu.com/item/%E8%A7%A3%E9%87%8A%E5%99%A8',
        'https://baike.baidu.com/item/%E7%A8%8B%E5%BA%8F/71525',
        'https://baike.baidu.com/item/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80',
        'https://baike.baidu.com/item/C%2B%2B',
        'https://baike.baidu.com/item/%E8%B7%A8%E5%B9%B3%E5%8F%B0',
        'https://baike.baidu.com/item/Web/150564',
        'https://baike.baidu.com/item/%E7%88%B1%E5%A5%BD%E8%80%85',
        'https://baike.baidu.com/item/%E6%95%99%E5%AD%A6',
        'https://baike.baidu.com/item/Unix%20shell',
        'https://baike.baidu.com/item/TIOBE',
        'https://baike.baidu.com/item/%E8%AF%BE%E7%A8%8B',
        'https://baike.baidu.com/item/MATLAB',
        'https://baike.baidu.com/item/Perl']

def fn_timer(function):
    '''
    函数计时装饰器
    :param function: 函数对象
    :return: 装饰器
    '''
    @wraps(function)
    def function_timer(*args,**kwargs):
        # 起始时间
        t0 = time.time()
        # 调用函数
        result = function(*args,**kwargs)
        # 结束时间
        t1 = time.time()
        # 打印函数耗时
        print '[finished function:{func_name} in {time:.2f}s]'.format(func_name = function.__name__,time = t1 - t0)
        return result
    return function_timer

# 测试
@fn_timer
def add(x,y):
    time.sleep(1.22)
    return x + y

if __name__ == '__main__':
    # 测试
    sum = add(1,2)
    print sum
    # 输出:
    '''
    [finished function:test in 1.23s]
    3
    '''

实例讲解多线程的用法

从听音乐、看电影讲起

我现在要做两件事情:听音乐、看电影,听一首音乐假如耗时1秒,看一部电影假如耗时5秒,用两个函数定义这两个任务如下:

# 耗时任务:听音乐
def music(name):
    print 'I am listening to music {0}'.format(name)
    time.sleep(1)

# 耗时任务:看电影
def movie(name):
    print 'I am watching movie {0}'.format(name)
    time.sleep(5)

假如我现在要听10首音乐、看2部电影,那么我就有如下几种方案:

方案一:先一个个听完10首音乐,再一个个看完2部电影,顺序完成,代码如下:

# 单线程操作:顺序执行听10首音乐,看2部电影
@fn_timer
def single_thread():
    for i in range(10):
        music(i)
    for i in range(2):
        movie(i)

让我们执行一下这段代码,输出如下:

    I am listening to music 0
    I am listening to music 1
    I am listening to music 2
    I am listening to music 3
    I am listening to music 4
    I am listening to music 5
    I am listening to music 6
    I am listening to music 7
    I am listening to music 8
    I am listening to music 9
    I am watching movie 0
    I am watching movie 1
    [finished function:single_thread in 20.14s]

可以看到,老老实实严格按照先后顺序来一件件做这些事情,所需的总时间和每件事情耗时加起来是一样多的。

方案二:刚刚的方案不太好,太费时间了,那么能不能同时进行一些事情呢?答案是可以的,可以同时听多首音乐,同时看多部电影进行,代码如下:

# 多线程执行:听10首音乐,看2部电影
@fn_timer
def multi_thread():
    # 线程列表
    threads = []
    for i in range(10):
        # 创建一个线程,target参数为任务处理函数,args为任务处理函数所需的参数元组
        threads.append(threading.Thread(target = music,args = (i,)))
    for i in range(2):
        threads.append(threading.Thread(target = movie,args = (i,)))

    for t in threads:
        # 设为守护线程
        t.setDaemon(True)
        # 开始线程
        t.start()
    for t in threads:
        t.join()

执行上述代码,运行结果:

    I am listening to music 0
    I am listening to music 1
    I am listening to music 2
    I am listening to music 3
    I am listening to music 4
    I am listening to music 5
    I am listening to music 6
    I am listening to music 7
    I am listening to music 8
    I am listening to music 9
    I am watching movie 0
    I am watching movie 1
    [finished function:multi_thread in 5.02s]

这次只用了5秒就完成了,完成效率显著提升。这次试用多线程执行多个任务,所有任务最终的总耗时 = 耗时最长的那个单个任务的耗时,即看一部电影的5秒钟时间。

方案三:使用线程池。上面使用多线程的方式比较繁琐,下面使用线程池来实现:

# 使用线程池执行:听10首音乐,看2部电影
@fn_timer
def use_pool():
    # 设置线程池大小为20,如果不设置,默认值是CPU核心数
    pool = Pool(20)
    pool.map(movie,range(2))
    pool.map(music,range(10))
    pool.close()
    pool.join()

执行结果:

    I am listening to music 0
    I am listening to music 1
    I am listening to music 2
    I am listening to music 3
    I am listening to music 4
    I am listening to music 5
    I am listening to music 6
    I am listening to music 7
    I am listening to music 8
    I am listening to music 9
    I am watching movie 0
    I am watching movie 1
    [finished function:use_pool in 6.12s]

可以看出使用线程池反而比手工调度线程多耗时一秒钟,可能是因为线程池内部对线程的调度和线程切换的耗时造成的。

实例:使用多线程下载网页

话不多说,直接上代码,用多线程并发下载20个百度百科网页的实例代码及运行结果如下:

# coding:utf-8
# 测试多线程
import threading
import time
from utils import fn_timer
from multiprocessing.dummy import Pool
import requests
from utils import urls

# 应用:使用单线程下载多个网页的内容
@fn_timer
def download_using_single_thread(urls):
    resps = []
    for url in urls:
        resp = requests.get(url)
        resps.append(resp)
    return resps

# 应用:使用多线程下载多个网页的内容
@fn_timer
def download_using_multi_thread(urls):
    threads = []
    for url in urls:
        threads.append(threading.Thread(target = requests.get,args = (url,)))
    for t in threads:
        t.setDaemon(True)
        t.start()
    for t in threads:
        t.join()

# 应用:使用线程池下载多个网页的内容
@fn_timer
def download_using_pool(urls):
    pool = Pool(20)
    # 第一个参数为函数名,第二个参数一个可迭代对象,为函数所需的参数列表
    resps = pool.map(requests.get,urls)
    pool.close()
    pool.join()
    return resps

def main():
    # 1.使用单线程
    resps = download_using_single_thread(urls)
    print len(resps)
    # 输出:
    '''
    [finished function:download_using_single_thread in 6.18s]
    20
    '''
    # 2. 使用多线程
    download_using_multi_thread(urls)
    # 输出:
    '''
    [finished function:download_using_multi_thread in 0.73s]
    '''

    # 3.使用线程池
    resps = download_using_pool(urls)
    print len(resps)
    # 输出:
    '''
    [finished function:download_using_pool in 0.84s]
    20
    '''

if __name__ == '__main__':
    main()

实例讲解多进程的用法

多进程和进程池的使用

实例代码如下:

# coding:utf-8
# 测试多进程
import os
import time
from multiprocessing import Process,Pool,Queue
from utils import fn_timer
import random

# 简单的任务
@fn_timer
def do_simple_task(task_name):
    print 'Run child process {0}, task name is: {1}'.format(os.getpid(),task_name)
    time.sleep(1.2)
    return task_name

@fn_timer
# 1. 测试简单的多进程
def test_simple_multi_process():
    p1 = Process(target=do_simple_task, args=('task1',))
    p2 = Process(target=do_simple_task, args=('task2',))
    print 'Process will start...'
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    print 'Process end.'

@fn_timer
# 2. 测试使用进程池
def test_use_process_pool():
    # 创建一个进程池,数字表示一次性同时执行的最大子进程数
    pool = Pool(5)
    # 任务返回值列表
    results = []
    # 任务名称列表
    task_names = []
    for i in range(7):
        task_names.append('task{0}'.format(i))
    # 并发执行多个任务,并获取任务返回值
    results = pool.map_async(do_simple_task,task_names)
    print 'Many processes will start...'
    pool.close()
    pool.join()

    print 'All processes end, results is: {0}'.format(results.get())

def main():
    test_simple_multi_process()
    # 输出:
    '''
    Process will start...
    Run child process 1524, task name is: task2
    Run child process 1728, task name is: task1
    [finished function:do_simple_task in 1.20s]
    [finished function:do_simple_task in 1.20s]
    Process end.
    [finished function:test_simple_multi_process in 1.34s]
    '''

    test_use_process_pool()
    # 输出:
    '''
    Many processes will start...
    Run child process 7568, task name is: task0
    Run child process 7644, task name is: task1
    Run child process 7628, task name is: task2
    Run child process 7620, task name is: task3
    Run child process 7660, task name is: task4
    [finished function:do_simple_task in 1.20s]
    Run child process 7568, task name is: task5
    [finished function:do_simple_task in 1.20s]
    Run child process 7644, task name is: task6
    [finished function:do_simple_task in 1.20s]
    [finished function:do_simple_task in 1.20s]
    [finished function:do_simple_task in 1.20s]
    [finished function:do_simple_task in 1.20s]
    [finished function:do_simple_task in 1.20s]
    All processes end, results is: ['task0', 'task1', 'task2', 'task3', 'task4', 'task5', 'task6']
    [finished function:test_use_process_pool in 2.62s]
    '''
if __name__ == '__main__':
    main()

进程之间的通信

进程间的通信采用队列来实现,实例代码如下:

# coding:utf-8
# 测试进程间的通信
import os
import time
from multiprocessing import Process,Pool,Queue
from utils import fn_timer
import random

# 写进程执行的任务
def write(q):
    for value in ['A','B','C']:
        print 'Put value: {0} to queue.'.format(value)
        q.put(value)
        time.sleep(random.random())

# 读进程执行的任务
def read(q):
    while True:
        value = q.get(True)
        print 'Get value: {0} from queue.'.format(value)

# 测试进程间的通信
def test_communication_between_process():
    q = Queue()
    # 写进程
    pw = Process(target = write,args = (q,))
    # 读进程
    pr = Process(target = read,args = (q,))
    pw.start()
    pr.start()
    pw.join()
    # 因为读任务是死循环,所以要强行结束
    pr.terminate()

def main():
    test_communication_between_process()
    # 输出
    '''
    Put value: A to queue.
    Get value: A from queue.
    Put value: B to queue.
    Get value: B from queue.
    Put value: C to queue.
    Get value: C from queue.
    '''

if __name__ == '__main__':
    main()

实例讲解协程的用法

下面用协程下载同样的20个网页,实例代码如下:

# coding:utf-8
# 测试协程
import requests
import gevent
import utils
from utils import fn_timer
from gevent.pool import Pool
from gevent import monkey
# 打动态补丁,把标准库中的thread/socket等替换掉,让它们变成非阻塞的
monkey.patch_all()

session = requests.Session()
session.headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36'

@fn_timer
def download_using_single_thread(urls):
    '''
    顺序执行下载多个网页
    :param urls: 要下载的网页内容
    :return: 响应列表
    '''
    resps = []
    for url in urls:
        resps.append(session.get(url))
    return resps

@fn_timer
def download_using_coroutine(urls):
    '''
    使用协程下载
    :param urls: 要下载的网页内容
    :return: 响应列表
    '''
    spawns = []
    for url in urls:
        spawns.append(gevent.spawn(session.get,url))
    # 在遇到IO操作时,gevent会自动切换,并发执行(异步IO)
    rets = gevent.joinall(spawns)
    # joinall函数会返回gevent.greenlet.Greenlet对象的列表,如果想要获得每次调用session.get函数的返回结果,还需分别调用每个Greenlet对象的get函数
    results = [ret.get() for ret in rets]

@fn_timer
def download_using_coroutine_pool(urls):
    # 创建协程池,并设置最大并发量
    pool = Pool(20)
    # pool.map函数直接返回每次调用session.get函数返回的结果列表
    rets = pool.map(session.get,urls)

def main():
    # 1.使用单线程下载20个网页
    download_using_single_thread(utils.urls)
    # 输出:
    '''
    [finished function:download_using_single_thread in 1.83s]
    '''

    # 2.使用协程下载20个网页
    download_using_coroutine(utils.urls)
    # 输出:
    '''
    [finished function:download_using_coroutine in 0.69s]
    '''

    # 3.使用协程池下载20个网页
    download_using_coroutine_pool(utils.urls)
    # 输出:
    '''
    [finished function:download_using_coroutine_pool in 0.78s]
    '''

if __name__ == '__main__':
    main()

可以发现,协程的效率也是非常高的。

多线程、多进程和协程并发效率的对比

下面分别使用线程池、进程池和协程池下载100个相同的网页,来对比其效率:

# coding:utf-8
# 对比多线程、多进程和协程下载网页
import requests
import utils
from utils import fn_timer
from multiprocessing.dummy import Pool as thread_pool
from multiprocessing import Pool as process_pool
from gevent.pool import Pool as gevent_pool
from gevent import monkey
# 打动态补丁,把标准库中的thread/socket等替换掉,让它们变成非阻塞的
monkey.patch_all()

session = requests.Session()
session.headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36'

# 1. 使用线程池下载多个网页的内容
@fn_timer
def download_using_thread_pool(urls):
    pool = thread_pool(100)
    # 第一个参数为函数名,第二个参数一个可迭代对象,为函数所需的参数列表
    resps = pool.map(session.get,urls)
    pool.close()
    pool.join()
    return resps

# 2. 测试使用进程池
@fn_timer
def download_using_process_pool(urls):
    # 创建一个进程池,数字表示一次性同时执行的最大子进程数
    pool = process_pool(100)
    # 任务返回值列表
    results = []
    # 并发执行多个任务,并获取任务返回值
    results = pool.map_async(session.get,urls)
    pool.close()
    pool.join()
    return results.get()

# 3. 使用协程池下载
@fn_timer
def download_using_coroutine_pool(urls):
    # 创建协程池,并设置最大并发量
    pool = gevent_pool(100)
    pool.map(session.get,urls)

def main():
    # 1. 使用线程池下载100个网页
    download_using_thread_pool(utils.urls * 5)
    # 输出:
    '''
    [finished function:download_using_thread_pool in 3.68s]
    '''

    # 2. 使用进程池下载100个网页
    download_using_process_pool(utils.urls * 5)
    # 输出:
    '''
    卡死了
    '''

    # 3.使用协程池下载20个网页
    download_using_coroutine_pool(utils.urls * 5)
    # 输出:
    '''
    [finished function:download_using_coroutine_pool in 3.46s]
    '''

if __name__ == '__main__':
    main()

从结果来看,使用协程池的效率还是略高一点。

作者:m2fox
链接:

https://www.jianshu.com/p/857e0780946b


识别图中二维码,领取python全套视频资料

Python多线程、多进程和协程的实例讲解的更多相关文章

  1. 多线程、多进程、协程、IO多路复用请求百度

    最近学习了多线程.多进程.协程以及IO多路复用,那么对于爬取数据来说,这几个方式哪个最快呢,今天就来稍微测试一下 普通方式请求百度5次 import socket import time import ...

  2. 物无定味适口者珍,Python3并发场景(CPU密集/IO密集)任务的并发方式的场景抉择(多线程threading/多进程multiprocessing/协程asyncio)

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_221 一般情况下,大家对Python原生的并发/并行工作方式:进程.线程和协程的关系与区别都能讲清楚.甚至具体的对象名称.内置方法 ...

  3. 进击的Python【第十章】:Python的socket高级应用(多进程,协程与异步)

    Python的socket高级应用(多进程,协程与异步)

  4. web服务-2、四种方法实现并发服务器-多线程,多进程,协程,(单进程-单线程-非堵塞)

    知识点:1.使用多线程,多进程,协程完成web并发服务器 2.单进程-单线程-非堵塞也可以实现并发服务器 1.多进程和协程的代码在下面注释掉的部分,我把三种写在一起了 import socket im ...

  5. Python全栈开发:协程代码实例

    协程代码1 #!/usr/bin/env python # -*- coding;utf-8 -*- # 导入协程模块 """ 协程工作原理 ""&q ...

  6. python进阶——进程/线程/协程

    1 python线程 python中Threading模块用于提供线程相关的操作,线程是应用程序中执行的最小单元. #!/usr/bin/env python # -*- coding:utf-8 - ...

  7. Python多线程多进程那些事儿看这篇就够了~~

    自己以前也写过多线程,发现都是零零碎碎,这篇写写详细点,填一下GIL和Python多线程多进程的坑~ 总结下GIL的坑和python多线程多进程分别应用场景(IO密集.计算密集)以及具体实现的代码模块 ...

  8. Python异步IO之协程(一):从yield from到async的使用

    引言:协程(coroutine)是Python中一直较为难理解的知识,但其在多任务协作中体现的效率又极为的突出.众所周知,Python中执行多任务还可以通过多进程或一个进程中的多线程来执行,但两者之中 ...

  9. windows下多进程加协程并发模式

    好久没更新博客了.正好最近要整理一下最近这段时间做过的项目以及学习python的一些心得.如标题所示,今天就来说说windows下多进程加协程并发模式.其实网上还是蛮多在linux下的多进程加协程并发 ...

随机推荐

  1. 用swift开发仪表盘控件(二)

    二.代码分析 这个控件本质就是从UIView继承的一个类而已.所以整个代码事实上就是一个定制的UIView类. 依据UIView的规则进行例如以下初始化: required init(coder aD ...

  2. C语言 · Sine之舞

    基础练习 Sine之舞   时间限制:1.0s   内存限制:512.0MB    问题描述 最近FJ为他的奶牛们开设了数学分析课,FJ知道若要学好这门课,必须有一个好的三角函数基本功.所以他准备和奶 ...

  3. ajax——实现三级联动下拉列表

    数据库: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3 ...

  4. 亿级Web系统的容错性建设实践(转)

    三年多前,我在腾讯负责的活动运营系统,因为业务流量规模的数倍增长,系统出现了各种各样的异常,那个时候,我7*24小时地没日没夜处理告警,周末和凌晨也经常上线,疲于奔命.后来,当时的老领导对我说:你不能 ...

  5. 禁止复制 + 锁右键 + 禁止全选(兼容IE Chrome等)

    function iEsc() { return false; }function iRec() { return true; }function DisableKeys() {    if (eve ...

  6. elasticsearch安装与使用(1)-- centos7 elasticsearch的两种简单安装方法

    转自:http://www.cnblogs.com/miao-zp/p/6003160.html 简单修改 前言 elasticsearch(下面称为ES)是一个基于Lucene的搜索服务器(By 百 ...

  7. 高精度 - SGU 112 a^b-b^a

    a^b-b^a Problem's Link Mean: 略 analyse: 简单题,只用编个高精度乘法和减法即可. Time complexity: O(N) view code  java im ...

  8. java.lang下面有一个接口:Comparable(可比较的)

    对于自定义对象,Sort不知道规则,所以无法比较.这种情况下一定要定义排序规则.方式有两种: java.lang下面有一个接口:Comparable(可比较的) 可以让自定义对象实现一个接口,这个接口 ...

  9. 验证:record项元的多少影响修改速度。

    验证erlang官网提供的思想:record的修改是复制. -module (test_record). -record (record_5,{ aa1 = 0, aa2 = 0, aa3 = 0, ...

  10. 审批流_state_selection 相关用法

    审批流 _state_selection 前部分参数代表:新加的状态 后部分参数代表:原有系统的状态