python中的线程相关概念简单梳理

1.简单启动一个线程

python标准库提供了threading模块,启动一个线程就是把一个函数传给Thread实例,再调用start()运行起来

import threading
import time

def loop():
    print(f'----------thread :{threading.current_thread().name} is running...')
    n = 0
    while n<5:
        n = n+1
        print(f'thread: {threading.current_thread().name} n: {n}')
        time.sleep(1)
    print(f'----------thread: {threading.current_thread().name} ended')

print(f'thread : {threading.current_thread().name}')
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print(f'thread : {threading.current_thread().name} ended')

输出:

thread : MainThread
----------thread :LoopThread is running...
thread: LoopThread n: 1
thread: LoopThread n: 2
thread: LoopThread n: 3
thread: LoopThread n: 4
thread: LoopThread n: 5
----------thread: LoopThread ended
thread : MainThread ended

任何进程都会默认启动一个线程,我们把这个默认线程叫做主线程MainThread,主线程可以启动新的子线程,子线程的名字可以在创建的时候指定LoopThread,threading模块中的current_thread()函数永远返回当前线程的实例

2.锁的概念

锁是Python的threading模块提供的最基本的同步机制。在任一时刻,一个锁对象可能被一个线程获取,或者不被任何线程获取。如果一个线程尝试去获取一个已经被另一个线程获取到的锁对象,那么这个想要获取锁对象的线程只能暂时终止执行直到锁对象被另一个线程释放掉。
锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁

import threading

# 创建一个锁
lock = threading.Lock()

def run_thread(n):
    # 先获取锁在干活,多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,其他线程继续等待直到获得锁为止才能继续执行
    lock.acquire()
    try:
        # 干活,内容省略
        pass
    finally:
        # 干完活一定要释放掉锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程
        lock.release()

在Python 2.5及以后的版本中,可以使用with语句。在使用锁的时候,with语句会在进入语句块之前自动的获取到该锁对象,然后在语句块执行完成后自动释放掉锁:

import threading

lock = threading.Lock()

def run_thread(n):
    with lock:
        pass

acquire方法带一个可选的等待标识,它可用于设定当有其它线程占有锁时是否阻塞。如果你将其值设为False,那么acquire方法将不再阻塞,只是如果该锁被占有时它会返回False。
可以使用locked方法来检查一个锁对象是否已被获取,注意不能用该方法来判断调用acquire方法时是否会阻塞,因为在locked方法调用完成到下一条语句(比如acquire)执行之间该锁有可能被其它线程占有。

import threading

lock = threading.Lock()

if not lock.locked():
    #: 其它线程可能在下一条语句执行之前占有了该锁
    lock.acquire()  #: 可能会阻塞

3.简单锁的缺点

标准的锁对象并不关心当前是哪个线程占有了该锁;如果该锁已经被占有了,那么任何其它尝试获取该锁的线程都会被阻塞,即使是占有锁的这个线程。考虑一下下面这个例子:

lock = threading.Lock()

def get_first_part():
    lock.acquire()
    try:
        ... 从共享对象中获取第一部分数据
    finally:
        lock.release()
    return data

def get_second_part():
    lock.acquire()
    try:
        ... 从共享对象中获取第二部分数据
    finally:
        lock.release()
    return data

示例中,我们有一个共享资源,有两个分别取这个共享资源第一部分和第二部分的函数。两个访问函数都使用了锁来确保在获取数据时没有其它线程修改对应的共享数据。
现在,如果我们想添加第三个函数来获取两个部分的数据,我们将会陷入泥潭。一个简单的方法是依次调用这两个函数,然后返回结合的结果:

def get_both_parts():
    first = get_first_part()
    seconde = get_second_part()
    return first, second

这里的问题是,如有某个线程在两个函数调用之间修改了共享资源,那么我们最终会得到不一致的数据。最明显的解决方法是在这个函数中也使用lock:

    def get_both_parts():
        lock.acquire()
        try:
            first = get_first_part()
            seconde = get_second_part()
        finally:
            lock.release()
        return first, second

然而,这是不可行的。里面的两个访问函数将会阻塞,因为外层语句已经占有了该锁。为了解决这个问题,你可以通过使用标记在访问函数中让外层语句释放锁,但这样容易失去控制并导致出错。幸运的是,threading模块包含了一个更加实用的锁实现:re-entrant锁。

4.Re-Entrant Locks (RLock)

RLock类是简单锁的另一个版本,它的特点在于,同一个锁对象只有在被其它的线程占有时尝试获取才会发生阻塞;而简单锁在同一个线程中同时只能被占有一次。如果当前线程已经占有了某个RLock锁对象,那么当前线程仍能再次获取到该RLock锁对象。

import threading

lock = threading.RLock()
lock.acquire()
lock.acquire()  #: 这里不会发生阻塞
print(222222222)

lock = threading.Lock()
lock.acquire()
lock.acquire()  #: 这里将会阻塞
print(1111111111)

输出:

222222222

