码迷,mamicode.com
首页 > 编程语言 > 详细

Python中的线程锁

时间:2020-07-01 22:09:58      阅读:60      评论:0      收藏:0      [点我收藏+]

标签:属性   开启   语句   ret   ica   pytho   获得   val   上线   

Python中的线程锁

前言

  本章节继续探讨threading模块下关于锁的应用,注意。这一期很重要,依然是围绕着理论篇来讲,这一章节主要围绕理论篇中的线程切换做讲解,因此一定要有一些线程切换方面的知识。

  官方中文文档

线程安全

 

  线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

 

  在聊线程安全之前,我还是决定拿之前的那个例子来描述它。

 

  线程安全的问题还是由于线程切换导致的,比如这个房间(进程)一共有10颗糖(资源),一个小人(子线程)吃了3颗糖被CPU通知歇息一会,那么他会认为还剩下7颗糖,另一个干活的小人又吃了3颗糖后去休息了,那么现在第一个休息的小人上线了,但是真正的糖果数量只剩下了4颗,他还傻乎乎的认为是7颗。

 

  这里关于线程安全的代码测试我决定不用上面例举的这个例子,而是用多线程做密集型计算做演示,数值越大效果越明显:

import threading

num = 0
def add():
    global num
    for i in range(10000000):  # 一千万次
        num += 1

def sub():
    global num
    for i in range(10000000):  # 一千万次
        num -= 1

if __name__ == __main__:


    t1 = threading.Thread(target=add,)
    t2 = threading.Thread(target=sub,)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

    print("最终结果:",num)

# ==== 执行结果 ==== 三次采集

"""
最终结果: -1472151
最终结果: -2814372
最终结果: -5149396
"""

 

  一个加一千万次,一个减一千万次,按理来说最后的结果应该是0,但是为什么相差这么大?更加恐怖的是每次的结果都完全不一样呢?

  其实这就是由于线程切换导致出的线程安全问题,因为我们不能精确的知道CPU会在什么时候进行线程的切换,那么如何控制它呢?我们就需要用到锁,其实通俗点来讲你可以这么认为他:

 

  线程安全的目标:

    线程之间共同操作一个资源 如何保持资源的同步性

    因为线程的切换是由CPU控制的,所以我们要控制它的切换

 

  线程安全的定义:

    线程安全:多线程操作时,内部会让所有线程排队处理

    线程不安全:我们需要一个机制来让所有线程进行排队处理

 

  那么什么时候我们要考虑线程安全的问题呢?

    多个线程对同一数据源进行写入操作时

 

  Ps:在CPython的八大基本数据类型中,listdict本身就是属于线程安全的容器。

 

锁的作用

  锁就提供给我们能够自行操控线程切换的一种手段,而并非系统自带的切换机制进行切换。

 

Lock同步锁

方法大全


 

Lock同步(互斥)锁方法方法大全  
方法/属性名称 功能描述
acquire(blocking=True, timeout=-1) 上锁,在上锁状态中的代码块运行时不允许切换至其他线程运行。
release() 解锁,解锁后系统可以切换至其他线程运行。
locked() 如果获得了锁则返回真值。

 

技术图片

 

使用方式


 

  被上锁和解锁期间的代码块执行时,不会切换至其他线程,有一点要注意的是对于lock锁而言,一次acquire()必须对应一次release(),不能出现重复的使用两次acquire()的操作,这会造成死锁!

  注意:同步锁是一次只能放行一个线程。

 

import threading

num = 0


def add():
    lock.acquire()  # 上锁
    global num
    for i in range(10000000):  # 一千万次
        num += 1
    lock.release()  # 解锁


def sub():
    lock.acquire()  # 上锁
    global num
    for i in range(10000000):  # 一千万次
        num -= 1
    lock.release()  # 解锁


if __name__ == __main__:
    lock = threading.Lock()  # 实例化同步锁对象

    t1 = threading.Thread(target=add, )
    t2 = threading.Thread(target=sub, )
    t1.start()
    t2.start()
    t1.join()
    t2.join()

    print("最终结果:", num)

# ==== 执行结果 ==== 三次采集

"""
最终结果: 0
最终结果: 0
最终结果: 0
"""
技术图片
import threading

