threading– 基于线程并行性

源代码: Lib / threading.py


这个模块在低级_thread之上构造更高级别的线程接口模块。另见queue module.

更改版本3.7:此模块以前是可选的,现在总是可用的.

注意

虽然它们未在下面列出,但camelCase此模块仍然支持Python 2.x系列中此模块中某些方法和函数使用的名称.

该模块定义了以下函数:

threading.active_count

返回当前活着的Thread对象的数量。returnscount等于enumerate().

threading.current_thread

返回的列表长度//返回当前Thread对象,对应于调用者的控制线程。如果调用者的控制线程不是通过threading模块创建的,则返回功能有限的虚拟线程对象.

threading.get_ident

返回当前线程的“线程标识符”。这是一个非零整数。它的价值没有直接意义;它旨在作为一个神奇的cookie用于例如索引特定于线程的数据的字典。当线程退出并创建另一个线程时,可以回收线程标识符.

3.3版本中的新功能

threading.enumerate// ()

返回所有Thread目前活着的物品的清单。该列表包括守护线程,由current_thread(),和主线程。它排除了尚未启动的终止线程和线程.

threading.main_thread ()

返回主要Thread宾语。在正常情况下,主线程是启动Python解释器的线程.

新版本3.4.

threading.settrace (func)

为从threading模块启动的所有线程设置跟踪函数。func将在调用sys.settrace()方法之前传递给run()每个线程.

threading.setprofile(func)

threadingfunc方法被调用之前,sys.setprofile()将被传递到run()每个线程.

