08 多线程

1. 定义

1.1 多线程 vs 多进程
程序:一堆代码以文本形式存入一个文档
进程: 程序运行的一个状态
- 包含地址空间,内存,数据栈等
- 每个进程由自己完全独立的运行环境,多进程共享数据是一个问题

线程
- 一个进程的独立运行片段,一个进程可以由多个线程
- 轻量化的进程
- 一个进程的多个线程间共享数据和上下文运行环境
- 共享互斥问题
- 全局解释器锁(GIL)
    - Python代码的执行是由python虚拟机进行控制
    - 在主循环中稚嫩更有一个控制线程在执行

2. Python包

2.1 包介绍
thread:有问题,不好用
_thread:python3改成了_thread
threading: 通行的包
2.2 _thread包的使用
- 案例01: 顺序执行,耗时比较长
import time

def loop1():
    # ctime 得到当前时间
    print('Start loop 1 at :', time.ctime())
    # 睡眠多长时间,单位是秒
    time.sleep(4)
    print('End loop 1 at:', time.ctime())

def loop2():
    print('Start loop 2 at :', time.ctime())
    time.sleep(2)
    print('End loop 2 at:', time.ctime())

def main():
    print("Starting at:", time.ctime())
    loop1()
    loop2()
    print("All done at:", time.ctime())

if __name__ == '__main__':
    main()


>Starting at: Wed Aug 22 21:58:57 2018
 Start loop 1 at : Wed Aug 22 21:58:57 2018
 End loop 1 at: Wed Aug 22 21:59:01 2018
 Start loop 2 at : Wed Aug 22 21:59:01 2018
 End loop 2 at: Wed Aug 22 21:59:03 2018
 All done at: Wed Aug 22 21:59:03 2018
案例02: 改用多线程,缩短总时间,使用_thread
import time
import _thread as thread

def loop1():
    print('Start loop 1 at :', time.ctime())
    time.sleep(4)
    print('End loop 1 at:', time.ctime())

def loop2():
    print('Start loop 2 at :', time.ctime())
    time.sleep(2)
    print('End loop 2 at:', time.ctime())

def main():
    print("Starting at:", time.ctime())
    # 启动多线程的意思是用多线程去执行某个函数
    # 启动多线程函数为start_new_thead 
    # 参数两个,一个是需要运行的函数名,第二是函数的参数作为元祖使用,为空则使用空元祖 
    # 注意:如果函数只有一个参数,需要参数后由一个逗号  
    thread.start_new_thread(loop1, ())
    thread.start_new_thread(loop2, ())
    print("All done at:", time.ctime())


if __name__ == '__main__':
    main()
    while True:
        time.sleep(1)

>Starting at: Wed Aug 22 22:20:00 2018
 All done at: Wed Aug 22 22:20:00 2018
 Start loop 1 at : Wed Aug 22 22:20:00 2018
 Start loop 2 at : Wed Aug 22 22:20:00 2018
 End loop 2 at: Wed Aug 22 22:20:02 2018
 End loop 1 at: Wed Aug 22 22:20:04 2018
案例03: 多线程,传参数
import time
import _thread as thread

def loop1(in1):
    # ctime 得到当前时间
    print('Start loop 1 at :', time.ctime())
    # 把参数打印出来
    print("我是参数 ",in1)
    # 睡眠多长时间,单位是秒
    time.sleep(4)
    print('End loop 1 at:', time.ctime())

def loop2(in1, in2):
    # ctime 得到当前时间
    print('Start loop 2 at :', time.ctime())
    # 把参数in 和 in2打印出来,代表使用
    print("我是参数 " ,in1 , "和参数  ", in2)
    # 睡眠多长时间,单位是秒
    time.sleep(2)
    print('End loop 2 at:', time.ctime())

def main():
    print("Starting at:", time.ctime())
    thread.start_new_thread(loop1,("王老大", ))
    thread.start_new_thread(loop2,("王大鹏", "王晓鹏"))
    print("All done at:", time.ctime())

if __name__ == "__main__":
    main()
    # 一定要有while语句
 # 因为启动多线程后本程序就作为主线程存在 # 如果主线程执行完毕,则子线程可能也需要终止  
    while True:
        time.sleep(10)