num = 0
def add():
    lock.acquire()  # 上锁
    lock.acquire()  # 死锁
    global num
    for i in range(10000000):  # 一千万次
        num += 1
    lock.release()
    lock.release()

def sub():
    lock.acquire()  # 上锁
    lock.acquire()  # 死锁
    global num
    for i in range(10000000):  # 一千万次
        num -= 1
    lock.release()
    lock.release()  

if __name__ == __main__:

    lock = threading.Lock() # 实例化同步锁对象

    t1 = threading.Thread(target=add,)
    t2 = threading.Thread(target=sub,)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

    print("最终结果:",num)

# ==== 执行结果 ==== 三次采集

"""
卡住不动了
"""
Lock同步锁的死锁现象

 

上下文管理


  threading.Lock()对象中实现了__enter____exit__方法,因此我们可以使用with语句进行上下文管理式的加锁。

技术图片
import threading

num = 0
def add():
    with lock:  # 自动加锁与解锁
        global num
        for i in range(10000000):  # 一千万次
            num += 1


def sub():
    with lock:  # 自动加锁与解锁
        global num
        for i in range(10000000):  # 一千万次
            num -= 1


if __name__ == __main__:

    lock = threading.Lock()

    t1 = threading.Thread(target=add,)
    t2 = threading.Thread(target=sub,)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

    print("最终结果:",num)

# ==== 执行结果 ==== 三次采集

"""
最终结果: 0
最终结果: 0
最终结果: 0
"""
with语句的使用

 

RLock递归锁

方法大全


Rlock递归锁方法方法大全  
方法/属性名称 功能描述
acquire(blocking=True, timeout=-1) 上锁,在上锁状态中的代码块运行时不允许切换至其他线程运行。如果已上锁,递归层数增加一层。
release() 解锁,解锁后系统可以切换至其他线程运行。

 

使用方式


 

  RLock递归锁的使用方式与同步锁相同,唯一的不同点就是可以多次acquire(),这并不会产生死锁,但是有几次acquire()就应该有相应的几次release(),否则依然会造成死锁!

  注意:递归锁与同步锁相同,也是一次只能放行一个线程。并且也支持上下文管理。

 

import threading

num = 0
def add():
    lock.acquire()  # 上锁 + 1
    lock.acquire()  # 上锁 + 1
    global num
    for i in range(10000000):  # 一千万次
        num += 1
    lock.release()  # 解锁 - 1
    lock.release()  # 解锁 - 1

def sub():
    lock.acquire()  # 上锁
    lock.acquire()  # 死锁
    global num
    for i in range(10000000):  # 一千万次
        num -= 1
    lock.release()  # 解锁 - 1
    lock.release()  # 解锁 - 1

if __name__ == __main__:

    lock = threading.RLock()  # 实例化递归锁对象

    t1 = threading.Thread(target=add,)
    t2 = threading.Thread(target=sub,)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

    print("最终结果:",num)

# ==== 执行结果 ==== 三次采集

"""
最终结果: 0
最终结果: 0
最终结果: 0
"""

 

Condition条件锁

方法大全


 

Condition条件锁方法方法大全  
方法/属性名称 功能描述
acquire(blocking=True, timeout=-1) 上锁,在上锁状态中的代码块运行时不允许切换至其他线程运行。
release() 解锁,解锁后系统可以切换至其他线程运行。
wait(timeout=None) 等待唤醒,此时该线程是处于暂停运行状态,要么等待通知要么等待到超时时间后继续执行该线程。
wait_for(predicate, timeout=None) 等待唤醒,直到返回了一个Truepredicate是一个可调用的对象,其返回值应该是True或者False
notify(n=1) 通知唤醒,可以唤醒多个处于wait()的线程,默认为1个。
notify_all() 通知唤醒,唤醒所有处于wait()的线程。

 

使用方式


 

  Condition条件锁是在递归锁的基础上增加了能够暂停线程运行的功能。并且我们可以使用wait()notify()来控制每个线程执行的个数。

  注意:条件锁可以自由设定一次放行几个线程。

 

import threading

num = 0

