多线程

python中可以使用thread(_thread)、threading和Queue模块来实现多线程编程。thread的功能没有threading强大、完善,而且当主线程结束时其它线程会强制退出,不会发出警告也不会进行适当的清理。所以最好使用threading,threading功能更完善,而且当重要的子线程退出前不会退出。

threading模块函数

  1. threading.active_count():返回当前存活的Thread对象数量。
  2. threading.current_thread():返回当前对应调用者的控制线程的Thread对象。如果调用者的控制线程不是threading创建,返回功能受限的虚拟线程对象
  3. threaidng.excepthook(args,/):处理由Thread.run()引发的未捕获异常。
  4. threading.get_ident():返回当前线程的线程标识符。
  5. threading.get_native_is():返回线程id,TID。3.8新功能。
  6. threading.enumerate(): 返回当前所有存活的Thread对象列表。长度为和threading.active_count()返回值一致。
  7. threading.main_thread():返回主Thread对象,一般情况下,主线程时python解释器开始时创建的线程。

threading常量

threading.TIMEOUT_MAX:
- 阻塞函数,Lock.acquire(),RLock.acquire(),Condition.wait()中timeout允许的最大值。传入超过该值的数会引起OverflowError异常。

Thread类

Thread类是threading的核心,是主要的执行对象。

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

- group为None,为日后扩展ThreadGroup类实现而保留
- target是用于run的可调用对象,
- name线程名
- args参数元组,kwargs关键字参数字典,传递给可调用对象target的参数
- 如果自类重载了构造函数,它一定要确保在做任何事前,先发起调用基类构造器(Thread.__init__())。
- 3.10 版本如果没有name参数的化使用target名
  1. start():开启线程活动,一个线程最多被调用一次超出会抛出RuntimeError,安排run方法在一个独立的控制进程中调用。
  2. run():代表线程活动。可以在子类里重载。使用args和kwargs调用target方法
  3. join(timeout=None):等待,直到线程结束 ,会阻塞调用这个方法的线程,直到被调用join的线程结束不管是正常结束还是异常结束。一个线程被join多次。如果对一个没有开始的线程调用和死锁会引起RuntimeError
  4. name:线程名,不同线程可以重名
  5. getName(),setName(name),3.10弃用
  6. is_alive(): 线程是否存活,当run方法刚开始直到run方法刚结束都返回True。
  7. daemon:布尔值,表示是否是守护线程,这个值必须在调用start之前设置,否则引发RuntimeError,初始值继承字创建线程,主线程不是守护线程,则由主线程创建的线程默认为False。当所有非守护线程结束时,python程序才会退出
  8. isDaemon(),setDaemon(),3.10后启用。
  9. ident:线程的线程标识符,如果线程尚未开始则为None。是个非0整数。一个线程退出,另外一个线程被创建,线程标识符会被复用,即使线程退出后,仍可得到标识符。
  10. native_id:线程的ID(TID),由系统分配,非负整数。唯一标识某一个线程,线程结束后可以被系统回收再利用。3.8新功能
import threading
import time 
import os
def fun(n):
    print(f"start fun at {time.strftime('%X')}")
    print(threading.current_thread())
    time.sleep(n)
    print(f"end fun at {time.strftime('%X')}")

def block_io():
    print(f"star block_io at {time.strftime('%X')}")
    files = os.listdir("../")
    for f in files:
        try:
            print(f"read {f} at {time.strftime('%X')}")
            with open("../"+f, 'rb') as f:
                f.readlines()
        except Exception:
            pass 
        finally:
            print(f"read {f} has done at {time.strftime('%X')}")
    print(f"end block_io at {time.strftime('%X')}")

def main():
    print(f"start at {time.strftime('%X')}")
    thread1 = threading.Thread(target=fun, args=(2,), name='fun_1')
    thread0 = threading.Thread(target=fun, args=(1,), name='fun_0')
    thread2 = threading.Thread(target=block_io, name='block_io')
    print("-"*20)
    thread0.start()
    thread2.start()
    thread1.start()
    print("-"*20)
    print(threading.enumerate())
    print("-"*20)
    thread1.join()
    thread2.join()
    print(f"all done at {time.strftime('%X')}")


if __name__ == "__main__":
    main()

传递一个可调用类

import time 
import threading

class threadFunc(object):
    def __init__(self, func, args) -> None:
        self.func = func 
        self.args = args 

    def __call__(self):
        self.func(*self.args)


def fun(name, t):
    print(f"start a thread{name} at {time.strftime('%X')}")
    time.sleep(t)
    print(f"end a thread{name} at {time.strftime('%X')}")

