Python—threading模块

threading

CPU密集型与I/O密集型

  • 计算密集型:CPU密集型也叫计算密集型,要进行大量的计算,消耗CPU资源(CPU占用率高)。
  • I/O密集型:CPU占用率低,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。

GIL

CPython下,因为 Global Interpreter Lock(GIL),一个时刻只有一个线程可以执行Python代码(尽管如此,某些性能导向的库可能会克服这个限制)。如果你想让你的应用更好的利用多核计算机的计算性能,推荐你使用 multiprocessing 或者 concurrent.futures.ProcessPoolExecutor。但是如果你想同时运行多个I/O绑定任务,线程仍然是一个合适的模型。

由于GIL的存在,CPython不能有效的利用多核处理器,表现为任意时间一个进程只有一个线程在跑;而IO密集型运算,多数是在IO读写将线程堵塞掉了,这个时候线程切换是很合理的,反正线程只是单纯地等待,在这个等待的时候去做其他的事情,资源利用率就上去了。

因为GIL锁,Python的多线程只能使用一个CPU核心;如果是CPU密集型应该用多进程模型(大量的计算),如果是IO密集型应该用多线程模型(数据的读取写入、网络IO数据传输等)。

函数

  • threading.active_count()

    返回当前活动的threading.Thread对象数——threading.enumerate()返回的列表的长度。

  • threading.current_thread()

    返回当前线程的Thread对象。

  • threading.get_ident()

    返回当前线程的标识。

  • threading.enumerate()

    返回当前活动线程Thread对象的列表。

  • threading.main_thread()

    返回主线程main Thread对象:一般情况下,主线程是Python解释器开始时创建的线程。

  • etc.

线程对象

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

创建线程对象后,使用start()方法可将其激活,这会在独立的线程控制中调用run方法。

当线程活动后,该线程总会被认为是alive;如果run()方法结束了,该线程就不是存活的;可使用is_alive()方法检查线程是否存活。

name属性是线程的名称,可以通过构造函数传递或者属性访问方式访问或修改。

注意

  1. 如果需要重写Thread类的方法,子类只应该重写Thread类的__init__run方法。
  2. 如果子类型重载了构造函数,它一定要确保在做任何事前,先发起调用基类构造器(Thread.__init__())

Thread对象属性/方法:

  • start():激活线程。

  • run():代表线程活动的方法。

  • join(timeout=None):等待该线程结束。

    注意:join()方法只有在你需要等待线程完成的时候才是有用的。

    例子1:不使用join。添加一个线程,输出“Job start.”和“Job end.”标记线程的开始和结束。线程create_thread和线程main同时运行,其中create_thread消耗较长的时间(通过time.sleep模拟)。mian执行结束后,added_thread才执行完毕。

    import threading
    import time
    
    def job():
        print('Job start.')
        time.sleep(1)
        print('Job end.')
    
    def main():
        create_thread = threading.Thread(target=job)
        create_thread.start()
        print("All Worker Done.")
    
    if __name__ == '__main__':
        main()
    
    Job start.
    All Worker Done.
    >>> Job end.
    

    例子2:使用join,join使得线程main等待create_thread执行完毕后才结束。

    import threading
    import time
    
    def job():
        print('Job start.')
        time.sleep(1)
        print('Job end.')
    
    def main():
        create_thread = threading.Thread(target=job)
        create_thread.start()
        create_thread.join()
        print("All Worker Done.")
    
    if __name__ == '__main__':
        main()
    
    Job start.
    Job end.
    All Worker Done.
    
  • is_alive():返回线程是否存活。

  • name:线程的名称。

  • ident:线程的标识。

  • daemon:判断线程是(True)否(False)守护线程。

线程锁

每个线程互相独立,相互之间没有任何关系,但是在同一个进程中的资源,线程是共享的,如果不进行资源的合理分配,对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。

class threading.Lock

  • acquire(blocking=True, timeout=-1):可以阻塞或非阻塞地获得锁(或称为加锁)。
    • 无参数调用:如果这个线程已经拥有锁,递归级别增加一,并立即返回。否则,如果其他线程拥有该锁,则阻塞至该锁解锁。一旦锁被解锁(不属于任何线程),则抢夺所有权,设置递归等级为一,并返回。如果多个线程被阻塞,等待锁被解锁,一次只有一个线程能抢到锁的所有权。在这种情况下,没有返回值。
    • blocking=True:阻塞直到锁被释放,然后将锁锁定并返回 True ;
    • blocking=True, timeout=2:允许等待2秒,等了2s也获得不了锁,返回False,否则返回True。
    • blocking=False:非阻塞方式获得锁。如果已被锁定,直接返回False,不等待;如果未被锁定,则返回Ture。
  • release():释放锁(或称为解锁)。