def task():
    obj = threading.current_thread()
    print("当前是线程[{0}],已经开始运行了...".format(obj.getName()))
    cond.acquire()  # 上锁
    global num
    print("当前是线程[{0}],处于等待状态...".format(obj.getName()))
    cond.wait()  # 暂停,等待唤醒
    num += 1
    print("当前是线程[{0}],等待状态结束,继续运行...".format(obj.getName()))
    cond.release()  # 解锁

if __name__ == __main__:

    cond = threading.Condition()  # 实例条件锁对象

    for i in range(10):
        t1 = threading.Thread(target=task,)  # 开启10条线程
        t1.start()  # 等待CPU调度执行

    while num < 10:
        task_num = int(input("请输入你要执行的线程数量:"))

        cond.acquire()
        cond.notify(task_num)  # 通知唤醒
        cond.release()

    print("最终结果:",num)

# ==== 执行结果 ====

"""
当前是线程[Thread-1],已经开始运行了...
当前是线程[Thread-1],处于等待状态...
当前是线程[Thread-2],已经开始运行了...
当前是线程[Thread-2],处于等待状态...
当前是线程[Thread-3],已经开始运行了...
当前是线程[Thread-3],处于等待状态...
当前是线程[Thread-4],已经开始运行了...
当前是线程[Thread-4],处于等待状态...
当前是线程[Thread-5],已经开始运行了...
当前是线程[Thread-5],处于等待状态...
当前是线程[Thread-6],已经开始运行了...
当前是线程[Thread-6],处于等待状态...
当前是线程[Thread-7],已经开始运行了...
当前是线程[Thread-7],处于等待状态...
当前是线程[Thread-8],已经开始运行了...
当前是线程[Thread-8],处于等待状态...
当前是线程[Thread-9],已经开始运行了...
当前是线程[Thread-9],处于等待状态...
当前是线程[Thread-10],已经开始运行了...
当前是线程[Thread-10],处于等待状态...
请输入你要执行的线程数量:2
当前是线程[Thread-1],等待状态结束,继续运行...
当前是线程[Thread-2],等待状态结束,继续运行...
请输入你要执行的线程数量:3
当前是线程[Thread-3],等待状态结束,继续运行...
当前是线程[Thread-5],等待状态结束,继续运行...
当前是线程[Thread-4],等待状态结束,继续运行...
请输入你要执行的线程数量:5
当前是线程[Thread-6],等待状态结束,继续运行...
当前是线程[Thread-7],等待状态结束,继续运行...
当前是线程[Thread-8],等待状态结束,继续运行...
当前是线程[Thread-10],等待状态结束,继续运行...
当前是线程[Thread-9],等待状态结束,继续运行...
请输入你要执行的线程数量:1
最终结果: 10
"""

 

上下文管理


 

技术图片
import threading

num = 0

def task():
    obj = threading.current_thread()
    print("当前是线程[{0}],已经开始运行了...".format(obj.getName()))

    with cond:
        global num
        print("当前是线程[{0}],处于等待状态...".format(obj.getName()))
        cond.wait()  # 暂停,等待唤醒
        num += 1
        print("当前是线程[{0}],等待状态结束,继续运行...".format(obj.getName()))


if __name__ == __main__:

    cond = threading.Condition()  # 实例化递归锁对象

    for i in range(10):
        t1 = threading.Thread(target=task,)  # 开启10条线程
        t1.start()  # 等待CPU调度执行

    while num < 10:
        task_num = int(input("请输入你要执行的线程数量:"))

        with cond:
            cond.notify(task_num)  # 通知唤醒


    print("最终结果:",num)

# ==== 执行结果 ====