>Starting at: Thu Aug 23 21:52:50 2018
 All done at: Wed Aug 22 22:24:40 2018
 Start loop 1 at : Wed Aug 22 22:24:40 2018
 我是参数  王老大
 Start loop 2 at : Wed Aug 22 22:24:40 2018
 我是参数  王大鹏 和参数   王晓鹏
 End loop 2 at: Wed Aug 22 22:24:42 2018
 End loop 1 at: Wed Aug 22 22:24:44 2018
2.3 threading的使用
直接利用threading.Thread生成Thread实例
    语法:
        t = threading.Thread(target=xxx, args=(xxx,))
        t.start():启动多线程
        t.join(): 等待多线程执行完成

案例04:
import time
# 导入多线程处理包
import threading

def loop1(in1):
    # ctime 得到当前时间
    print('Start loop 1 at :', time.ctime())
    # 把参数打印出来
    print("我是参数 ",in1)
    # 睡眠多长时间,单位是秒
    time.sleep(4)
    print('End loop 1 at:', time.ctime())

def loop2(in1, in2):
    # ctime 得到当前时间
    print('Start loop 2 at :', time.ctime())
    # 把参数in 和 in2打印出来,代表使用
    print("我是参数 " ,in1 , "和参数  ", in2)
    # 睡眠多长时间,单位是秒
    time.sleep(2)
    print('End loop 2 at:', time.ctime())

def main():
    print("Starting at:", time.ctime())
    # 生成threading.Thread实例
    t1 = threading.Thread(target=loop1, args=("王老大",))
    t1.start()

    t2 = threading.Thread(target=loop2, args=("王大鹏", "王小鹏"))
    t2.start()

    print("All done at:", time.ctime())

if __name__ == "__main__":
    main()
    while True:
        time.sleep(10)

>Starting at: Wed Aug 22 22:38:40 2018
 Start loop 1 at : Wed Aug 22 22:38:40 2018
 我是参数  王老大 Start loop 2 at :All done at:
  Wed Aug 22 22:38:40 2018
Wed Aug 22 22:38:40 2018我是参数  王大鹏
 和参数   王小鹏
End loop 2 at: Wed Aug 22 22:38:42 2018
End loop 1 at: Wed Aug 22 22:38:44 2018

## 说明:部分时间先后无法明确区分
案例05: join()
import time
import threading

def loop1(in1):
    print('Start loop 1 at :', time.ctime())
    print("我是参数 ",in1)
    time.sleep(4)
    print('End loop 1 at:', time.ctime())

def loop2(in1, in2):
    print('Start loop 2 at :', time.ctime())
    print("我是参数 ", in1, "和参数  ", in2)
    time.sleep(2)
    print('End loop 2 at:', time.ctime())

def main():
    print("Starting at:", time.ctime())
    # 生成threading.Thread实例
    t1 = threading.Thread(target=loop1, args=("王老大",))
    t1.start()

    t2 = threading.Thread(target=loop2, args=("王大鹏", "王小鹏"))
    t2.start()

    t1.join()
    t2.join()

    print("All done at:", time.ctime())

if __name__ == "__main__":
    main()
    while True:
        time.sleep(10)


>Starting at: Thu Aug 23 21:57:45 2018
 Start loop 1 at : Thu Aug 23 21:57:45 2018
 我是参数  Start loop 2 at :王老大 
 Thu Aug 23 21:57:45 2018
 我是参数  王大鹏 和参数   王小鹏
 End loop 2 at: Thu Aug 23 21:57:47 2018
 End loop 1 at: Thu Aug 23 21:57:49 2018
 All done at: Thu Aug 23 21:57:49 2018
2.4 守护线程-daemon
在程序中将子线程设置成守护现成,则子线程会在主线程结束的时候自动退出
一般认为,守护线程不重要或者不允许离开主线程独立运行
守护线程案例能否有效果跟环境相关

案例06:非守护线程
import time as t
import threading

def fun():
    print('start fun')
    t.sleep(2)
    print('end fun')

print('main thread')

## 注意:此处函数不带括号
t1 = threading.Thread(target=fun, args=())
t1.start()
t.sleep(1)
print('main thread end')

>main thread
 start fun
 main thread end
 end fun
 ## 从结果看出:主程序结束后,线程并未结束
案例07:守护线程
import time
import threading