def main():
    print(f"start at {time.strftime('%X')}")
    threads = []
    for i in range(1,4):
        t = threading.Thread(target=threadFunc(fun, (str(i), i)))
        threads.append(t)
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    print(f"end at {time.strftime('%X')}")


if __name__ == "__main__":
    main()

继承Thread类

from concurrent.futures import thread
import time 
import threading 

class myThread(threading.Thread):
    def __init__(self, func, args, name=''):
        super().__init__()
        self.func = func 
        self.args = args 
        self.name = name 

    def run(self):  # 继承Thread类需要实现run方法
        self.func(*self.args)

def fun(*args):
    print(f"start a thread{args[0]} at {time.strftime('%X')}")
    time.sleep(args[1])
    print(f"end a thread{args[1]} at {time.strftime('%X')}")

def main():
    threads = []
    for i in range(3):
        thread = myThread(fun,(i, i))
        threads.append(thread)
    for t in threads:
        t.start()
    for t in threads:
        t.join()


if __name__ == "__main__":
    main()

Python GIL全局解释器锁

import time 
import threading
def fun_(n=100000):
    while n>0:
        n-=1
    

if __name__ == "__main__":
    start = time.time()
    fun_()
    end = time.time()
    print(f"time used:{end-start}")  # 单线程用时0.005秒左右
    thread1 = threading.Thread(target=fun_, args=(1000000//2,))
    thread2 = threading.Thread(target=fun_, args=(1000000//2,))
    start = time.time()
    thread1.start()
    thread2.start()
    end = time.time()
    print(f"time used:{end-start}")  # 用时0.04秒左右
    # 多线程的性能反而降低

GIL全局解释器锁,使得python程序每一时刻只能有一个线程在执行。python中的多线程是每个线程交替运行。
多线程环境中,python解释器将按照下面的方式执行线程

  1. 申请GIL
  2. 获得GIL后执行逻辑代码
  3. 当线程时间片到后将释放GIL无论是否已经执行完毕,阻塞遇到IO操作时也会释放GIL。
  4. 其他线程重复执行1、2、3操作
  5. 等其他线程执行完(轮转一次)后,再次切换到之前的线程继续执行。时间片到后再次切换线程。

间隔式检查check_interval

python解释器会去轮询检查GIL锁的情况,每隔一段时间就会强制当前线程去释放GIL,这样别的线程才能有执行的机会。
间隔时间大致为15毫秒,不同版本的python不一样。总之解释器会在一个合理的时间范围内释放GIL。
GIL虽然限制了同一时刻只能由一个线程在执行,但是不能绝对保证线程安全
例如当多个线程对同一变量进行数值改变操作,数值改变操作并不是原子的,线程A可能在执行数值改变操作过程中GIL锁被释放,而另外一个线程的数值改变操作成功了,进而影响了线程A的操作。

为什么需要GIL?
1.python在设计之初cpu还是以单核为主,没有并行需求
2. python使用引用计数来管理的回收,如果不限制同一时刻只能有一个线程操作可能会导致某个变量的引用计数错误的为0,解释器释放了其所占内存。

同步原语

锁 threading.Lock

锁对象支持上下文管理协议
一个线程获取某个锁后,会阻塞其他想获取该锁的线程直到该锁被释放。可以由不同的线程锁定和释放锁。

  1. lock.acquire(blocking=True, timeout=-1)
    • blocking:是否会阻塞,默认为True,timeout最大等待时间,如果blocking为False禁止指定timeout
    • 如果成功获得锁返回True,否则返回False(超时的时候)
  2. lock.release()
    • 释放锁,如果对一个没有锁定的锁调用将引起RuntimeError
  3. lock.locked()
    • 如果锁被某个线程获取,返回True
# encoding=utf8
 
import threading
import time
 
lock = threading.Lock()
l = []
 
def test1(n):
    lock.acquire()  # 获取锁,如果锁被其他线程占用,会一直阻塞
    # 当多个线程同时等待锁时,当锁被释放后的执行顺序未知
    l.append(n)
    time.sleep(0.5)
    print(l)
    lock.release()
    
def test(n):
    """不加锁,运行结果不确定"""
    l.append(n)
    time.sleep(0.5)
    print(l)

    
def main():
	for i in range(0, 10):
		th = threading.Thread(target=test1, args=(i, ))
		th.start()

if __name__ == '__main__':
	main()

threading.RLock

可重入锁,当一个线程获取一个可重入锁后可再次调用acquire方法而不被阻塞。acquire和release必须成对出现,重入锁必须由获取它的线程释放。而Lock可以由不同的线程释放。
RLock和Lock都是互斥锁,同一时刻只能有一个线程访问临界区。而RLock的作用是在一定程度上防止死锁。因为一个获得RLock的线程再次acquire时不会阻塞只是将递归级别加一,而Lock对象同一线程内重复调用acquire会造成死锁。

  1. acquire(blocking=True, timeout=-1)
    • 获取一个锁,如果该线程已经获取该锁,递归级别加一,如果其他线程获取了锁对象,则阻塞
  2. release()
    • 将锁的递归级别减一,如果减一后锁的递归级别变为0,则该锁被释放,等待该锁被释放的线程会得以运行。
    rLock = threading.RLock()  # RLock对象
    rLock.acquire()
    rLock.acquire()  # 在同一线程内,程序不会堵塞。如果是threading.Lock对象会阻塞而造成死锁
    rLock.release()
    rLock.release()

条件对象 threading.Condition

生产者消费者模型:只要换成未满,生产者一直向缓存生成。只要缓存不空,消费者一直从缓存取出。当缓冲不为空时,生产者通知消费者,当缓冲不满,消费者将通知生产者。
threading.Condition(lock=None)

  • lock参数必须为Lokc或RLock对象,如果没有给出lock参数,将会创建一个RLock对象,将其作为底层锁
  1. acquire(): 请求条件对象的底层锁, 返回值是底层锁相应方法的返回值。
  2. release():释放底层锁,没有返回值
  3. wait(timeout=None):释放锁,然后阻塞直到其它线程调用notfiy方法唤醒,被唤醒后将重修获得锁并返回。
    • 等待直到被通知或发生超时,如果没有超时后没有获得锁将引发RuntimeError
    • 释放底层锁,然后阻塞,直到另外一个线程种调用notify或notify_all被唤醒或者直到超时,被唤醒时,将重新获得锁并返回。
  4. wait_for(predicate, timeout=None)
    • 等待直到条件predictae为真,predicate是一个可调用对象,而且它的返回值可解释为一个布尔值。可以提供timeout参数给出最大等待时间。该方法会重复调用wait方法直到条件为真或超时
  5. notify(n=1)
    • 默认唤醒一个等待这个条件的线程。如果调用线程没有获得锁的情况下调用这个方法,会引发RuntimeError异常。
  6. notify_all(): 唤醒所有等待这个条件对象的线程。
    **notify和notify_all方法并不会释放锁,被唤醒的线程不会立即从wait中返回,而是在调用notify方法的线程放弃了锁的所有权后返回。
import threading 

condition = threading.Condition()
current = 'A'

class threadA(threading.Thread):
    def run(self):
        global current 
        for _ in range(10):
            with condition:
                # if current !='A':
                while current !='A':
                    condition.wait()  
                    # 阻塞当前线程直到其他线程调用了notify,然后获得锁,
                    # 但是那个线程会获得锁未知,需要用while判断current状态,如果是A,则跳出循环
                    # 如果不是A则再次调用wait释放锁,然后阻塞
                print('A')
                current='B'
                condition.notify_all()

class threadB(threading.Thread):
    def run(self):
        global current 
        for _ in range(10):
            with condition:
                while current !='B':
                    condition.wait()
                print('B')
                current='C'
                condition.notify_all()

class threadC(threading.Thread):
    def run(self):
        global current 
        for _ in range(10):
            with condition:
                while current !='C':
                    condition.wait()
                print('C')
                current='A'
                condition.notify_all()


if __name__ == '__main__':

    threads = [threadA(), threadB(),threadC()]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()
    

信号量 Semaphore

一个信号量管理一个内部计数器,acquire计数器减一,release计数器加一。
threading.Semaphore(value=1)
信号量管理一个原子性的计数器。

  1. acquire(blocking=True, timeout=None)
    • 获取一个信号量。当内部计数器大于0时,则将内部技术器减一并理机返回True。当内部计数器的值为0时,则会阻塞直到release被调用而被唤醒。
  2. release(n=1)
    • 释放一个信号量,将内部计数器的值增加n,会唤醒等待信号量的线程,但是被唤醒的次序是不可确定的。
      threading.BoundedSemaphore(value=1)
      有界信号量,有界信号量通过检查以确保它当前的值不会超过初始值。如果超过会引发valueError。用于保护数量有限的资源。
import threading 
import time 


sempahore = threading.Semaphore()
def consumer():
    for i in range(5):
        print("consumer is waiting")
        sempahore.acquire()
        print("counsumer notify: consumed one item")

def producer():
    for i in range(6):
        time.sleep(1)
        print("producer notify: produced one item")
        sempahore.release()  # 可以释放超过初始值的信号量


if __name__ == "__main__":
    threadp = threading.Thread(target=producer)
    threadc = threading.Thread(target=consumer)
    threadp.start()
    threadc.start()
    threadp.join()
    threadc.join()

有界信号量
Semaphore没有设置上限值,所以可能会出现信号量释放的次数过多(多于本来规划的值)。为了避免这个情况的出现,使用有界信号量BoundedSemaphore

bound_semaphore = threading.BoundedSemaphore(3)

事件对象

一个线程发出事件信号,其他线程等待该信号.
一个事件对象管理一个内部标识,调用set方法将其设为True,clear设为False。wait方法将阻塞直到标识为true
threading.Event()

  1. is_set():当内部标识为True时,返回True
  2. set():把内部标识设为True。所有等待这个事件的线程将被唤醒。
  3. clear(): 把标识设为False
  4. wait(timeout=None):阻塞线程直到内部标识为True。当且仅当内部标识为True时返回Ture。
import threading
import time
import random

items = []
event = threading.Event()

def consumer(i):
    
    event.wait()
    item = items.pop()
    print(f"consumer{i} notify: {item} has benn consumed")

def producer():

    item = random.randint(0,256)
    items.append(item)
    print(f"producer notify: item{item} has been produced")
    item = random.randint(0,256)
    items.append(item)
    print(f"producer notify: item{item} has been produced")
    item = random.randint(0,256)
    items.append(item)
    print(f"producer notify: item{item} has been produced")
    event.set()  # 唤醒所有等待该事件的线程
    time.sleep(5)
    print(f"event cleared")
    event.clear()

if __name__ == "__main__":
    t0 = threading.Thread(target=consumer, args=(0,))
    t1 = threading.Thread(target=consumer, args=(1,))
    t2 = threading.Thread(target=consumer, args=(2,))
    t3 = threading.Thread(target=producer)
    t0.start()
    t1.start()
    t2.start()
    t3.start()
    t0.join()
    t1.join()
    t2.join()
    t3.join()

定时器对象Timer

Timer类时Thread类的子类。通过start方法启动定时器,cancel方法可以停止计时器(在计时结束之前)。定时器在执行其操作之前等待的时间间隔可能与用户指定的时间间隔不完全相同。
threading.Timer(interval, function, args=None, kwargs=None)
在interval秒后,执行function。

import threading 
import time 

def hello(friend):
    print(f"hello {friend}, {time.strftime('%X')}")

if __name__ == "__main__":
    timer = threading.Timer(3, hello, ("john",))
    print(f" {time.strftime('%X')}")
    timer.start()
    timer.join()

栅栏对象Barrier

当一定数目的线程调用了wait方法后,所有线程被同时释放
threading.Barrier(parties, action=None, timeout=None)
- parties:需要几个线程调用wait方法后才会放行。每parties个线程调用wait就会放行对应线程。
- action:可调用对象,当线程被放行后会被防止其中一个线程自动调用

  1. wait(timeout=None):
    • 当栅栏中所有线程都已经调用了这个函数,它们将同时被释放。如果提供了 timeout 参数,这里的 timeout 参数优先于创建栅栏对象时提供的 timeout 参数。
    • 返回0到parties-1,每个线程的返回值不同
  2. reset():
    • 重置栅栏为默认初始状态,如果有线程等待释放,这些线程将会收到BrokenBarrierError异常
  3. abort():
    • 使栅栏处于损坏状态。 这将导致任何现有和未来对 wait() 的调用失败并引发 BrokenBarrierError。 例如可以在需要中止某个线程时使用此方法,以避免应用程序的死锁。
import threading 
import time 

def hello():
    thread = threading.current_thread()
    print(f"action in {thread.name}")

barrier = threading.Barrier(2,hello)

def fun():
    barrier.wait()
    thread = threading.current_thread()
    print(f"{thread.name} has been executed at {time.strftime('%X')}")


if __name__ =="__main__":
    threads = []
    for i in range(6):
        t = threading.Thread(target=fun, name=f"thread{i}")
        threads.append(t)
    for t in threads:
        t.start()
    for t in threads:
        t.join()


posted @ 2022-07-01 23:08  店里最会撒谎白玉汤  阅读(58)  评论(0)    收藏  举报