"""
当前是线程[Thread-1],已经开始运行了...
当前是线程[Thread-1],处于等待状态...
当前是线程[Thread-2],已经开始运行了...
当前是线程[Thread-2],处于等待状态...
当前是线程[Thread-3],已经开始运行了...
当前是线程[Thread-3],处于等待状态...
当前是线程[Thread-4],已经开始运行了...
当前是线程[Thread-4],处于等待状态...
当前是线程[Thread-5],已经开始运行了...
当前是线程[Thread-5],处于等待状态...
当前是线程[Thread-6],已经开始运行了...
当前是线程[Thread-6],处于等待状态...
当前是线程[Thread-7],已经开始运行了...
当前是线程[Thread-7],处于等待状态...
当前是线程[Thread-8],已经开始运行了...
当前是线程[Thread-8],处于等待状态...
当前是线程[Thread-9],已经开始运行了...
当前是线程[Thread-9],处于等待状态...
当前是线程[Thread-10],已经开始运行了...
当前是线程[Thread-10],处于等待状态...
请输入你要执行的线程数量:2
当前是线程[Thread-1],等待状态结束,继续运行...
当前是线程[Thread-2],等待状态结束,继续运行...
请输入你要执行的线程数量:3
当前是线程[Thread-3],等待状态结束,继续运行...
当前是线程[Thread-5],等待状态结束,继续运行...
当前是线程[Thread-4],等待状态结束,继续运行...
请输入你要执行的线程数量:5
当前是线程[Thread-6],等待状态结束,继续运行...
当前是线程[Thread-7],等待状态结束,继续运行...
当前是线程[Thread-8],等待状态结束,继续运行...
当前是线程[Thread-10],等待状态结束,继续运行...
当前是线程[Thread-9],等待状态结束,继续运行...
请输入你要执行的线程数量:1
最终结果: 10
"""
with语句的使用

 

Event事件锁

方法大全


 

Event事件锁方法方法大全  
方法/属性名称 功能描述
is_set() 用来判断当前红绿灯(标志位)的状态,红灯为False,绿灯为True
set() 通知所有处于红灯状态的线程开始运行,这相当于将标志位改为True
clear() 将所有处于绿灯状态的线程暂停,这相当于将标志位改为False
wait(timeout=None) 阻塞当前线程直到被放行,即等待红绿灯的通知,红灯停绿灯行。

 

使用方式


 

  这玩意儿是基于Condition条件锁做的,跟条件锁的区别就在于他是非常干脆利落的。

  注意:事件锁只能一次全部放行,相当于红绿灯一样,等车的红灯全停,绿灯全过。

  此外,事件锁不支持上下文管理协议。

 

import threading


def task():
    obj = threading.current_thread()
    print("当前是线程[{0}],已经开始运行了...".format(obj.getName()))
    print("当前是线程[{0}],红灯了,停车...".format(obj.getName()))
    event.wait()  # 暂停,等待绿灯通行
    print("当前是线程[{0}],绿灯了放行...".format(obj.getName()))
    print("当前是线程[{0}],卧槽怎么又是红灯,停车...".format(obj.getName()))
    event.wait()  # 暂停,等待绿灯通行
    print("当前是线程[{0}],继续运行...".format(obj.getName()))


if __name__ == __main__:

    event = threading.Event()  # 实例化事件锁对象

    for i in range(10):
        t1 = threading.Thread(target=task,)  # 开启10条线程
        t1.start()  # 等待CPU调度执行

    event.set()  # 设置为绿灯 针对第一次
    event.clear()  # 设置为红灯,如果没有他那么上面不管wait()多少次都没用了。因为全都是绿灯
    event.set()  # 再次设置为绿灯,针对第二次


# ==== 执行结果 ====

"""
当前是线程[Thread-1],已经开始运行了...
当前是线程[Thread-1],红灯了,停车...
当前是线程[Thread-2],已经开始运行了...
当前是线程[Thread-2],红灯了,停车...
当前是线程[Thread-3],已经开始运行了...
当前是线程[Thread-3],红灯了,停车...
当前是线程[Thread-4],已经开始运行了...
当前是线程[Thread-4],红灯了,停车...
当前是线程[Thread-5],已经开始运行了...
当前是线程[Thread-5],红灯了,停车...
当前是线程[Thread-6],已经开始运行了...
当前是线程[Thread-6],红灯了,停车...
当前是线程[Thread-7],已经开始运行了...
当前是线程[Thread-7],红灯了,停车...
当前是线程[Thread-8],已经开始运行了...
当前是线程[Thread-8],红灯了,停车...
当前是线程[Thread-9],已经开始运行了...
当前是线程[Thread-9],红灯了,停车...
当前是线程[Thread-10],已经开始运行了...
当前是线程[Thread-10],红灯了,停车...
当前是线程[Thread-10],绿灯了放行...
当前是线程[Thread-10],卧槽怎么又是红灯,停车...
当前是线程[Thread-5],绿灯了放行...
当前是线程[Thread-9],绿灯了放行...
当前是线程[Thread-9],卧槽怎么又是红灯,停车...
当前是线程[Thread-1],绿灯了放行...
当前是线程[Thread-1],卧槽怎么又是红灯,停车...
当前是线程[Thread-6],绿灯了放行...
当前是线程[Thread-6],卧槽怎么又是红灯,停车...
当前是线程[Thread-3],绿灯了放行...
当前是线程[Thread-4],绿灯了放行...
当前是线程[Thread-2],绿灯了放行...
当前是线程[Thread-1],继续运行...
当前是线程[Thread-5],卧槽怎么又是红灯,停车...
当前是线程[Thread-3],卧槽怎么又是红灯,停车...
当前是线程[Thread-7],绿灯了放行...
当前是线程[Thread-7],卧槽怎么又是红灯,停车...
当前是线程[Thread-7],继续运行...
当前是线程[Thread-4],卧槽怎么又是红灯,停车...
当前是线程[Thread-3],继续运行...
当前是线程[Thread-10],继续运行...
当前是线程[Thread-5],继续运行...
当前是线程[Thread-8],绿灯了放行...
当前是线程[Thread-6],继续运行...
当前是线程[Thread-4],继续运行...
"""

 