RLock的主要作用是解决嵌套访问共享资源的问题,就像前面描述的示例。要想解决前面示例中的问题,我们只需要将Lock换为RLock对象,这样嵌套调用也会OK.

lock = threading.RLock()


def get_first_part():
    ... see above


def get_second_part():
    ... see above


def get_both_parts():
    ... see above

这样既可以单独访问两部分数据也可以一次访问两部分数据而不会被锁阻塞或者获得不一致的数据。

注意RLock会追踪递归层级,因此记得在acquire后进行release操作。

5.信号量概念/Semaphores

信号量是一个更高级的锁机制。信号量内部有一个计数器而不像锁对象内部有锁标识,而且只有当占用信号量的线程数超过信号量时线程才阻塞。这允许了多个线程可以同时访问相同的代码区。

semaphore = threading.BoundedSemaphore()
semaphore.acquire()  #: counter减小
... 访问共享资源
semaphore.release()  #: counter增大

当信号量被获取的时候,计数器减小;当信号量被释放的时候,计数器增大。当获取信号量的时候,如果计数器值为0,则该进程将阻塞。当某一信号量被释放,counter值增加为1时,被阻塞的线程(如果有的话)中会有一个得以继续运行。
信号量通常被用来限制对容量有限的资源的访问,比如一个网络连接或者数据库服务器。在这类场景中,只需要将计数器初始化为最大值,信号量的实现将为你完成剩下的事情。

max_connections = 10
semaphore = threading.BoundedSemaphore(max_connections)

如果你不传任何初始化参数,计数器的值会被初始化为1.
Python的threading模块提供了两种信号量实现。Semaphore类提供了一个无限大小的信号量,你可以调用release任意次来增大计数器的值。为了避免错误出现,最好使用BoundedSemaphore类,这样当你调用release的次数大于acquire次数时程序会出错提醒。

6.事件概念/Events

一个事件是一个简单的同步对象,事件表示为一个内部标识(internal flag),线程等待这个标识被其它线程设定,或者自己设定、清除这个标识。

event = threading.Event()

#: 一个客户端线程等待flag被设定
event.wait()

#: 服务端线程设置或者清除flag
event.set()
event.clear()

一旦标识被设定,wait方法就不做任何处理(不会阻塞),当标识被清除时,wait将被阻塞直至其被重新设定。任意数量的线程可能会等待同一个事件。

7 .条件概念/Conditions

条件是事件对象的高级版本。条件表现为程序中的某种状态改变,线程可以等待给定条件或者条件发生的信号。
下面是一个简单的生产者/消费者实例。首先你需要创建一个条件对象:

#: 表示一个资源的附属项
condition = threading.Condition()

生产者线程在通知消费者线程有新生成资源之前需要获得条件:

#: 生产者线程
... 生产资源项
condition.acquire()
... 将资源项添加到资源中
condition.notify()  #: 发出有可用资源的信号
condition.release()

消费者必须获取条件(以及相关联的锁),然后尝试从资源中获取资源项:

#: 消费者线程
condition.acquire()
while True:
    ...从资源中获取资源项
    if item:
        break
    condition.wait()  #: 休眠,直至有新的资源
condition.release()
... 处理资源

wait方法释放了锁,然后将当前线程阻塞,直到有其它线程调用了同一条件对象的notify或者notifyAll方法,然后又重新拿到锁。如果同时有多个线程在等待,那么notify方法只会唤醒其中的一个线程,而notifyAll则会唤醒全部线程。
为了避免在wait方法处阻塞,你可以传入一个超时参数,一个以秒为单位的浮点数。如果设置了超时参数,wait将会在指定时间返回,即使notify没被调用。一旦使用了超时,你必须检查资源来确定发生了什么。
注意,条件对象关联着一个锁,你必须在访问条件之前获取这个锁;同样的,你必须在完成对条件的访问时释放这个锁。在生产代码中,你应该使用try-finally或者with.
可以通过将锁对象作为条件构造函数的参数来让条件关联一个已经存在的锁,这可以实现多个条件公用一个资源:

lock = threading.RLock()
condition_1 = threading.Condition(lock)
condition_2 = threading.Condition(lock)

8.ThreadLocal的概念

多线程环境下,每一个线程均可以使用所属进程的全局变量。如果一个线程对全局变量进行了修改,将会影响到其他所有的线程。为了避免多个线程同时对变量进行修改,引入了线程同步机制,通过互斥锁,条件变量或者读写锁来控制对全局变量的访问。
只用全局变量并不能满足多线程环境的需求,很多时候线程还需要拥有自己的私有数据,这些数据对于其他线程来说不可见。因此线程中也可以使用局部变量,局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问。
有时候使用局部变量不太方便,因此 python 还提供了 ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的,真正做到了线程之间的数据隔离。

import threading

global_data = threading.local()

def thread_call():
    global_data.num = 0
    for _ in range(1000):
        global_data.num +=1
    print(f'thread : {threading.current_thread().name}, {global_data.num}')
    
threads = []
...

每个线程都可以通过 global_data.num 获得自己独有的数据,并且每个线程读取到的 global_data 都不同,真正做到线程之间的隔离

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值