def fun():
    print("Start fun")
    time.sleep(2)
    print("end fun")

print("Main thread")

t1 = threading.Thread(target=fun, args=() )
# 社会守护线程的方法,必须在start之前设置,否则无效
t1.setDaemon(True)
# 设置线程的第二种方式
#t1.daemon = True
t1.start()

time.sleep(1)
print("Main thread end")

>Main thread
 Start fun
 Main thread end
 # 主线程结束后,守护线程也结束
2.5 线程常用属性
threading.currentThread:返回当前线程变量
threading.enumerate:返回一个包含正在运行的线程的list,正在运行的线程指的是线程启动后,结束前的状态
threading.activeCount: 返回正在运行的线程数量,效果跟 len(threading.enumerate)相同
threading.setName: 给线程设置名字
threading.getName: 得到线程的名字

案例08:线程常用属性
import time
import threading

def loop1():
    print('Start loop 1 at :', time.ctime())
    time.sleep(6)
    print('End loop 1 at:', time.ctime())

def loop2():
    print('Start loop 2 at :', time.ctime())
    time.sleep(1)
    print('End loop 2 at:', time.ctime())

def loop3():
    print('Start loop 3 at :', time.ctime())
    time.sleep(5)
    print('End loop 3 at:', time.ctime())

def main():
    print("Starting at:", time.ctime())
    t1 = threading.Thread(target=loop1, args=( ))
    # threading.setName: 给线程设置名字
    t1.setName("THR_1")
    t1.start()

    t2 = threading.Thread(target=loop2, args=( ))
    t2.setName("THR_2")
    t2.start()

    t3 = threading.Thread(target=loop3, args=( ))
    t3.setName("THR_3")
    t3.start()

    # 3秒后,thread2已经自动结束,
    time.sleep(3)
    # enumerate 得到正在运行子线程,即子线程1和子线程3
    for thr in threading.enumerate():
        # getName能够得到线程的名字
        print("正在运行的线程名字是: {0}".format(thr.getName()))
        # threading.activeCount: 返回正在运行的线程数量,效果跟 len(threading.enumerate)相同
    print("正在运行的子线程数量为:{0}".format(threading.activeCount()))

    print("All done at:", time.ctime())

if __name__ == "__main__":
    main()
    # 一定要有while语句
    # 因为启动多线程后本程序就作为主线程存在 # 如果主线程执行完毕,则子线程可能也需要终止  
     while True:
        time.sleep(10)

>Starting at: Thu Aug 23 22:19:20 2018
 Start loop 1 at : Thu Aug 23 22:19:20 2018
 Start loop 2 at : Thu Aug 23 22:19:20 2018
 Start loop 3 at : Thu Aug 23 22:19:20 2018
 End loop 2 at: Thu Aug 23 22:19:21 2018
 正在运行的线程名字是: MainThread
 正在运行的线程名字是: THR_1
 正在运行的线程名字是: THR_3
 正在运行的子线程数量为:3
 All done at: Thu Aug 23 22:19:23 2018
 End loop 3 at: Thu Aug 23 22:19:25 2018
 End loop 1 at: Thu Aug 23 22:19:26 2018
2.6 继承方法使用多线程
以上均为直接生成Thread的实例,现在采用第二种直接继承自threading.Thread
语法:
    - 直接继承Thread
    - 重写run函数
    - 类实例可以直接运行

案例09:线程常用属性
import threading
import time

# 1. 类需要继承自threading.Thread
class MyThread(threading.Thread):
    def __init__(self, arg):
        super(MyThread, self).__init__()
        self.arg = arg

    # 2 必须重写run函数,run函数代表的是真正执行的功能
    def run(self):
        time.sleep(2)
        print("The args for this class is {0}".format(self.arg))

for i in range(5):
    t = MyThread(i)
    t.start()
    t.join()

print("Main thread is done!!!!!!!!")

>The args for this class is 0
 The args for this class is 1
 The args for this class is 2
 The args for this class is 3
 The args for this class is 4
 Main thread is done!!!!!!!!
案例10:企业多线程写法
import threading
from time import sleep, ctime