Semaphore信号量锁

方法大全


Semaphore信号量锁方法方法大全  
方法/属性名称 功能描述
acquire(blocking=True, timeout=-1) 上锁,在上锁状态中的代码块运行时不允许切换至其他线程运行。
release() 解锁,解锁后系统可以切换至其他线程运行。

 

使用方式


 

  这玩意儿是可以规定一次最多跑多少线程的一个东西。也是基于Condition条件锁做的。

  注意:区分与Condition条件锁的区别,Semaphore信号量锁不能自由规定,只能规定一次,而条件锁可以多次规定。

 

import threading
import time

def task():
    sema.acquire()
    time.sleep(1)
    obj = threading.current_thread()
    print("当前是线程[{0}],已经开始运行了...".format(obj.getName()))
    sema.release()


if __name__ == __main__:

    sema = threading.Semaphore(3)  # 实例化信号量锁对象,代表每次都跑3条。

    for i in range(10):
        t1 = threading.Thread(target=task,)  # 开启10条线程
        t1.start()  # 等待CPU调度执行



# ==== 执行结果 ====

"""
当前是线程[Thread-1],已经开始运行了...
当前是线程[Thread-3],已经开始运行了...
当前是线程[Thread-2],已经开始运行了...

当前是线程[Thread-4],已经开始运行了...
当前是线程[Thread-6],已经开始运行了...
当前是线程[Thread-5],已经开始运行了...

当前是线程[Thread-7],已经开始运行了...
当前是线程[Thread-9],已经开始运行了...
当前是线程[Thread-8],已经开始运行了...

当前是线程[Thread-10],已经开始运行了...
"""

 

上下文管理



技术图片
import threading
import time

def task():
    with sema:
        time.sleep(1)
        obj = threading.current_thread()
        print("当前是线程[{0}],已经开始运行了...".format(obj.getName()))



if __name__ == __main__:

    sema = threading.Semaphore(3)  # 实例化信号量锁对象,代表每次都跑3条。

    for i in range(10):
        t1 = threading.Thread(target=task,)  # 开启10条线程
        t1.start()  # 等待CPU调度执行



# ==== 执行结果 ====

"""
当前是线程[Thread-1],已经开始运行了...
当前是线程[Thread-3],已经开始运行了...
当前是线程[Thread-2],已经开始运行了...

当前是线程[Thread-4],已经开始运行了...
当前是线程[Thread-6],已经开始运行了...
当前是线程[Thread-5],已经开始运行了...

当前是线程[Thread-7],已经开始运行了...
当前是线程[Thread-9],已经开始运行了...
当前是线程[Thread-8],已经开始运行了...

当前是线程[Thread-10],已经开始运行了...
"""
with语句的使用

 

扩展:练习题

Condition条件锁的应用


 

  需求:一个空列表,两个线程轮番往里面加值(一个加偶数,一个加奇数),让该列表中的值为 1 - 100。

 

import threading
import time

li = []

