Python多线程1:threading
threading模块提供了高级别的线程接口,基于低级别的_thread模块实现。
模块基本方法
threading.active_count()
返回当前活跃的Thread对象数量。
返回值和通过enumerate()返回的列表长度是相等的。
threading.current_thread()
返回当前线程对象,相应调用者的控制线程。
假设调用者的控制线程不是通过threading模块创建,一个功能受限的虚拟线程被返回。
threading.get_ident()
返回当前线程的“线程标识符”。
这是一个非0整数,没有特定含义,通经常使用于索引线程特定数据的字典。
线程标识符能够被循环使用。
threading.enumerate()
返回当前活跃的全部线程对象的列表。该列表包含精灵线程、被current_thread()创建的虚拟线程对象、和主线程。
它不包含终止的线程和还没有启动的线程。
threading.main_thread()
返回主线程对象。
在正常情况下,主线程就是Python解释器启动的线程。
threading.settrace(func)
为全部从threading模块启动的线程设置一个trace函数。
在每一个线程的run()方法被调用前。函数将为每一个线程被传递到sys.settrace()。
threading.setprofile(func)
为全部从threading模块启动的线程设置一个profile函数。在每一个线程的run()方法被调用前,函数将为每一个线程被传递到sys.setprofile() 。
threading.stack_size([size])
返回当创建一个新线程是使用的线程栈大小,0表示使用平台或配置的默认值。
平台顶一个常量例如以下:
threading.TIMEOUT_MAX
堵塞函数(Lock.acquire()、RLock.acquire()、Condition.wait()等)的超时參数同意的最大值。指定的值超过该值将抛出OverflowError。
该模块也定义了一些类,在以下会讲到。
该模块的设计是仿照Java的线程模型。然而。Java使lock和condition变量成为每一个对象的基本行为。在Python中则是分离的对象。Python的Thread类支持Java的线程类的行为的一个子集;当前,没有优先级,没有线程组,而且线程不能被销毁、停止、暂停、恢复、或者中断。当实现时,Java的线程类的静态方法被相应到模块级函数。
形容在以下的全部方法都被原子地运行。
线程本地数据
mydata = threading.local()
mydata.x = 1
为不同线程实例的值将是不同的。
class threading.local
表示线程本地数据的类。
很多其它的细节參考_threading_local的文档字符串。
线程对象
一旦一个线程对象被创建,他的行为必须通过线程的start()方法启动。这将在一个独立的控制线程中调用run()方法。
一旦线程的行为被启动,这个线程被觉得是'活跃的'。
正常情况下,当它的run()终止时它推出活跃状态。或者出现为处理的异常。is_alive()方法可用于測试线程是否活跃。
其他线程能调用一个线程的join()方法。
这将堵塞调用线程直到join()方法被调用的线程终止。
一个线程有一个名称,名称能被传递给构造器,并能够通过name属性读取或者改变。
一个线程能被标注为“精灵线程”。
这个标志的意义是当今有精灵线程遗留时,Python程序将退出。初始值从创建的线程继承,这个标志能够通过daemon属性设置,或者通过构造器的daemon參数传入。
注意,精灵线程在关闭时会突然地停止。他们的资源(比如打开的文件、数据库事务等)不能被正确的释放。假设你想你的线程优雅地停止,应该使它们是非精灵线程而且使用一个适当的信号机制,比如Event(后面解说)。
有一个“主线程”对象,这相应到Python程序的初始控制线程,它不是精灵线程。
有可能“虚拟线程对象”被创建,则会存在线程对象相应到“外星人线程”,其控制线程在threading模块之外启动,比如直接从C代码启动。虚拟线程对象的功能是受限的,它们总是被觉得是活跃的精灵线程,不能被join()。
他们不能被删除,因为探測外星人线程的终止是不可能的。
以下是Thread类的构造方法:
class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
1)group:应该是None,保留为未来的扩展,当一个ThreadGroup类被实现的时候须要;
2)target:一个callable对象,被run()方法调用。默觉得None,意味着什么都不做。
3)name:线程名,默认情况下,一个形式为“Thread-N”的唯一名被构造,N是一个小的十进制数;
4)args:调用target的參数元组,默觉得();
5)kwargs:为target调用的參数字典,默觉得{};
6)daemon:假设不为None。则设置该线程是否为精灵线程。假设为None,则从当前线程继承;
假设子类覆盖了构造器。在做其他不论什么事情之前。它必须确保先调用基类的构造器(Thread.__init__())。
线程的经常用法例如以下:
1)start()
启动线程。
每一个线程最多仅仅能调用一次。它会导致对象的run()方法在独立的控制线程中被调用。
假设在同一个线程对象上调用超过一次将抛出RuntimeError。
2)run()
表示线程行为的方法。
你能够在子类中重载该方法。
标准的run()方法调用传入到构造器中callable对象(相应target參数),使用相应的args或者kwargs參数。
3)join(timeout=None)
等待直到线程结束。这将堵塞当前线程直到join()方法被调用的线程终止或者抛出一个未处理的异常,或者设置的溢出时间到达。
假设timeout參数指定且为非None,则它应该是一个浮点数,用于指定操作的溢出时间,单位为秒。因为join()总是返回None,因此在join()结束后你必须调用is_alive()来推断线程是否结束。假设线程任然是活跃的,join()调用则是时间溢出。
当timeout不被指定,或者指定为None时。操作将堵塞直到线程终止。
一个线程能被join()多次。
假设一个join当前线程的尝试将导致一个死锁,join()将抛出RuntimeError。
在一个线程启动之前对该线程做join()操作也将导致相同的异常。
4)name
线程名,仅用于标识一个线程,没有语义,多个线程能够被给相同的名字,初始的名称被构造器设置。
5)getName()
setName()
name的旧的getter/setter API。如今直接使用name属性取代。
6)ident
这个线程的“线程标识符”,假设线程没有启动,则为None。
这是一个非0整数。
线程标识符能够被循环使用,一个线程退出后它的线程标识符能够被其他线程使用。
7)is_alive()
返回线程是否活跃。
该方法仅仅有在run()启动后而且在终止之前才返回True。模块函数enumerate()返回全部活跃线程的一个列表。
8)daemon
一个布尔值。用于表示该线程是否精灵线程。
该值必须在start()方法调用之前设置,否则RuntimeError被抛出。它的初始值从创建的线程继承。主线程不是一个精灵线程,因此全部在主线程中创建的线程daemon默觉得False。
当没有活跃的非精灵线程执行时。整个Python程序退出。
9)isDaemon()
setDaemon()
老的getter/setter API。如今直接使用daemon属性取代。
Lock对象
在Python中。它是当前可用的最低级别的同步基元,通过_thread扩展模块直接实现。
一个基元锁存在两种状态,“锁”或者“未锁”,初始创建时处于未锁状态。
他有两个基本方法:acquire()和release()。当状态是未锁时,acquire()将改变其状态到锁而且马上返回;当状态是锁时,acquire()堵塞直到还有一个线程调用release()释放了锁,然后acquire()获取锁并重设锁的状态到锁而且返回。
release()应该仅仅在锁处理锁状态时才调用,它改变锁的状态到未锁而且马上返回。假设尝试释放一个未锁的锁,一个RuntimeError将被抛出。
锁也支持上下文管理协议。
当超过一个线程被锁堵塞,当锁被释放后仅有一个线程能获取到锁,获取到锁的线程不确定。依赖详细的实现。
相关类例如以下:
class threading.Lock
该类实现了基元锁对象。线程能够通过acquire请求该锁。假设已经存在其他线程获取了锁。则线程堵塞。直到其他线程释放锁。
1)acquire(blocking=True, timeout=-1)
请求一个锁,堵塞或者非堵塞。
当blocking參数为True(默认),将堵塞直到锁被释放。然后获取锁并返回True。
当blocking參数为False,将不堵塞。假设已经存在线程获取了锁,调用将马上返回False。否则。将获取锁并返回True。
当timeout參数大于0时。最多堵塞timeout指定的秒值。
timeout为-1(默认)表示一直等待。当blocking为False时不同意指定timeout參数。
假设锁请求成功。则返回True,否则返回False(比如超时)。
2)release()
释放一个锁。这能在不论什么线程中调用,不仅在获取锁的线程中。
当锁处于锁状态时,重设它为未锁,并返回。其他堵塞等待该锁的线程中将有一个线程能获取到锁。
在一个未锁的锁上调用该方法。将抛出RuntimeError。
无返回值。
RLock对象
为了获取锁,一个线程调用acquire()方法。获取锁后返回;为了释放锁,一个线程调用release()方法。acquire()/release()的调用能够是嵌套的,仅仅有最后的release()重设锁到未锁。
可重入锁也支持上下文管理协议。
class threading.RLock
该类实现了可重入锁对象。一个可重入锁必须被请求它的线程释放,一旦一个线程拥有了一个可重入锁,该线程能够再次请求它,注意请求锁的次数必须和释放锁的次数相应。
注意RLock实际上是一个工厂函数,返回当前平台支持的效率最高的RLock类版本号的一个实例。
1)acquire(blocking=True, timeout=-1)
请求一个锁,堵塞或者非堵塞方式。
当參数为空时:假设这个线程已经拥有锁,递归级别加一,然后返回。否则,假设还有一个线程拥有锁,堵塞直到锁被释放。
假设锁处于未锁状态(不被不论什么线程拥有),则设置拥有者线程。并设置递归级别为1,然后返回。假设超过一个线程处于堵塞等待队列中,一次仅有一个线程能获取锁。
该场景没有返回值。
当blocking为True时,和没有參数的场景同样,并返回True。
当blocking为False时。将不堵塞。假设锁处于锁状态,则马上返回False;否则。和没有參数的场景同样,并返回True。
当timeout參数大于0时。最多堵塞timeout秒。假设在timeout秒内获取了锁,则返回True,否则超时返回False。
2)release()
释放一个锁,降低递归级别。假设递归级别降低到0,则重设锁的状态到未锁(不被不论什么线程拥有)。假设降低后递归级别任然大于0。则锁任然被调用者线程保持。
仅当调用者线程拥有锁时才调用该方法。否则抛出RuntimeError。
没有返回值。
Condition对象
当几个condition变量必须共享同一个锁时传入是实用的。锁是condition对象的一部分:你不必分别跟踪它。
condition变量遵守上下文管理协议:在代码块中用with语句获取相关的锁。acquire()和release()方法也调用相关锁的相应方法。
其他方法必须被相关锁的持有者调用。
wait()方法释放锁,然后堵塞直到还有一个线程调用notify()或者notify_all()唤醒它。唤醒后,wait()又一次获取锁并返回。它也能够指定一个超时时间。
notify()方法唤醒等待线程中的一个;notify_all()方法唤醒全部等待线程。
注意:notify()和notify_all()方法不释放锁;这意味着唤醒的线程或者线程组将不会从wait()调用中马上返回。
使用condition变量的一个典型的应用就是用锁同步对一些共享状态的进入;对某个特定状态感兴趣的线程重复调用wait()。直到出现他们感兴趣的状态。改动这个状态的线程则调用notify()或者notify_all()来通知等待的线程状态已经改变。比如。以下是一个典型的使用了无限缓存的生产者-消费者模式:
# 消费一个条目
with cv:
while not an_item_is_available():
cv.wait()
get_an_available_item() # 产生一个条目
with cv:
make_an_item_available()
cv.notify()
while循环检查是否有条目可用。由于wait()能够在等待随意时间后返回,也有可能调用notify()的线程并没有使条件为真。在多线程编程中这个问题始终存在。wait_for()方法能被用于自己主动的条件检測,简化超时的计算:
# 消费一个条目
with cv:
cv.wait_for(an_item_is_available)
get_an_available_item()
对于notify()和notify_all()。使用哪个在于应用场景中有一个还是多个等待线程。
比如。在一个典型的生产者-消费者场景中。添加一个条目到缓存仅须要唤醒一个消费者线程。
class threading.Condition(lock=None)
该类实现条件变量对象。一个条件变量同意一个或多个线程等待。直到他们被还有一个线程通知。
假设lock參数被指定且不为None。它必须是Lock或者RLock对象,被用做隐含锁。
否则。一个新的RLock对象被创建并作为隐含锁。
1)acquire(*args)
请求一个隐含锁,这种方法会调用隐含锁的相应方法。返回值即为隐含锁的方法的返回值。
2)release()
释放隐含锁。
这种方法调用隐含锁的相应方法。没有返回值。
3)wait(timeout=None)
等待直到被唤醒。或者超时。
假设调用线程没有请求锁。RuntimeError被抛出。
该方法会释放隐含锁,然后堵塞直到它被还有一个线程调用同一个condition对象的notify()或者notify_all()方法唤醒,或者直到指定的timeout时间溢出。一旦唤醒或者超时,它又一次请求锁并返回。
当timeout參数被指定并不为None,则指定了一个秒级的超时时间。
当隐含锁是一个RLock锁,它不通过release()方法释放锁,由于假设线程请求了多次锁,使用release()方法不能解锁(必须调用和lock方法同样的次数才干解锁)。
一个RLock类的内部接口被使用,该接口能释放锁,无论锁请求了多少次。
当锁被请求时,还有一个内部接口被用于还原锁的递归层级。
方法返回True,假设超时则返回False。
4)wait_for(predicate, timeout=None)
等待直到条件为True。
predicate应该是一个callable,返回值为布尔值。
timeout用于指定超时时间。
该方法相当于重复调用wait()直到条件为真,或者超时。返回值是最后的predicate的返回值。或者超时返回Flase。
忽略超时特性,调用这种方法相当于:
while not predicate():
cv.wait()
因此。调用该方法和调用wait()具有相同的规则:调用是或者从堵塞中返回时必须先获取锁。
默认情况下,唤醒一个等待线程。假设调用该方法的线程没有获取锁,则RuntimeError被抛出。
这种方法子多唤醒n(默觉得1)个等待线程;假设没有线程等待。则没有操作。
假设至少n个线程正在等待。当前的实现是刚好唤醒n个线程。
然而,依赖这个行为是不安全的,由于,未来某些优化后的实现可能会唤醒超过n个线程。
注意:一个唤醒的线程仅仅有当请求到锁后才会从wait()调用中返回。因为notify()不释放锁,所以它的调用者应该释放锁。
6)notify_all()
唤醒全部等待线程。这种方法的行为类似于notify(),可是唤醒全部等待线程。假设调用线程未获取锁,则RuntimeError被抛出。
Semaphore对象
semaphore管理一个内部计数,每次调用acquire()时该计数减一,每次调用release()时计数加一。
计数不会小于0。当acquire()发现计数为0时,则堵塞。等待直到其他线程调用release()。
semaphore也支持上下文管理协议。
class threading.Semaphore(value=1)
该类实现semaphore对象。一个semaphore管理一个表示计数表示能并行进入的线程数量。假设计数为0。则acquire()堵塞直到计数大于0。
value默觉得1.
value给出了内部计数的初始值,默觉得1。假设传入的value小于0,则抛出ValueError。
1)acquire(blocking=True, timeout=None)
请求semaphore。
当没有參数时:假设内部计数大于0。将计数减1并马上返回。假设计数为0。堵塞。等待直到还有一个线程调用了release()。这使用互锁机制实现,保证了假设有多个线程调用acquire()堵塞。则release()将仅仅会唤醒一个线程。唤醒的线程是随机选择一个,不依赖堵塞的顺序。
成功返回True(或者无限堵塞)。
假设blocking为False,将不堵塞。假设无法获取semaphore。则马上返回False;否则。同没有參数时的操作,并返回True。
当设置了timeout而且不是None,它将堵塞最多timeout秒。假设在该时间内没有成功获取semaphore,则返回False;否则返回True。
2)release()
释放一个semaphore。计数加1。假设计数初始为0,则须要唤醒等待队列中的一个线程。
class threading.BoundedSemaphore(value=1)
该类实现了有界的semaphore对象。一个有界的semaphore会确保它的当前值没有溢出他的初始值。假设溢出。则ValueError被抛出。
在大部分场景下。semaphore被用于限制资源的使用。
假设semaphore被释放太多次,往往表示出现了bug。
Semaphore使用实例
在启动其它工作线程之前。你的主线程首先初始化Semaphore:
maxconnections = 5
# ...
pool_sema = BoundedSemaphore(value=maxconnections)
其他工作线程则在须要连接到server时调用Semaphore的请求和释放方法:
with pool_sema:
conn = connectdb()
try:
# ... use connection ...
finally:
conn.close()
有个有界的Semaphore能够降低程序出错的机会,防止semaphore释放的次数大于请求次数引发的问题。
Event对象
一个Event对象管理一个内部标志,使用set()方法能够将其设置为True,使用clear()方法能够将其重设为False。wait()方法将堵塞直到标志为True。
class threading.Event
该类实现事件对象。
一个事件管理一个标志,能够通过set()方法将其设置为True,通过clear()方法将其重设为False。wait()方法堵塞直到标志为True。
标志初始为False。
1)is_set()
当且仅当内部标志为True时返回True。
2)set()
设置标志为True。
全部等待的线程都将被唤醒。一旦标志为True。调用wait()的线程将不再堵塞。
3)clear()
重设标志为False。
接下来,全部调用wait()的线程将堵塞直到set()被调用。
4)wait(timeout=None)
堵塞直到标志被设置为True。假设标志已经为True,则马上返回;否则,堵塞直到还有一个线程调用set(),或者直到超时。
当timeout參数被指定且不为None,线程将仅等待timeout的秒数。
该方法当标志为True时返回True,当超时时返回False。
Timer对象
Timer通过调用start()方法启动,通过调用cancel()方法停止(必须在行为被运行之前)。Timer运行指定行为之间的等待时间并非精确的,也就是说可能与用户指定的间隔存在差异。
比如:
def hello():
print("hello, world") t = Timer(30.0, hello)
t.start() # 30秒后,"hello, world"将被打印
class threading.Timer(interval, function, args=None, kwargs=None)
创建一个Timer,在interval秒之后,将使用參数args和kwargs作为參数运行function。假设args为None(默认),将使用空list。假设kwargs是None(默认)。则使用空字典。
1)cancel()
停止定时器。而且取消定时器的行为的运行。这仅当定时器任然处理等待状态时才有效。
Barrier对象
尝试通过栅栏的每一个线程都会调用wait()方法,然后堵塞直到全部的线程都调用了该方法。然后,全部线程同一时候被释放。
栅栏能被反复使用随意多次,但必须是同等数量的线程。
以下是一个样例,一个同步client和服务端线程的简单方法:
b = Barrier(2, timeout=5) def server():
start_server()
b.wait()
while True:
connection = accept_connection()
process_server_connection(connection) def client():
b.wait()
while True:
connection = make_connection()
process_client_connection(connection)
class threading.Barrier(parties, action=None, timeout=None)
为parties个线程创建一个栅栏对象,假设提供了action,则当线程被释放时。它将被线程中的一个调用。timeout表示wait()方法的默认超时时间值。
1)wait(timeout=None)
通过栅栏。当全部使用栅栏的线程都调用了该方法后,他们将被同一时候释放。假设timeout被提供,他优先于类构造器提供的timeout參数。
返回值是0到parties的整数,每一个线程都不同。这能用于选择某个特定的线程做一些特定的操作,比如:
i = barrier.wait()
if i == 0:
# Only one thread needs to print this
print("passed the barrier")
假设一个action被提供给了构造器,线程中的当中一个将在被释放时调用它。假设调用抛出一个异常。则栅栏进入损坏状态。
假设调用超时。则栅栏进入损坏状态。
假设栅栏处于损坏状态,或者有线程在等待时被重设了。则该方法会抛出BrokenBarrierError异常。
2)reset()
恢复栅栏到默认状态。
不论什么处于等待中的线程将收到BrokenBarrierError异常。
注意这里须要额外的同步。假设一个栅栏被损坏,创建一个新的栅栏或许是更好的选择。
3)abort()
放置栅栏到损坏状态。
这导致当前处于等待的线程和未来对wait()的调用都会抛出BrokenBarrierError。通常使用该方法是为了避免死锁。
使用一个超时时间应该是更好的选择。
4)parties
要求通过栅栏的线程的数量。
5)n_waiting
当前处于等待中的线程数量。
6)broken
栅栏是否处于损坏状态,假设是则为True。
exception threading.BrokenBarrierError
该异常是RuntimeError的子类,当栅栏对象被重设或者损坏时被抛出。
在with语句中使用locks、conditions和semaphores
当进入堵塞状态时acquire()方法将被调用,而当推出堵塞状态时release()方法将被调用。以下是详细的语法:
with some_lock:
# do something...
等价于:
some_lock.acquire()
try:
# do something...
finally:
some_lock.release()
当前,Lock、RLock、Condition、Semaphore和BoundedSemaphore都能够在with语句中管理。
Python多线程1:threading的更多相关文章
- Python多线程(threading模块)
线程(thread)是操作系统能够进行运算调度的最小单位.它被包含在进程之中,是进程中的实际运作单位.一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务. ...
- python多线程与threading模块
python多线程与_thread模块 中介绍了线程的基本概念以及_thread模块的简单示例.然而,_thread模块过于简单,使得我们无法用它来准确地控制线程,本文介绍threading模块,它提 ...
- python 多线程并发threading & 任务队列Queue
https://docs.python.org/3.7/library/concurrency.htmlpython程序默认是单线程的,也就是说在前一句语句执行完之前后面的语句不能继续执行先感受一下线 ...
- Python多线程的threading Event
Python threading模块提供Event对象用于线程间通信.它提供了一组.拆除.等待用于线程间通信的其他方法. event它是沟通中最简单的一个过程之中,一个线程产生一个信号,号.Pytho ...
- Python多线程,threading的用法
虫师的文章: 需要注意的是: threads = [ ] t1 = threading.Thread(target=music,args=(u'爱情买卖',)) threads.append(t1) ...
- day-3 python多线程编程知识点汇总
python语言以容易入门,适合应用开发,编程简洁,第三方库多等等诸多优点,并吸引广大编程爱好者.但是也存在一个被熟知的性能瓶颈:python解释器引入GIL锁以后,多CPU场景下,也不再是并行方式运 ...
- python多线程与_thread模块
进程与线程 1.进程:计算机程序只是存储在磁盘中的可执行二进制(或其他类型)的文件.只有把他们加载到内存中并被操作系统调用,才具有其生命周期.进程则是一个执行中的程序.每个进程都拥有自己的地址空间,内 ...
- 人人都能学会的 Python 多线程指南~
大家好鸭!有没有想我~(https://jq.qq.com/?_wv=1027&k=rX9CWKg4) 在 Python 中,多线程最常见的一个场景就是爬虫,例如这样一个需求,有多个结构一样的 ...
- Python的多线程(threading)与多进程(multiprocessing )
进程:程序的一次执行(程序载入内存,系统分配资源运行).每个进程有自己的内存空间,数据栈等,进程之间可以进行通讯,但是不能共享信息. 线程:所有的线程运行在同一个进程中,共享相同的运行环境.每个独立的 ...
- python多线程threading
本文通过 4个example 介绍python中多线程package —— threading的常用用法, 包括调用多线程, 同步队列类Queue, Ctrl+c结束多线程. example1. 调用 ...
随机推荐
- ionic开发环境搭建之ios
前言 公司在做完ionic androud版后就开始做ios版,虽然ios的坑我觉得比起androud少了很多,但是作为第一次接触ios的我来说,环境实在太麻烦,从搭环境到打包一个正式版的ios ap ...
- ipad2 wifi ios7.x 1.0.1还是无法越狱
原话: Warning! We have reports that the iPad 2 (wifi) is not yet compatible with the jailbreak and wil ...
- Hibernate查询语言
HQL(Hibernate Query Language)查询语言是完全面向对象的查询语言,它提供了更加面向对象的封装,它可以理解如多态.继承和关联的概念.HQL看上去和SQL语句相似,但它却提供了更 ...
- meterpreter源码
/* by codeliker @2014.12.08 github: https://github.com/codeliker*/ #include <WinSock2.h> ...
- WinForm特效:同时让两个窗体有激活效果
windows api,一个窗体激活的时候给另外一个发消息 using System; using System.Windows.Forms; using System.Runtime.Interop ...
- 清空iframe的内容
document.getElementById("web").contentWindow.document.body.innerText = "";
- grafana-zabbix图形简单配置
连接zabbix数据库 加入dashboard Home--Add--加入dashboad 设置dashboad 设置名字,和标签tag,tag可在输入后回车加入多个 加入简单的一张图,測试能否获取到 ...
- 机器学习-特征选择 Feature Selection 研究报告
原文:http://www.cnblogs.com/xbinworld/archive/2012/11/27/2791504.html 机器学习-特征选择 Feature Selection 研究报告 ...
- ASP.NET MVC4 Jquer 日期控件 测试范例
<!doctype html> <html lang="en"> <head> <meta charset="utf-8&q ...
- 微信小程序 - 五星评分(含半分)
转载自:http://blog.csdn.net/column/details/13721.html 演示: 下载:小程序-星级评论.zip