python之初探多线程

最近学了下python中多线程的一些知识,在此总结一下。

文章目录

    • 概述
    • 线程创建和启动
    • 线程的生命周期
    • 控制线程
    • 线程同步
    • 线程通信
    • 线程池

概述

几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能。

进程是系统进行资源分配和调度的一个独立单位。包括三个特征:独立性,动态性,并发性。

多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。

线程也被称为轻量级进程,线程是进程的执行单元。

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。

操作系统可以同时执行多个任务,每一个任务就是一个进程;进程可以同时执行多个任务,每一个任务就是一个线程。

多线程优点:

  1. 进程之间不能共享内存,但线程之间共享内存非常容易。
  2. 操作系统在创建进程时,需要为进程重新分配系统资源,但创建线程的代价则小得多。
  3. python语言内置了多线程功能支持,而不是单纯地作为底层操作系统地调度方式,从而简化了python的多线程编程。

线程创建和启动

import threading

def action(max):
    pass
#创建线程,
#target指定该线程要调度的目标方法
#args指定一个元组,以位置参数的形式为target指定的函数传入参数
#kwargs指定一个字典,以关键字参数的形式为target传入参数
#daemon指定所创建的线程是否为后台线程
t1 = threading.Thread(target=action, args=(100,))
#启动线程
t1.start()
#返回当前正在执行的线程对象
t = threading.current_thread()
#返回调用它的线程名字,同t.name
name = t.getName()

线程的生命周期

在线程的生命周期里,要经过新建、就绪、运行、阻塞和死亡5种状态。

当程序创建了一个Thread对象后,该线程处于新建状态。

start后处于就绪状态。

至于该线程何时开始运行,取决于python解释器中线程调度器的调度。

当发生如下情况时,线程将会进入阻塞状态:

  • 线程调用了sleep()方法主动放弃其所占用的处理器资源
  • 线程调用了一个阻塞式I/O方法,在该方法返回之前,该线程被阻塞。
  • 线程试图获得一个锁对象,但该锁对象正被其他线程所持有。
  • 线程在等待某个通知。

控制线程

join方法,让一个线程等待另一个线程完成的方法。当在某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join方法加入的join线程执行完成。

后台线程,任务是为了其他线程提供服务。例如python解释器的垃圾回收线程。

创建后台线程有两种方式:

  • 主动将线程的daemon属性设置为True
  • 后台线程启动的线程默认是后台线程。

线程睡眠:通过time模块的sleep(secs)函数来实现。使线程阻塞secs秒。

线程同步

Thread的run方法不具有线程安全性——程序中有多个并发程序在修改同一个对象。

threading模块提供了Lock和RLock两个类,他们都提供了如下两个方法来加锁和释放锁:

  • acquire(blocking=True, timeout=-1):请求对Lock或RLock加锁,其中timeout参数指定加锁多少秒。
  • release():释放锁。

Lock和RLock的区别:

  • Lock是一个基本的锁对象,每次只能锁定一次,其余的锁请求,需等待锁释放后才能获取。
  • RLock代表可重入锁。在同一个线程中可以对它进行多次锁定,也可以多次释放。acquire和release方法必须成对出现。

代码模板:

class X:
    #定义需要保证线程安全的方法
    def m(self):
        #加锁
        self.lock.acquire()
        try:
            #需要保证线程安全的代码
            #。。。方法体
        #使用finally来保证释放锁
        finally:
            #修改完成,释放锁
            self.lock.release()

通过使用Lock对象可以非常方便地实现线程安全的类,线程安全的类特征:

  • 该类的对象可以被多个线程安全地访问
  • 每个线程在调用该对象地任意方法之后,都将得到正确的结果,该对象依然保持合理的状态

死锁:当两个线程互相等待对方释放同步监视器。

一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,所有线程都处于阻塞状态,无法继续。

避免死锁的出现:

  • 避免多次锁定:尽量避免同一个线程对多个Lock进行锁定
  • 具有相同的加锁顺序:如果多个线程需要对多个Lock进行锁定,则应该保证他们以相同的顺序进行加锁
  • 使用定时锁
  • 死锁检测

线程通信

当线程在系统中运行时,线程的调度具有一定的透明性,通常程序无法准确控制线程的轮换执行,如果有需要,可通过线程通信来保证线程的协调运行。

使用Event控制线程通信:

一个线程发出一个Event,另一个线程可通过该Event被触发。

Event本身管理一个内部旗标,程序可以通过Event的set()方法将该旗标设置为True,也可以调用clear方法设置为False。可以调用wait方法来阻塞当前线程,知道Event的旗标为True

模板:

class X:
    def m1(self):
        self.lock.acquire()
        #如果为True
        if self.event.is_set():
            #...
            #...
            self.event.clear()
            self.lock.release()
            self.event.wait()
        else:
            self.lock.release()
            self.event.wait()
	def m2(self):
        self.lock.acquire()
        #如果为False
        if not self.event.is_set():
            #...
            #...
            self.event.set()
            self.lock.release()
            self.event.wait()
        else:
            self.lock.release()
            self.event.wait()

线程池

系统启动一个新线程的成本是比较高的,因为涉及到与操作系统的交互。如果程序需要创建大量生存期很短暂的线程时,应该考虑使用线程池。

线程池在系统启动时即创建大量空闲线程,程序只需要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行。当函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成K线状态,等待下一个函数。

线程池的基类是concurrent.futures模块中的Executor,Executor提供两个子类,ThreadPoolExecutor和ProcessPoolExecutor

Executor常用方法:

  • submit(fn,*args,**kwargs)

Future常用方法:

  • running():如果该Future代表的线程任务正在执行、不可被取消,该方法返回True
  • done():如果该Future代表的线程任务被成功取消或执行完成,该方法返回True
  • result(timeout=None):获取该Future如代表的线程任务最后返回的结果。如果线程任务未完成,则会阻塞当前线程
  • add_done_callback(fn):为该Future代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该fn函数

例子:

from concurrent.futures import ThreadPoolExecutor
import threading
import time

def action(max):
    pass
pool = ThreadPoolExecutor(max_workers=2)
future = pool.submit(action,50)

print(future.done())
print(future.result())
pool.shutdown()

你可能感兴趣的:(python,python,多线程,并发编程)