threading.stack_size ( [size]

返回创建新线程时使用的线程堆栈大小。可选的size参数指定用于随后创建的线程的堆栈大小,并且必须为0(使用平台或配置的默认值)或者至少为32,768(32 KiB)的positiveinteger值。如果未指定size,则使用0。如果不支持更改线程堆栈大小,则引发RuntimeError。如果指定的堆栈大小无效,则ValueError被提升,堆栈大小未经修改。32 KiBis目前是支持的最小堆栈大小值,以保证解释器本身有足够的堆栈空间。注意,一些平台可能对堆栈大小的值具有特定限制,例如要求最小堆栈大小>32 KiB或要求分配系统内存页面大小的倍数 – 平台文档应参考更多信息(4 KiB页面是常见的;在没有更具体信息的情况下,使用4096的倍数表示堆栈大小是建议的方法).

可用性:Windows,带有POSIX线程的系统

这个模块还定义了以下常量:

threading.TIMEOUT_MAX

timeout阻塞参数允许的最大值功能(Lock.acquire(), RLock.acquire(), Condition.wait()等等。)。指定超过此值的超时将提高OverflowError.

版本3.2.

这个模块定义了许多类,详见下面的部分.

该模块的设计基于Java的线程模型。但是,在Java使锁和条件变量成为每个对象的基本行为的地方,它们是Python中的独立对象。Python的Thread类支持Java的Thread类行为的asubset;目前,有nopriorities,没有线程组,线程不能被销毁,停止,暂停,恢复或中断。Java的Thread类的静态方法在实现时被映射到模块级函数.

下面描述的所有方法都是原子执行的.

Thread-Local Data

线程局部数据是其值是线程特定的数据。要管理线程本地数据,只需创建一个local(或asubclass)实例并在其上存储属性:

mydata = threading.local()mydata.x = 1

实例的值对于单独的线程是不同的

class threading.local

A表示线程本地数据的类.

有关详细信息和大量示例,请参阅_threading_local module.

Thread Objects

Threadclass表示在单独的控件线程中运行的活动。有两种方法可以指定活动:将acallable对象传递给构造函数,或者通过覆盖run()子类中的方法。应该在子类中重写其他方法(构造函数除外)。换句话说,only覆盖了这个类的__init__()run()方法.

一旦创建了一个线程对象,就必须通过调用线程的start()方法来启动它的活动。这将在一个单独的控制线程中调用run()方法.

一旦线程的活动开始,线程就被认为是“活着的”。当它run()方法终止时,它停止运行 – 正常情况下,或者通过引发未处理的异常。is_alive()方法测试线程是否处于活动状态.

其他线程可以调用线程的join()方法。这会阻塞调用线程,直到调用了join()方法的线程为止

线程有一个名字。名称可以传递给构造函数,并通过name属性读取或更改

一个线程可以被标记为“守护程序线程”。这个标志的意义在于当只剩下守护进程线程时整个Python程序退出。初始值继承自创建线程。可以通过daemon属性或daemon constructorargument.

Note

设置标志在停机时突然停止。他们的资源(例如打开文件,数据库事务等)可能无法正常发布。如果您希望线程优雅地停止,请使它们成为非守护进程并使用合适的信令机制,例如Event.

“主线”对象;这对应于Python程序中的初始控制线程。它不是守护线程

有可能创建“虚拟线程对象”。这些是与“外部线程”相对应的线程对象,它们是在线程模块外部开始的控制线程,例如直接来自C代码。Dummythread对象具有有限的功能;他们总是被认为是活着的,而且不可能是join()编辑。它们永远不会被删除,因为不可能检测到外星线程的终止.

class threading.Threadgroup=None, target=None, name=None, args=(), kwargs={}, *, daemon=None

应始终使用关键字参数调用此构造函数。Argumentsare:

group应该 None;当ThreadGroup类被实现时保留用于将来的扩展.

targetrun()方法调用的可调用对象。默认为None,意思是什么都没有叫做。

name是线程名称。默认情况下,唯一的名称由表单“Thread – N”构成,其中N是一个小的十进制数.

args是目标调用的参数元组。默认为().

kwargs是一个关键字参数的字典,用于目标调用。默认为{}.

如果不是None, daemon显式设置线程是否为守护进程。如果None(默认值),守护进程属性从当前线程继承

如果子类重写构造函数,它必须确保在对线程执行任何其他操作之前调用基类构造函数(Thread.__init__()

更改在版本3.3:添加了daemon参数.

start (

开始线程的活动.

每个线程对象最多必须调用一次。它安排对象的run()方法在一个单独的控制线程中调用.

如果在同一个线程对象上调用多次,这个方法会引发一个RuntimeError

run

表示线程活动的方法.

你可以在子类中覆盖这个方法。标准run()方法调用传递给对象构造函数的可调用对象target参数,如果有的话,分别来自argskwargs参数的顺序和关键字参数.

join (timeout=None )

等到线程终止。这会阻塞调用线程直到其join()方法被称为终止 – 正常或通过未处理的异常 – 或直到可选的timeout发生.

timeout参数存在而不是None,它应该是指定操作超时的浮点数,以秒为单位(或其分数)。因为join()总是返回None,你必须在is_alive()之后调用join()判断是否发生超时 – 如果线程仍然存在,则join()呼叫超时.

timeout参数不存在时或None,操作将阻塞,直到线程终止.

一个帖子可以join()很多次.

join()提起RuntimeError如果尝试加入当前线程,那将导致死锁。join()一个线程在它被启动之前并尝试这样做会引发同样的异常.

name

一个字符串仅用于识别目的。它没有语义。多个线程可以赋予相同的名称。初始名称由构造函数设置.

getName()
setName()

旧的getter / setter API name;请直接使用它作为aproperty .

ident

这个线程的’线程标识符’或None如果线程尚未启动。这是一个非零整数。见get_ident()功能。当线程退出并创建另一个线程时,线程标识符可以被回收。即使在线程退出后,该标识符也可用.

is_alive)

返回线程是否还活着

这个方法返回True就在run()方法启动之前,直到run()方法终止之后。模块函数enumerate()返回所有活动线程的列表

daemon

一个布尔值,表示该线程是否是守护线程(True)或不是(False)。这必须在start()被调用,否则RuntimeError被提出。它的初始值是从创建线程继承而来的;主线程不是守护程序线程,因此在主线程中创建的所有线程默认为daemon = False.

当没有剩下活着的非守护程序线程时,整个Python程序退出.

isDaemon
setDaemon

旧的getter / setter API for daemon;代替它直接使用它

CPython实现细节:在CPython中,由于全局解释器锁,只有一个线程可以执行Python一次代码(即使某些面向性能的库可能会克服这个限制)。如果您希望应用程序更好地利用多核机器的计算资源,建议您使用multiprocessingconcurrent.futures.ProcessPoolExecutor.但是,如果你想同时运行多个I / O绑定任务,线程仍然是一个合适的模型.

锁定对象

原始锁是一种同步原语,在锁定时不属于特定线程。在Python中,它是目前可用的最低级别同步原语,由_thread扩展模块

原始锁定处于“锁定”或“解锁”两种状态之一。它是在解锁状态下创建的。它有两个基本方法,acquire()release()。当国家解锁时,acquire()将状态更改为锁定并立即返回。当状态被锁定时,acquire()阻塞,直到调用release()在anotherthread中将其改为解锁,然后acquire()调用将其重置为已锁定并返回。release()方法只应在锁定状态下调用;它将状态更改为解锁并立即返回。如果试图释放解锁的锁,将RuntimeError抬起来

锁也支持上下文管理协议.

acquire()等待状态转为解锁,只有一个线程在release()呼叫重置状态解锁;其中一个等待线程没有定义,并且可能因实现而异.

所有方法都是原子地执行的.

class threading.Lock

实现原始锁定对象的类。一旦线程获得了alock,后续尝试获取它就会阻塞,直到它被释放;anythread可能会释放它

注意Lock实际上是一个工厂函数,它返回平台支持的具体Lock类的最有效版本的实例.

acquire (blocking=True, timeout=-1 )

获取锁定,阻塞或非阻塞.

blocking参数设置为True(默认值)时调用,阻塞直到锁定解锁,然后设置它锁定并返回True.

当调用blocking参数设置为False时,不要阻塞。如果blocking设置为True的调用会阻塞,则返回False立即;否则,将锁定为锁定并返回True.

当浮点timeout参数设置为positivevalue调用时,最多阻止timeout指定的秒数,并且只要无法获取锁定。timeout -1的参数指定无限制的等待。当timeout为false时,禁止指定blocking

如果成功获取锁定,则返回值为TrueFalse如果没有(例如,如果timeout已过期).

在版本3.2中更改: timeout参数是新的.

在版本3.2中更改:如果底层线程实现支持,则可以通过POSIX上的信号中断锁定获取.

release()

Release锁。这可以从任何线程调用,而不仅仅是已经获得锁定的线程.

当锁被锁定时,将其重置为解锁状态,然后返回。如果任何其他线程被阻塞等待锁定被解锁,则允许它们中的一个继续进行.

当在解锁的锁上调用时,RuntimeError被抬起

没有返回值.

RLock对象

可重入锁是一个同步原语,可以由同一个线程多次获取。在内部,除了原始锁使用的锁定/解锁状态之外,它还使用“拥有线程”和“递归级别”的概念。在锁定状态下,某些线程拥有锁;在解锁状态下,没有线程拥有它.

为了锁定锁,一个线程调用它acquire()方法;一旦线程拥有锁,这就会返回。要解锁锁,一个线程调用release()方法。acquire()/release()呼叫对可以嵌套;只有最后的release()(最外面一对的release())才能将锁重置为解锁状态,并允许在acquire()中阻止另一个线程继续进行.

同时锁也是支持上下文管理协议.

class threading.RLock

此类实现可重入锁定对象。必须由获取它的线程发布重入锁。一旦线程获得了意义锁定,同一个线程可以再次获取它而不会阻塞;每次获取它时,线程必须释放一次.

注意RLock实际上是一个工厂函数,它返回平台支持的具体RLock类的最高效版本的实例。.

acquire (blocking=True, timeout=-1)

锁定,阻塞或无阻塞.

在不带参数的情况下调用:如果此线程已拥有锁,则将递归级别递增1,并立即返回。否则,如果anotherthread拥有锁,则阻止直到锁被解锁。一旦锁被解锁(不是由任何线程拥有),然后获取所有权,将递归级别设置为1,然后返回。如果多个线程被阻塞等待直到lockis解锁,那么一次只能有一个线程获取锁的所有权。在这种情况下没有返回值.

blocking参数设置为true的情况下调用时,执行与不带参数调用时相同的操作,并返回true.

当使用blocking参数设置为false,不要阻塞。如果没有参数的call会阻塞,立即返回false;否则,在没有参数的情况下做同样的事情,并返回true .

当浮点timeout参数设置为positivevalue调用时,最多阻止timeout并且只要无法获得锁定。如果已获取锁定则返回true,如果超时已经过,则返回false。

在版本3.2中更改: timeout参数是新的.

release

释放锁定,递减递归级别。如果在减量之后它是零,则将锁重置为解锁(不是由任何线程拥有),并且如果任何其他线程被阻止等待锁被解锁,则允许其中的一个继续进行。如果在递减之后递归级别仍为非零,则锁保持锁定并由调用线程拥有.

仅在调用线程拥有锁时调用此方法。一个RuntimeError如果在锁定被解锁时调用此方法,则会引发此问题.

没有回报价值

条件对象

条件变量总是与某种锁相关联;这可以通过,或者默认情况下会创建一个。当几个条件变量必须共享同一个锁时,传入一个是有用的。锁是条件对象的一部分:你不必单独跟踪它

条件变量服从上下文管理协议:使用with语句在封闭块的持续时间内获取关联的锁。acquire()release()方法也调用相关锁的相应方法.

其他方法必须在保持相关锁的情况下调用。wait()方法释放锁,然后阻止untilaother线程通过调用notify()notify_all()唤醒它。一旦被唤醒,wait()重新获得锁并返回。也可以指定超时.

notify()方法唤醒其中一个等待条件变量的线程,如果有的话等待。notify_all()唤醒等待条件变量的所有线程.

注意:notify()notify_all()方法不释放锁;这意味着唤醒的一个或多个线程不会立即从它们的wait()调用返回,但只有当调用notify()notify_all()的线程最终放弃锁定的所有权时才会返回

使用条件变量的典型编程风格使用lock tosynchronize访问某些共享状态;对状态变化感兴趣的线程重复调用wait()直到它们看到所需状态,而修改状态的线程调用notify()notify_all()当它们改变状态时它可能是一个服务员的理想状态。例如,以下代码是具有无限缓冲容量的通用生产者 – 消费者情况:

# Consume one itemwith cv:    while not an_item_is_available():        cv.wait()    get_an_available_item()# Produce one itemwith cv:    make_an_item_available()    cv.notify()

while循环检查应用程序的条件是必要的,因为wait()可以在任意长时间后返回时间,以及促使notify()电话可能不再适用。这是多线程编程所固有的。wait_for()方法可用于自动化条件检查,并简化超时计算:

# Consume an itemwith cv:    cv.wait_for(an_item_is_available)    get_an_available_item()

notify()notify_all(),考虑一个状态更改是否只对一个或几个等待线程感兴趣。例如。在典型的生产者 – 消费者情况下,向缓冲区添加oneitem只需唤醒一个消费者线程

class threading.Conditionlock=None

这个类实现了条件变量对象。条件变量允许一个或多个线程等待,直到另一个线程通知它们

如果lock参数给出而不是None,它必须是LockRLock对象,并且它被用作底层锁。否则,创建一个新的RLock对象并将其用作底层锁.

在版本3.3中更改:从工厂功能更改为类

acquire// (*args)

获取底层锁。此方法在底层锁上调用相应的方法;返回值是该方法返回的任何值.

release ( )

释放底层锁。此方法在底层锁上调用相应的方法;没有回报价值

wait// (timeout=None)

等到通知或直到发生超时。如果在调用此方法时调用线程没有获取锁定,则RuntimeError israised.

此方法释放底层锁,然后阻塞,直到被notify()notify_all()在另一个线程中调用相同的条件变量,或直到可选的timeoutoccurs。一旦被唤醒或超时,它就会重新获得锁并返回

timeout参数存在而不是None,它应该是指定操作超时的浮点数,以秒为单位(或其中的分数).

当底层锁是RLock,它没有发布使用release()方法,因为当它被递归多次获取时,这实际上可能无法解锁。相反,使用RLock类的内部接口,当它被递归地多次获取时,它实际上解锁了迭代。然后使用另一个内部接口来恢复锁定时的递归级别.

返回值为True除非给定的timeout已过期,在这种情况下它是False.

在版本3.2中更改:以前,该方法始终返回None.

wait_forpredicate, timeout=None

等待条件计算结果为true。predicate应该是可以接受的,结果将被解释为一个布尔值。可以提供一个timeout给出最长的等待时间.

这个实用方法可以调用wait()重复直到谓词满意,或直到超时发生。返回值是谓词的最后一个返回值,如果方法超时,将评估为False

忽略超时功能,调用此方法大致相当于:

while not predicate():    cv.wait()

因此,同样的规则适用于wait():锁定必须在调用时保留,并在返回时重新获取。锁定时对谓词进行评估.

新版本3.2.

notify(n=1)

默认情况下,唤醒一个等待此情况的线程,如果有的话。如果调用此方法时调用线程没有获取锁定,则会引发RuntimeError

这个方法最多唤醒等待条件变量的线程n;如果没有线程在等待,它是一个无操作.

当前的实现正好n线程唤醒,如果至少n线程在等待。然而,依赖于这种行为是不安全的。未来,优化的实现可能偶尔会唤醒n threads.

注意:一个唤醒的线程实际上并没有从它返回wait()调用直到它可以重新获取锁定。由于notify()不发布锁,其来电者应该

notify_all//(

唤醒等待这种情况的所有线程。这种方法就像notify(),但唤醒所有等待的线程而不是一个。如果在调用此方法时调用线程没有获取锁定,则会引发RuntimeError

//信号对象

这是早期荷兰计算机科学家Edsger W. Dijkstra发明的计算机科学史上最古老的同步原语之一(他使用了P()V()而不是acquire()release()).

一个信号量管理一个内部计数器,每个acquire()调用递减并且每个递增release()呼叫。计数器永远不会低于零;什么时候 acquire()发现它为零,它阻塞,等待其他线程调用release().

信号量也支持上下文管理协议.

class threading.Semaphorevalue=1

该类实现信号量对象。信号量管理一个原子计数器,表示release()呼叫数减去acquire()呼叫数加上初始值。acquire()如果必要的话,方法块可以返回而不会使计数器为负。如果没有给出,value默认为1.

可选参数给出内部计数器的初始值value;它默认为1。如果给出的value小于0,则ValueError israised.

在版本3.3中更改:从工厂功能改为班级

acquireblocking=True, timeout=None

获取信号量

当没有参数调用时:

  • 如果内部计数器在进入时大于零,减去一个并立即返回真实.
  • 如果内部计数器在进入时为零,则阻止直到通过调用release()唤醒。一旦唤醒(并且计数器大于0),将计数器递减1并返回true。每次调用release()都会唤醒完全onethread。不应该依赖线程被唤醒的顺序.

blocking设置为false,不要阻止。如果没有参数的call会阻塞,立即返回false;否则,与没有参数调用时相同,并返回true.

当用timeout调用而不是None时,它会阻止至少timeout秒。如果获取未在该间隔内成功完成,则返回false。否则返回原来.

在版本3.2中更改: timeout参数是新的.

release ()

释放信号量,将内部计数器递增1。当它在进入时为零并且另一个线程正在等待它再次变为大于零时,唤醒该线程

class threading.BoundedSemaphore// (value=1)

实现有界信号量对象的类。有界信号量检查以确保其当前值不超过其初始值。如果是,则ValueError被提出。在大多数情况下,信号量用于保护容量有限的资源。如果信号量被释放太多次,则表示存在错误。如果没有给出,value默认为1.

在版本3.3中更改:从工厂函数更改为类

Semaphore示例

信号量通常用于保护容量有限的资源,例如数据库服务器。在资源大小固定的任何情况下,您应该使用有界信号量。在产生任何工作线程之前,你的主线程会初始化信号量:

maxconnections = 5# ...pool_sema = BoundedSemaphore(value=maxconnections)

一旦产生,工作线程在需要连接到服务器时调用信号量的获取和释放方法:

with pool_sema:    conn = connectdb()    try:        # ... use connection ...    finally:        conn.close()

使用有界线程信号量减少了一个编程错误,这个编程错误导致信号量被释放的次数超过它所获得的信号将被检测不到.

事件对象

这是线程之间最简单的通信机制之一:onreadread信号一个事件和其他线程等待它.

一个事件对象管理一个内部标志,可以用set()方法设置为true,并用clear()方法重置为false。wait()方法阻止,直到标志为真.

class threading.Event

实现事件对象的类。事件管理一个标志,可以使用set()方法设置为totrue,并使用clear()方法重置为false。wait()方法阻塞,直到标志为真。标志最初为假.

在版本3.3中更改:从工厂功能更改为类.

is_set

当且仅当内部标志为真时才返回true.

set()

将内部标志设置为true。等待它变为真实的所有线程都被唤醒。调用wait()一旦旗帜真实,就不会阻挡.

clear ()

将内部旗帜重置为假。随后,调用wait()的线程将阻塞,直到set()被调用再次将内部标志设置为真.

wait (timeout=None )

阻止,直到内部标志为真。如果内部标志为真,则立即返回。否则,阻塞直到另一个线程调用set()将标志设置为true,或者直到可选的超时发生.

超时参数出现而不是None,它应该是浮点数指定操作的超时(以秒为单位)。或者它的分数.

当且仅当内部标志已设置为totrue时,无论是在等待调用之前还是在等待开始之后,此方法都返回true,因此willalways将返回True除非给出超时并且操作时间结束了

在版本3.1:更改以前,该方法总是返回None.

计时器对象

此类表示应该在经过一定时间后运行的操作 – 计时器。TimerThread因此也可以作为创建自定义线程的一个例子.

启动计时器,就像线程一样,通过调用它们start()方法。可以通过调用cancel()方法。计时器在执行其动作之前等待的间隔可能与用户指定的间隔不完全相同.

例如:

def hello():    print("hello, world")t = Timer(30.0, hello)t.start()  # after 30 seconds, "hello, world" will be printed
class threading.Timerinterval, function, args=None, kwargs=None

创建一个将运行的计时器function带有参数args和keywordarguments kwargs,经过interval秒后。如果argsNone(默认值)则为空列表将被使用。如果kwargsNone(默认)那么将使用一个空的字典.

在版本3.3中改变:从一个改变工厂功能到一个类.

cancel()

停止计时器,取消执行计时器的动作。如果计时器仍处于等待阶段,这将起作用.

Barrier Objects

版本3.2.

中的新功能提供了一个简单的同步原语供固定使用需要等待的线程数。每个线程都试图通过调用wait()方法来屏障屏障,并且会阻止线程的不连续调用wait()调用。此时,线程同时被释放

对于相同数量的线程,屏障可以重复使用任意次数.

例如,这是一种简单的同步方法客户端和服务器线程:

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.Barrierparties, action=None, timeout=None

parties线程数创建一个屏障对象。action,当提供时,是一个可调用的,当它们被释放时被其中一个线程调用。timeout如果没有指定wait()方法

wait// (timeout=None

通过障碍。当屏障的所有线程都调用了这个函数时,它们都被同时释放。如果一个 timeout提供,它优先于提供给classconstructor的任何东西使用.

返回值是0到parties– 1,每个线程不同。这可以用来选择一个线程做一些专门的保管,例如:

i = barrier.wait()if i == 0:    # Only one thread needs to print this    print("passed the barrier")

如果action被提供给构造函数,其中一个线程将在被释放之前调用它。如果此调用出现错误,屏障就会进入破坏状态.

如果呼叫超时,屏障就会进入破坏状态.

如果在线程等待时屏障被破坏或重置,这种方法可能会引发BrokenBarrierError异常.

reset

将屏障恢复为默认的空状态。等待它的任何线程都会收到BrokenBarrierError例外.

请注意,如果存在状态未知的其他线程,则使用此函数可能需要一些外部同步。如果abarrier被打破,最好离开它并创建一个新的.

abort

将屏障置于破碎的状态。这导致任何活动或未来调用wait()失败BrokenBarrierError。使用这个例如,如果其中一个需要中止,以避免死锁应用.

可能最好简单地创造一个合理的屏障timeout自动防护其中一个线程的值.

parties

通过障碍物所需的线数

n_waiting

目前在障碍物中等待的线数

broken

一个布尔是True如果屏障处于破碎状态

exception threading.BrokenBarrierError

这个例外,是RuntimeError,当Barrier对象被重置或损坏时被引发

//在with语句中使用锁,条件和信号量

提供的所有对象通过这个acquire()release()方法的模块可以用作with语句的上下文管理器。当块被加入时将调用acquire()方法,并且当块退出时将调用release()。因此,下面的代码片段:

with some_lock:    # do something...

相当于:

some_lock.acquire()try:    # do something...finally:    some_lock.release()

目前,Lock, RLock, Condition,SemaphoreBoundedSemaphore对象可以用作with语句上下文管理器