def even():
    """加偶数"""
    with cond:  # 加锁
        for i in range(2, 101, 2):
            if len(li) % 2 != 0:
                li.append(i)
                cond.notify()  # notify()并不会立即终止当前线程的执行,而是告诉另一线程。你可以走了,不过得等我wait()之后
                cond.wait()  # 阻塞住,执行另一线程,直到另一线程发送了notify()并且它wait()了之后。
            else:
                cond.wait()
                li.append(i)
                cond.notify()
        else:
            cond.notify()

def odd():
    """加奇数"""
    with cond:
        for i in range(1, 101, 2):
            if len(li) %2 == 0:
                li.append(i)
                cond.notify()
                cond.wait()
        else:
            cond.notify()



if __name__ == __main__:

    cond = threading.Condition()

    t1 = threading.Thread(target=odd)
    t2 = threading.Thread(target=even)

    t1.start()
    t2.start()
    t1.join()
    t2.join()

    print(li)

 

Event事件锁的应用


 

  有两个线程,如何让他们一人一句对答?文本如下:

 

    杜甫:老李啊,来喝酒!

    李白:老杜啊,不喝了我喝不下了!

    杜甫:老李啊,再来一壶?

    杜甫:...老李?

    李白:呼呼呼...睡着了..

 

import threading


def libai():
    event.wait()  
    print("李白:老杜啊,不喝了我喝不下了!")
    event.set()
    event.clear()
    event.wait()
    print("李白:呼呼呼...睡着了..")

def dufu():
    print("杜甫:老李啊,来喝酒!")
    event.set()  
    event.clear()
    event.wait()
    print("杜甫:老李啊,再来一壶?")
    print("杜甫:...老李?")
    event.set()


if __name__ == __main__:

    event = threading.Event()

    t1 = threading.Thread(target=libai)
    t2 = threading.Thread(target=dufu)

    t1.start()
    t2.start()
    t1.join()
    t2.join()

 

扩展:锁的关系浅析

  这里我们来聊一聊锁的关系。

 

  Rlock递归锁,Condition条件锁,Event事件锁以及Semaphore信号量锁内部都是以同步锁为基础的。

 

  RLock递归锁的实现方式非常简单,因为内部维护着一个计数器。当计数器不为0的时候该线程不能被I/O操作和时间轮询机制切换。但是当计数器为0的时候便不会如此了。

  我们可以看一下递归锁的源码:

def __init__(self):
    self._block = _allocate_lock()
    self._owner = None
    self._count = 0  # 计数器

 

  Condition条件锁的内部其实是有两把锁的,一把底层锁(同步锁)一把高级锁(递归锁)。而低层锁的解锁方式有两种,使用wait()方法会暂时解开底层锁同时加上一把高级锁,只有当接收到别的线程里的notfiy()后才会解开高级锁和重新上锁低层锁.

def __init__(self, lock=None):
    if lock is None:
        lock = RLock()  # 可以看到条件锁的内部是基于递归锁,而递归锁又是基于同步锁来做的
    self._lock = lock

    self.acquire = lock.acquire
    self.release = lock.release
    try:
        self._release_save = lock._release_save
    except AttributeError:
        pass
    try:
        self._acquire_restore = lock._acquire_restore
    except AttributeError:
        pass
    try:
        self._is_owned = lock._is_owned
    except AttributeError:
        pass
    self._waiters = _deque()

 

  Event事件锁内部是基于条件锁来做的。

class Event:

    def __init__(self):
        self._cond = Condition(Lock())  # 实例化出了一个条件锁。
        self._flag = False

    def _reset_internal_locks(self):
        # private!  called by Thread._reset_internal_locks by _after_fork()
        self._cond.__init__(Lock())

    def is_set(self):
        """Return true if and only if the internal flag is true."""
        return self._flag

    isSet = is_set

 

  Semaphore信号量锁内部也是基于条件锁来做的。

class Semaphore:

    def __init__(self, value=1):
        if value < 0:
            raise ValueError("semaphore initial value must be >= 0")
        self._cond = Condition(Lock()) # 可以看到,这里是实例化出了一个条件锁
        self._value = value

 

Python中的线程锁

标签:属性   开启   语句   ret   ica   pytho   获得   val   上线   

原文地址:https://www.cnblogs.com/Yunya-Cnblogs/p/13221502.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!