class ThreadFunc:

    def __init__(self, name):
        self.name = name

    def loop(self, nloop, nsec):
        """
        :param nloop: loop函数的名称
        :param nsec: 系统休眠时间
        :return:
        """
        print('Start loop ', nloop, 'at ', ctime())
        sleep(nsec)
        print('Done loop ', nloop, ' at ', ctime())

def main():
    print("Starting at: ", ctime())

    # 以下t1 和  t2的定义方式相等
    t = ThreadFunc("loop")
    t1 = threading.Thread(target=t.loop, args=("LOOP1", 4))
    # 下面这种写法更西方人,工业化一点
    t2 = threading.Thread(target=ThreadFunc('loop').loop, args=("LOOP2", 2))

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print("ALL done at: ", ctime())

if __name__ == '__main__':
    main()

>Starting at:  Thu Aug 23 22:55:49 2018
 Start loop  LOOP1 at  Thu Aug 23 22:55:49 2018
 Start loop  LOOP2 at  Thu Aug 23 22:55:49 2018
 Done loop  LOOP2  at  Thu Aug 23 22:55:51 2018
 Done loop  LOOP1  at  Thu Aug 23 22:55:53 2018
 ALL done at:  Thu Aug 23 22:55:53 2018
2.7 共享变量
共享变量: 当多个现成同时访问一个变量的时候,会产生共享变量的问题

案例11:共享变量问题
import threading

sum = 0
n = 1000000

def Add():
    global sum, n
    for i in range(n):
        sum += 1

def Minu():
    global sum, n
    for i in range(n):
        sum -= 1

if __name__ == '__main__':
    print('Starting......{0}'.format(sum))
    t1 = threading.Thread(target=Add, args=())
    t2 = threading.Thread(target=Minu, args=())
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('End......{0}'.format(sum))


>Starting......0
 End......-182314
共享变量问题解决方法:锁(Lock)---是一个标志,表示一个线程在占用一些资源

案例12:上锁-->使用共享资源-->取消锁,释放锁
import threading

sum = 0
n = 1000000

lock =threading.Lock()

def Add():
    global sum, n
    for i in range(n):
        ## 上锁,申请锁
  lock.acquire()
        sum += 1
  ## 释放锁
  lock.release()

def Minu():
    global sum, n
    for i in range(n):
        lock.acquire()
        sum -= 1
  lock.release()

if __name__ == '__main__':
    print('Starting......{0}'.format(sum))
    t1 = threading.Thread(target=Add, args=())
    t2 = threading.Thread(target=Minu, args=())
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('End......{0}'.format(sum))

>Starting......0
 End......0
线程安全问题:如果一个资源/变量,他对于多线程来讲,不用加锁也不会引起任何问题,则称为线程安全
    线程不安全变量类型: list, set, dict
    线程安全变量类型: queue

案例13 :生产者消费者问题
    - 一个模型,可以用来搭建消息队列, 
    - queue是一个用来存放变量的数据结构,特点是先进先出,内部元素排队,可以理解成一个特殊的list
import threading
import time

# Python2
# from Queue import Queue

# Python3
import queue

class Producer(threading.Thread):
    def run(self):
        global queue
        count = 0
        while True:
            # qsize返回queue内容长度
            if queue.qsize() < 1000:
                for i in range(100):
                    count = count +1
                    msg = '生成产品'+str(count)
                    # put是网queue中放入一个值
                    queue.put(msg)
                    print(msg)
                    time.sleep(0.5)

class Consumer(threading.Thread):
    def run(self):
        global queue
        while True:
            if queue.qsize() > 100:
                for i in range(3):
                    # get是从queue中取出一个值
                    msg = self.name + '消费了 '+queue.get()
                    print(msg)
                    time.sleep(1)

if __name__ == '__main__':
    queue = queue.Queue()

    for i in range(500):
        queue.put('初始产品'+str(i))
    for i in range(2):
        p = Producer()
        p.start()
    for i in range(5):
        c = Consumer()
        c.start()

案例14:死锁问题
import threading
import time

lock_1 = threading.Lock()
lock_2 = threading.Lock()

def func_1():

   print("func_1 starting.........")
   lock_1.acquire()
   print("func_1 申请了 lock_1....")
   time.sleep(2)
   print("func_1 等待 lock_2.......")
   lock_2.acquire()
   print("func_1 申请了 lock_2.......")

   lock_2.release()
   print("func_1 释放了 lock_2")

   lock_1.release()
   print("func_1 释放了 lock_1")

   print("func_1 done..........")