注意:join()方法也被称为自旋锁

例子:

import time
import threading

counter_lock = threading.Lock()

counter = 0

class Thread(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = "Thread-"+name
    
    def run(self):
        global counter, counter_lock
        counter_lock.acquire()
        try:
            time.sleep(1)
            counter += 1
            print("I'm {}, set counter:{}".format(self.name, counter))
        finally:
        	counter_lock.release()

def main():
    for i in range(1,10):
        t = Thread(str(i))
        t.start()

if __name__ == '__main__':
    main()

运行结果:

I'm Thread-1, set counter:1
I'm Thread-2, set counter:2
I'm Thread-3, set counter:3
I'm Thread-4, set counter:4
I'm Thread-5, set counter:5
I'm Thread-6, set counter:6
I'm Thread-7, set counter:7
I'm Thread-8, set counter:8
I'm Thread-9, set counter:9

如果不加锁,那么结果可能会是如下情况:

I'm Thread-2, set counter:1
I'm Thread-1, set counter:2
I'm Thread-4, set counter:3
I'm Thread-3, set counter:4
I'm Thread-6, set counter:5
I'm Thread-7, set counter:7
I'm Thread-9, set counter:9
I'm Thread-8, set counter:8
I'm Thread-5, set counter:6

信号量

class threading.Semaphore(value=1)

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

一个信号量管理一个内部计数器,该计数器因 acquire()方法的调用而递减,因 release()方法的调用而递增。 计数器的值永远不会小于零;当 acquire()方法发现计数器为零时,将会阻塞,直到其它线程调用 release()方法。

  • acquire():获取一个信号量。

    不带参数时调用:

    • 如果在进入时,内部计数器的值大于0,将其减1并立即返回True。
    • 如果在进入时,内部计数器的值为0,将会阻塞直到被 release()方法的调用唤醒。一旦被唤醒(并且计数器值大于0),将计数器值减少1并返回True。线程会被每次 release()方法的调用唤醒。线程被唤醒的次序是不确定的。

    blocking为False时:不会发生阻塞;如果不带参数的调用会发生阻塞(内部计数器的值为0)的话,带参数的调用在相同情况下将会立即返回False。否则,执行和不带参数的调用一样的操作并返回True。

    当参数 timeout 不为 None 时,将最多阻塞 timeout 秒。如果未能在时间间隔内成功获取信号量,将返回False,否则返回True。

  • release():释放一个信号量,将内部计数器的值增加1;当计数器原先的值为0且有其它线程正在等待它再次大于0时,唤醒正在等待的线程。

信号量主要作用是保护有限的资源。以数据库连接数为例说明,假设当前数据库支持最大连接数为3,将信号量初始值设为3,那么同时最大可以有三个线程连接数据库,其他线程若再想连接数据库,则只有等待,直到某一个线程释放数据库连接。

import threading
import time

semaphore=threading.Semaphore(3)

def connect_db():
    semaphore.acquire()
    try:
        print(threading.current_thread().name + " connecting to db...")
        time.sleep(2)
    finally:
        semaphore.release()
        print(threading.current_thread().name + " released to db...")

def main():
    for i in range(4):
        t = threading.Thread(target=connect_db)
        t.start()

if __name__ == '__main__':
    main()
Thread-1 connecting to db...
Thread-2 connecting to db...
Thread-3 connecting to db...
Thread-1 released to db...
Thread-4 connecting to db...
Thread-2 released to db...
Thread-3 released to db...
Thread-4 released to db...

注意:建议在With语句(try-finally)中使用线程锁、信号量、条件。

条件对象

class threading.Condition(lock=None)

Condition应用场景

线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。

通俗的讲,生产者,消费者的模型。 condition很适合那种主动休眠,被动唤醒的场景。 condition使用难度要高于mutex,一不注意就会被死锁。

属性/方法:

  • acquire(*args):请求底层锁。

  • release():释放底层锁。

  • wait(timeout=None):等待直到被通知(notify)或发生超。此操作会释放底层锁,然后阻塞,直到在另外一个线程中调用同一个条件变量的 notify()notify_all()唤醒它,或者直到可选的超时发生。一旦被唤醒或者超时,它重新获得锁并返回。

  • wait_for(predicate, timeout=None):等待,直到条件计算为真。这个实用方法会重复地调用 wait()直到满足判断式或者发生超时。返回值是判断式最后一个返回值,而且如果方法发生超时会返回 False

    忽略timeout,大致于如下代码:

    while not predicate():
        cv.wait()
    
  • notify(n=1):默认唤醒一个等待这个条件的线程;这个方法唤醒最多 n 个正在等待这个条件变量的线程;如果没有线程在等待,这是一个空操作。

  • notify_all():唤醒所有正在等待这个条件的线程。

生产者-消费者——对话示例
import threading
import time

condition = threading.Condition()

class Producer(threading.Thread):
    def __init__(self, condition, name):
        super().__init__()
        self.condition = condition
        self.name = name
    
    def run(self):
        self.condition.acquire()#获得锁
        print("{name}: {msg}".format(name=self.name, msg="山无棱,天地合,乃敢与君绝!"))
        self.condition.notify()#通知等待线程
        self.condition.wait()#释放对琐的占用,同时线程挂起在这里,直到被notify并重新占有琐。
        
        print("{name}: {msg}".format(name=self.name, msg="紫薇!!!"))
        self.condition.notify()
        self.condition.wait()
        
        print("{name}: {msg}".format(name=self.name, msg="是你"))
        self.condition.notify()
        self.condition.wait()
        
        print("{name}: {msg}".format(name=self.name, msg="有钱吗? 借点"))
        self.condition.notify()
        self.condition.release()

class Consumer(threading.Thread):
    def __init__(self, condition, name):
        super().__init__()
        self.condition = condition
        self.name = name
    
    def run(self):
        time.sleep(1)#等待1s,让Producer先获得锁
        self.condition.acquire()
        print("{name}: {msg}".format(name=self.name, msg="海可枯,石可烂,激情永不散!"))
        self.condition.notify()
        self.condition.wait()
        
        print("{name}: {msg}".format(name=self.name, msg="尔康!!!"))
        self.condition.notify()
        self.condition.wait()
        
        print("{name}: {msg}".format(name=self.name, msg="是我"))
        self.condition.notify()
        self.condition.wait()
        
        print("{name}: {msg}".format(name=self.name, msg="滚!"))
        self.condition.release()

def main():
    producer = Producer(condition, "尔康")
    consumer = Consumer(condition, "紫薇")
    producer.start()
    consumer.start()

if __name__ == '__main__':
    main()

对话结果:

尔康: 山无棱,天地合,乃敢与君绝!
紫薇: 海可枯,石可烂,激情永不散!
尔康: 紫薇!!!
紫薇: 尔康!!!
尔康: 是你
紫薇: 是我
尔康: 有钱吗? 借点
紫薇: 滚!

事件

class threading.Event

事件对象管理一个内部标志,调用 set()方法可将其设置为True。调用 clear()方法可将其设置为False。调用 wait()方法将进入阻塞直到标志为True。这个标志初始时为False。

Event其实就是一个简化版的 Condition。Event没有锁,无法使线程进入同步阻塞状态。

例子:小明交女朋友了,高兴,就打算请室友出去吃吨饭;饭馆已经定好,小明和女朋友正等他室友,人到齐了就开吃。

import threading

event = threading.Event()

class Friend(threading.Thread):
    def __init__(self, event, name):
        super().__init__()
        self.event = event
        self.name = name
    
    def run(self):
        print("{} 到达饭馆.".format(self.name))
        self.event.wait()
    	print("{} 开吃咯.".format(self.name))
   
def main():
    for name in ['小华', '小刚', '小王']:
        t = Friend(event, name)
        t.start()
    
    print("小明:兄弟们开吃了.")
    event.set()

if __name__ == '__main__':
    main()
小华 到达饭馆.
小刚 到达饭馆.
小王 到达饭馆.
小明:兄弟们开吃了.
小刚 开吃咯.
小王 开吃咯.
小华 开吃咯.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值