def func_2():
   print("func_2 starting.........")
   lock_2.acquire()
   print("func_2 申请了 lock_2....")
   time.sleep(4)
   print("func_2 等待 lock_1.......")
   lock_1.acquire()
   print("func_2 申请了 lock_1.......")

   lock_1.release()
   print("func_2 释放了 lock_1")

   lock_2.release()
   print("func_2 释放了 lock_2")

   print("func_2 done..........")

if __name__ == "__main__":

   print("主程序启动..............")
   t1 = threading.Thread(target=func_1, args=())
   t2 = threading.Thread(target=func_2, args=())

   t1.start()
   t2.start()

   t1.join()
   t2.join()

   print("主程序启动..............")
案例15:锁的等待时间问题
import threading
import time

lock1 =threading.Lock()
lock2 =threading.Lock()

def func_1():
    lock1.acquire(timeout=4)
    time.sleep(2)
    rst = lock2.acquire(timeout=2)
    if rst:
        lock2.release()
        print(rst)
    else:
        print('函数1没有申请到锁2,会释放锁1')
    lock1.release()

def func_2():
    lock2.acquire()
    time.sleep(4)
    rst = lock1.acquire()
    if rst:
        print('函数2申请到了锁1')
    lock1.release()
    lock2.release()

if __name__ == '__main__':
    print('主程序启动......')

    t1 = threading.Thread(target=func_1,args=())
    t2 = threading.Thread(target=func_2,args=())

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print('主程序完成......')

>主程序启动......
 函数1没有申请到锁2,会释放锁1
 函数2申请到了锁1
 主程序完成......

案例16:semphore--设置一个资源最多由几个多线程同时使用
import threading
import time

# 参数定义最多几个线程同时使用资源
semaphore = threading.Semaphore(3)

def func():
    if semaphore.acquire():
        print(threading.currentThread().getName() + ' get semaphore')
        time.sleep(2)
        semaphore.release()
        print(threading.currentThread().getName() + ' release semaphore')

for i in range(8):
    t1 = threading.Thread(target=func)
    t1.start()
案例17:threading.Timer--利用多线程,在指定时间后启动一个功能
import threading
import time

def func():
    print("I am running.........")
    time.sleep(4)
    print("I am done......")

if __name__ == "__main__":
    t = threading.Timer(6, func)
    t.start()

    i = 0
    while True:
        print("{0}***************".format(i))
        time.sleep(3)
        i += 1
案例18:可重入锁
    - 一个锁,可以被一个线程多次申请
    - 主要解决递归调用的时候,需要申请锁的情况
import threading
import time

class MyThread(threading.Thread):
    ## 重写run函数
  def run(self):
        global num
        time.sleep(1)

        if mutex.acquire(1):
            num = num+1
  msg = self.name+' set num to '+str(num)
            print(msg)
            mutex.acquire()
            mutex.release()
            mutex.release()

num = 0
## 实例化可重入锁
mutex = threading.RLock()

def testTh():
    for i in range(5):
        t = MyThread()
        t.start()

3. 线程替代方案

3.1 替代进程
subprocess
    - 完全跳过线程,使用进程
    - 是派生进程的主要替代方案
    - python2.4后引入
multiprocessiong
    - 使用threadiing借口派生,使用子进程
    - 允许为多核或者多cpu派生进程,接口跟threading非常相似
    - python2.6
concurrent.futures
    - 新的异步执行模块
    - 任务级别的操作
    - python3.2后引入
3.2 多进程
进程间通讯(InterprocessCommunication, IPC )
进程之间无任何共享状态
进程的创建

案例19:直接生成Process实例对象
import multiprocessing
from time import sleep, ctime

## interval 间隔
def clock(interval):
    while True:
        print("The time is %s" % ctime())
        sleep(interval)

if __name__ == '__main__':
    p = multiprocessing.Process(target=clock, args = (5,))
    p.start()

    while True:
        print('sleeping.......')
        sleep(1)
>略

 通过加过可看出此时两个进程交替运行

案例20:派生子进程
import multiprocessing
from time import sleep, ctime

class ClockProcess(multiprocessing.Process):
    def __init__(self, interval):
        super().__init__()
        self.interval = interval

    def run(self):
        while True:
            print("The time is %s" % ctime())
            sleep(self.interval)

if __name__ == '__main__':
    p = ClockProcess(3)
    p.start()

    while True:
        print('sleeping.......')
        sleep(1)

>sleeping.......
 The time is Sat Aug 25 14:55:23 2018
 sleeping.......
 sleeping.......
 sleeping.......
 The time is Sat Aug 25 14:55:26 2018
 sleeping.......
 sleeping....... 
案例21:查看pid,ppid以及他们的关系
from multiprocessing import Process
import os

def info(title):
    print(title)
    print('module name:', __name__)
    # 得到父亲进程的id
  print('parent process:', os.getppid())
    # 得到本身进程的id
  print('process id:', os.getpid())

def f(name):
    info('function f')
    print('hello', name)

if __name__ == '__main__':
    info('main line')
    p = Process(target=f, args=('bob',))
    p.start()
    p.join()

>main line
 module name: __main__
 parent process: 4348
 process id: 6500
 function f
 module name: __mp_main__
 parent process: 6500
 process id: 7340
 hello bob
 
 ## 可知,两个进程共用父进程

案例22:生产者消费者模型
- JoinableQueue
import multiprocessing
from time import ctime

def consumer(input_q):
    print("Into consumer:", ctime())
    while True:
        # 处理项
  item = input_q.get()
        print ("pull", item, "out of q") # 此处替换为有用的工作
  input_q.task_done() # 发出信号通知任务完成
  print ("Out of consumer:", ctime()) ##此句未执行,因为q.join()收集到四个task_done()信号后,主进程启动,未等到print此句完成,程序就结束了

def producer(sequence, output_q):
    print ("Into procuder:", ctime())
    for item in sequence:
        output_q.put(item)
        print ("put", item, "into q")
    print ("Out of procuder:", ctime())

# 建立进程
if __name__ == '__main__':
    q = multiprocessing.JoinableQueue()
    # 运行消费者进程
  cons_p = multiprocessing.Process (target = consumer, args = (q,))
    cons_p.daemon = True
  cons_p.start()

    # 生产多个项,sequence代表要发送给消费者的项序列
 # 在实践中,这可能是生成器的输出或通过一些其他方式生产出来  sequence = [1,2,3,4]
    producer(sequence, q)
    # 等待所有项被处理
  q.join()
案例23:队列中哨兵的使用
<pre style="background-color:#2b2b2b;color:#a9b7c6;font-family:'宋体';font-size:10.5pt;">import multiprocessing
from time import ctime

# 设置哨兵问题
def consumer(input_q):
    print("Into consumer:", ctime())
    while True:
        item = input_q.get()
        if item is None:
            break
  print("pull", item, "out of q")
    print ("Out of consumer:", ctime()) ## 此句执行完成,再转入主进程

def producer(sequence, output_q):
    print ("Into procuder:", ctime())
    for item in sequence:
        output_q.put(item)
        print ("put", item, "into q")
    print ("Out of procuder:", ctime())

if __name__ == '__main__':
    q = multiprocessing.Queue()
    cons_p = multiprocessing.Process(target = consumer, args = (q,))
    cons_p.start()

    sequence = [1,2,3,4]
    producer(sequence, q)

    q.put(None)
    cons_p.join()
案例24:哨兵的改进
import multiprocessing
from time import ctime

def consumer(input_q):
    print ("Into consumer:", ctime())
    while True:
        item = input_q.get()
        if item is None:
            break
  print("pull", item, "out of q")
    print ("Out of consumer:", ctime())

def producer(sequence, output_q):
    for item in sequence:
        print ("Into procuder:", ctime())
        output_q.put(item)
        print ("Out of procuder:", ctime())

if __name__ == '__main__':
    q = multiprocessing.Queue()
    cons_p1 = multiprocessing.Process (target = consumer, args = (q,))
    cons_p1.start()

    cons_p2 = multiprocessing.Process (target = consumer, args = (q,))
    cons_p2.start()

    sequence = [1,2,3,4]
    producer(sequence, q)

    q.put(None)
    q.put(None)

    cons_p1.join()
    cons_p2.join()

猜你喜欢

转载自blog.csdn.net/qq_25672165/article/details/88839672
今日推荐