python基础 -进程、线程


一、进程

1.定义

狭义定义: 进程是正在运行的程序的实例;

广义定义: 进程是一个具有一定独立功能的程序,关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

  • 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域和堆栈区域。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量;
  • 进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。

  对于操作系统来说,一个 任务 就是一个 进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。由于每个进程至少要干一件事,所以,一个进程至少有一个线程。

进程和程序的区别:

  • 程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念;
  • 进程是程序在处理机上的一次执行过程,它是一个动态的概念;
  • 程序可以作为一种软件资料长期存在,而进程是有一定生命期的;
  • 程序是永久的,进程是暂时的。

2.特征

  • 动态性: 进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生、动态消亡的;
  • 并发性: 任何进程都可以同其他进程一起并发执行;
  • 独立性: 进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
  • 异步性: 由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进;
  • 结构特征: 进程由程序、数据和进程控制块三部分组成。

  多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。

3.生命周期

  进程有三种状态不断切换,三种状态的转换如图所示:
在这里插入图片描述
  在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入几个状态:就绪、运行和阻塞。

  • 就绪(Ready)状态: 当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行时,这时的进程状态称为就绪状态;
  • 执行/运行(Running)状态: 当进程已获得处理机,其程序正在处理机上执行时,此时的进程状态称为执行状态;
  • 阻塞(Blocked)状态: 正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等;

4.multiprocess模块

4.1.Process类

  创建进程的类:Process,由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)。参数如下:

from multiprocessing import Process
Process(group=None,target=None,name=None,args=(),kwargs={})
  • group:参数未使用,值始终为None;
  • target:表示调用对象,即子进程要执行的任务;
  • args:表示调用对象的位置参数元组,args=(1,2,‘egon’,);args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号;
  • kwargs:表示调用对象的字典,kwargs={‘name’:‘egon’,‘age’:18};
  • name:为子进程的名称。

4.2.Process类的常用方法

  • p.start():启动进程,并调用该子进程中的p.run()。
  • p.run():进程启动时运行的方法,正是它去调用target指定的函数,自定类的子类中一定要实现该方法。
  • p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁。
  • p.is_alive():如果p仍然运行,返回True。
  • p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能限制住start开启的进程,而不能限制住run开启的进程。

4.3.Process类的属性

  • p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置。
  • p.name:进程的名称。
  • p.pid:进程的pid。
  • p.exitcode:进程在运行时为None,如果为–N,表示被信号N结束(了解即可)。
  • p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功。

4.4.使用Process模块创建进程

(1)启动一个子进程并等待其结束:

import os
from multiprocessing import Process

def run_proc(name):
    print('Run child process %s (%s)' %(name, os.getpid()))

if __name__ == '__main__':
    print('Parent process %s.' % os.getpid())
    p = Process(target=run_proc, args=('test', ))
    print('Child process will start...')
    p.start()
    p.join()
    print('Child process end...')

# 执行结果
Parent process 26548.
Child process will start...
Run child process test (28844)
Child process end...

  创建子进程时,需要传入一个执行函数和函数的参数,创建一个Procss实例,用start()方法启动,join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。

(2)如果要启动大量的子进程,可以用进程池的方式批量创建子进程:

from multiprocessing import Process, Pool
import time, os

def long_time_task(name):
    print('Run task %s (%s)' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end-start)))

if __name__ == '__main__':
    print('Parent process %s.' % os.getpid())
    p = Pool(4)
    for i in range(6):
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()
    print('All subprocess done...')

# 执行结果
Parent process 28416.
Waiting for all subprocesses done...
Run task 0 (29244)
Run task 1 (24420)
Run task 2 (25084)
Run task 3 (28696)
Task 1 runs 0.65 seconds.
Run task 4 (24420)
Task 4 runs 0.65 seconds.
Run task 5 (24420)
Task 2 runs 1.54 seconds.
Task 0 runs 1.68 seconds.
Task 3 runs 1.65 seconds.
Task 5 runs 1.06 seconds.
All subprocess done...

  对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。请注意输出的结果,task0,1,2,3是立刻执行的,而task4是等待前面某个task完成后才执行,这是因为Pool的默认大小在我的电脑上是4,最多同时执行4个进程。这是Pool有意设计的限制,并不是操作系统的限制。如果改成p = Pool(5)就可以同时跑5个进程。由于Pool的默认大小是CPU的核数,如果你不幸拥有8核CPU,你要提交至少9个子进程才能看到上面的等待效果。

(3)进程间通信:

  Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
  以Queue为例,在父进程中创建两个子进程,一个往Queue里面写数据,一个从Queue里面读数据:

from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码
def write(q):
    print('Process to write:%s' % os.getpid())
    for value in ['A','B','C']:
        print('Put %s to queue..' % value)
        q.put(value)
        time.sleep(random.random())

# 读数据进程执行的代码
def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        value = q.get(True)
        print('Get %s from queue...' % value)

if __name__ == '__main__':
    # 父进程创建Queue,并传给各个子进程
    q = Queue()
    pw = Process(target = write, args=(q,))
    pr = Process(target = read, args=(q,))
    # 启动子进程pw,写入:
    pw.start()
    # 启动子进程pr,读取:
    pr.start()
    # 等待pw结束
    pw.join()
    # pr进程里是死循环,无法等待其结束,只能强行终止
    pr.terminate()

# 执行结果
Process to write:29620
Put A to queue..
Process to read: 26316
Get A from queue...
Put B to queue..
Get B from queue...
Put C to queue..
Get C from queue...

4.5.继承Process类开启进程

  用户可以根据自己的需求自定义类继承Process,通过继承的方式开启进程:

from multiprocessing import Process
import os, time

# 定义一个类,继承Process类
class MyThread(Process):
    def __init__(slef,interval):
        Process.__init__(slef)
        slef.interval = interval
    # 重写Process类中的run()方法
    def run(self):
        # 开启这个进程所要执行的代码
        t_start = time.time()
        # time.sleep(3)         # 阻塞的另一种实现形式
        print('开启进程:%s 执行 MyThread 操作' % os.getpid())
        print('子进程(%s)开始执行,父进程为(%s)' % (os.getpid(), os.getppid()))
        time.sleep(self.interval)
        t_stop = time.time()
        print('子进程(%s)执行结束,耗时(%s)' % (os.getpid(), t_stop-t_stop))

if __name__ == '__main__':
    t_start = time.time()
    print('当前进程(%s),即主进程' %  os.getpid())
    p = MyThread(2)
    对于一个不包含target属性的Process类,执行start()方法,表示子进程就会运行类中的run()方法
    p.start()
    # p.join()
    time.sleep(10)  # 区分sleep与join()区别
    t_stop = time.time()
    print('主进程 %s 执行结束,耗时%f秒' % (os.getpid(), t_stop-t_start))

二、线程

1.线程状态

  大多数线程的生命周期经历有 新建、就绪、运行、阻塞、死亡 这五种状态,对于多线程来说,一个线程不可能始终占用着CPU,CPU需要在不同线程之间切换,于是线程的状态也会随之改变。
在这里插入图片描述
新建:

  当一个线程被创建后,该线程首先会处于新建状态,此时的线程对象并不会表现出任何线程的动态特征,程序也不会执行线程的执行体;

就绪:

  当调用线程对象的start()方法后,该线程处于就绪状态,至于何时开始运行完全取决于Python解释器中线程调度器的调度;

  不调用start()方法,而仅仅只调用run()方法的话,系统会把该线程当成一个普通对象,run()也只是一个普通方法,在run()方法运行返回之前,其它线程均无法并发执行,也就是说这个线程已经作为普通程序成了主线程的一部分。

运行:

  只有当处于就绪状态的线程获得了CPU,开始执行run()方法后,此时该线程处于运行状态。处于运行状态的线程不可能一直处于运行状态(除非它的线程执行体足够短以至于瞬间就能结束任务),线程调度会使正在运行的线程被中断,目的是使其它线程获得执行的机会,线程调度的细节取决于底层平台采用的策略。这种策略主要有两种:

  • 抢占式调度策略: 对于采用抢占式调度策略的系统而言,系统会给每一个可执行线程一小段时间来处理任务,当该时间段过去后,程序会剥夺该线程占用的资源,让其它线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。
  • 协同式调度策略: 在协同式调度策略下,只有当线程调用它的sleep()或yield()方法后才会放弃其所占用的资源,也就是说必须由线程主动放弃其所占的资源。

  处于运行状态的线程接下来只有两种去路,要么被堵塞,要么由于需要执行的任务处理完毕或异常原因进入死亡。

阻塞:

当一个线程被创建后,该线程首先会处于新建状态,此时的线程对象并不会表现出任何线程的动态特征,程序也不会执行线程的执行体;

线程会因为以下几种原因而 被阻塞:

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

线程会因为以下几种情况而 解除阻塞:

  • 调用的sleep()方法超出了指定时间;
  • 线程调用的阻塞式I/O方法已经返回;
  • 线程成功地获得了试图获取的锁对象;
  • 线程正在等待某个通知时,其它线程发出了通知;

  被阻塞的线程解除阻塞后并不会再次进入运行状态而是进入就绪状态 ,它需要再次等待线程调度器去调度它,只有当它再次获得处理器资源后,才会进入运行状态。

死亡:

  当线程的任务执行体执行完毕,或者线程执行遇到一些异常情况,该线程将会死亡。不要视图对已死亡的线程再次调用其start()方法,这会导致程序抛出异常。可以用线程对象的is_alive()方法判断线程是否死亡,只有当线程处于新建或者死亡状态时,该方法会返回False。

  注意:线程一旦被创建,它不会受到主线程的影响,它和主线程具有同等地位,主线程结束,其他线程也不会随之结束。

2.线程同步

  多线程的优势在于可以同时运行多个任务(但实际它们是交替运行的)。但是当线程需要共享数据时,可能存在数据不同步的问题。考虑这样一种情况:一个列表里所有元素都是0,线程”set”从后向前把所有元素改成1,而线程”print”负责从前往后读取列表并打印。那么,可能线程”set”开始改的时候,线程”print”便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。

  锁有两种状态 — 锁定和未锁定。每当一个线程比如”set”要访问共享数据时,必须先获得锁定;如果已经有别的线程比如”print”获得锁定了,那么就让线程”set”暂停,也就是同步阻塞;等到线程”print”访问完毕,释放锁以后,再让线程”set”继续。经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。
在这里插入图片描述

3.线程通信(条件变量)

  还有一种情况,共享数据并不是一开始就有的,而是通过线程create创建的如果”set”或者”print” 在”create”还没有运行的时候就访问列表,将会出现一个异常。使用锁可以解决这个问题,但是”set”和”print”将需要一个无限循环——它们不知道”create”什么时候会运行,让”create”在运行后通知”set”和”print”显然是一个更好的解决方案。于是,引入了条件变量。

  条件变量允许线程比如”set”和”print”在条件不满足的时候(列表为None时)等待,等到条件满足的时候(列表已经创建)发出一个通知,告诉”set” 和”print”条件已经有了,你们该起床干活了;然后”set”和”print”才继续运行。

  • 线程和条件变量的交互,等待通知锁定如下图所示:
    在这里插入图片描述
  • 线程和条件变量的交互,执行完毕释放如下图所示:

在这里插入图片描述

4.线程运行和阻塞的状态转换

  下面给出一张运行阻塞的状态图,看看线程运行和阻塞状态的转换:
在这里插入图片描述
阻塞有以下三种情况:

  • 同步阻塞 是指处于竞争锁定的状态,线程请求锁定时将进入这个状态,一旦成功获得锁定又恢复到运行状态;
  • 等待阻塞 是指等待其他线程通知的状态,线程获得条件锁定后,调用“等待”将进入这个状态,一旦其他线程发出通知,线程将进入同步阻塞状态,再次竞争条件锁定;
  • 其他阻塞 是指调用time.sleep()、anotherthread.join()或等待IO时的阻塞,这个状态下线程不会释放已获得的锁定。

5.threading模块

  python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

_thread模块 提供的其他方法如下:

  • _thread.interrupt_main():在其他线程中终止主线程;
  • _thread.get_ident():获得一个代表当前线程的魔法数字,常用于从一个字典中获得线程相关的数据。这个数字本身没有任何含义,并且当线程结束后会被新线程复用。

  由于thread提供的线程功能不多,无法在主线程结束后继续运行,不提供条件变量等原因,一般不使用_thread模块。

threading模块 提供的常用方法如下:

  • threading.currentThread():返回当前的线程变量
  • threading.enumerate():返回一个包含正在运行的线程的list,正在运行是指线程启动后、结束前,不包括启动前和终止后的线程。
  • threading.activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果

threading模块 提供的类如下:

  • Thread
  • Lock
  • RLock
  • Condition
  • Semaphore/BoundedSemaphore
  • Event
  • Timer
  • local

5.1.Thread

  Thread是线程类,有两种使用方法:直接传入要运行的方法或从Thread继承并覆盖run()。

  • start()方法:开始线程活动;
    对每一个线程对象来说它只能被调用一次,它安排对象在一个另外的单独线程(thread-1)中调用run()方法(而非当前所处线程(mainThread))。当该方法在同一个线程对象中被调用超过一次时,会引入RuntimeError(运行时错误)。
  • run()方法:代表了线程活动的方法;
    可以在子类中重写此方法。标准run()方法调用了传递给对象的构造函数的可调对象作为目标参数,如果有这样的参数的话,顺序和关键字参数分别从args和kargs取得。
import threading
# 方法一:直接传入要运行的方法
def func():
    print('func() passed to Thread')

if __name__ == '__main__':
    t = threading.Thread(target=func)
    t.start()

# 执行结果
func() passed to Thread
# 方法二:Thread继承并覆盖run()
class MyThread(threading.Thread):
    def run(slef):
        print('MyThread extended from Thread')

if __name__ == '__main__':
    t = MyThread()
    t.start()
    
# 执行结果
MyThread extended from Thread

构造方法:

import threading
threading.Thread(group=None,target=None,name=None,args=(),kwargs={})
  • group:线程组,目前还没有实现,库引用中提示必须是None;
  • target:要执行的方法;
  • name:线程名;
  • args/kwargs:要传入方法的参数;

实例方法:

  • isAlive():返回线程是否在运行。正在运行指启动后、终止前;
  • get/setName(name):获取/设置线程名;
  • is/setDaemon(bool):获取/设置是否守护线程。初始值从创建该线程的线程继承。当没有非守护线程仍在运行时,程序将终止;
  • start():启动线程;
  • join([timeout]):阻塞当前上下文环境的线程,直到调用此方法的线程终止或到达指定的timeout(可选参数);

启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()方法开始执行:

import threading, time

def loop():
    print('Thread %s is running...' % threading.current_thread().name)
    n = 0
    while n < 5:
        n += 1
        print('thread %s >>> %s' % (threading.current_thread().name, n))
        time.sleep(1)
    print('Thread %s ended' % threading.current_thread().name)

print('Thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()

# 执行结果
Thread MainThread is running...
Thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
Thread LoopThread ended

  由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字python就自动给线程命名为Thread-1,Thread-2…

5.2.Lock

  Lock(指令锁)是可用的最低级的同步指令。Lock处于锁定状态时,不被特定的线程拥有。Lock包含两种状态 — 锁定和非锁定,以及两个基本的方法。可以认为Lock有一个锁定池,当线程请求锁定时,将线程置于池中,直到获得锁定后出池。

构造方法:

import threading
threading.Lock()

实例方法:

  • acquire([timeout]):使线程进入同步阻塞状态,尝试获得锁定。
  • release():释放锁。使用前线程必须已获得锁定,否则将抛出异常。
import threading, time

data = 0
lock = threading.Lock()
def func():
    global data
    print('%s acquire lock...' % threading.currentThread().getName())
    # 调用acquire([timeout])时,线程将一直阻塞,直到获得锁定或者直到timeout秒后(timeout可选参数),返回是否获得锁
    if lock.acquire():
        # print(lock.acquire())
        print('%s get the lock.' % threading.currentThread().getName())
        data += 1
        # print(data)
        time.sleep(2)
        print('%s release the lock.' % threading.currentThread().getName())
        # 调用release()将释放锁
        lock.release()

t1 = threading.Thread(target=func)
t2 = threading.Thread(target=func)
t3 = threading.Thread(target=func)
t1.start()
t2.start()
t3.start()

# 执行结果
Thread-1 acquire lock...
Thread-1 get the lock.
Thread-2 acquire lock...
Thread-3 acquire lock...
Thread-1 release the lock.
Thread-2 get the lock.
Thread-2 release the lock.
Thread-3 get the lock.
Thread-3 release the lock.

  多运行几次,会发现打印的信息顺序并不一致,这就证实了线程在锁定池中谁将获得锁进入运行状态是由系统调度决定(随机、不确定)。

5.3.RLock

  RLock(可重入锁)是一个可以被同一个线程请求多次的同步指令。RLock使用了“拥有的线程”和“递归等级”的概念,处于锁定状态时,RLock被某个线程拥有。拥有RLock的线程可以再次调用acquire(),释放锁时需要调用release()相同次数。可以认为RLock包含一个锁定池和一个初始值为0的计数器,每次成功调用acquire()/release(),计数器将+1/-1,为0时锁处于未锁定状态。

构造方法:

import threading
threading.RLock()

实例方法:

  • acquire([timeout]):使线程进入同步阻塞状态,尝试获得锁定。
  • release():释放锁。使用前线程必须已获得锁定,否则将抛出异常。
import threading, time

rlock = threading.RLock()
def func():
    # 第一次请求锁定
    print('%s acquire lock...' % threading.currentThread().getName())
    # 调用acquire([timeout])时,线程将一直阻塞,直到获得锁定或者直到timeout秒后(timeout可选参数),返回是否获得锁
    if rlock.acquire():
        print('%s get the lock.' % threading.currentThread().getName())
        time.sleep(2)
        # 第二次请求锁定
        print('%s acquire lock again...' % threading.currentThread().getName())
        if rlock.acquire():
            print('%s get the lock.' % threading.currentThread().getName())
            time.sleep(2)
            # 第一次释放锁
            print('%s release the lock.' % threading.currentThread().getName())
            rlock.release()
        # 第二次释放锁
        print('%s release the lock.' % threading.currentThread().getName())
        rlock.release()

t1 = threading.Thread(target=func)
t2 = threading.Thread(target=func)
t3 = threading.Thread(target=func)
t1.start()
t2.start()
t3.start()

# 执行结果
Thread-1 acquire lock...
Thread-1 get the lock.
Thread-2 acquire lock...
Thread-3 acquire lock...
Thread-1 acquire lock again...
Thread-1 get the lock.
Thread-1 release the lock.
Thread-1 release the lock.
Thread-2 get the lock.
Thread-2 acquire lock again...
Thread-2 get the lock.
Thread-2 release the lock.
Thread-2 release the lock.
Thread-3 get the lock.
Thread-3 acquire lock again...
Thread-3 get the lock.
Thread-3 release the lock.
Thread-3 release the lock.

5.4.Condition

  Condition(条件变量)通常与一个锁关联。需要在多个Contidion中共享一个锁时,可以传递一个Lock/RLock实例给构造方法,否则它将自己生成一个RLock实例。可以认为,除了Lock带有的锁定池外,Condition还包含一个等待池,池中的线程处于状态图中的等待阻塞状态,直到另一个线程调用notify()/notifyAll()通知;得到通知后线程进入锁定池等待锁定。

构造方法:

import threading
threading.Condition()

实例方法:

  • acquire([timeout])/release():调用关联的锁的相应方法;
  • wait([timeout]):调用这个方法将使线程进入Condition的等待池等待通知,并释放锁。使用前线程必须已获得锁定,否则将抛出异常;
  • notify():调用这个方法将从等待池挑选一个线程并通知,收到通知的线程将自动调用acquire()尝试获得锁定(进入锁定池);其他线程仍然在等待池中。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常;
  • notifyAll():调用这个方法将通知等待池中所有的线程,这些线程都将进入锁定池尝试获得锁定。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常;

这里给出一个关于生产者/消费者模式的实例,具体diamagnetic如下:

import time, threading

product = None                  # 商品
con = threading.Condition()     # 条件变量
def produce():                  # 生产者方法
    global product
    if con.acquire():
        while True:
            if product is None:
                print('produce...')
                product = 'anything'
                con.notify()    # 通知消费者,商品已经生产
            con.wait()          # 等待通知
            time.sleep(2)

def consume():                  # 消费者方法
    global product
    if con.acquire():
        while True:
            if product is not None:
                print('consume...')
                product = None
                con.notify()    # 通知生产者,商品已经没了
            con.wait()          # 等待通知
            time.sleep(2)

t1 = threading.Thread(target=produce)
t2 = threading.Thread(target=consume)
t2.start()
t1.start()

5.5.Semaphore/BoundedSemaphore

  Semaphore(信号量)是计算机科学史上最古老的同步指令之一。Semaphore管理一个内置的计数器,每当调用acquire()时-1,调用release()时+1。计数器不能小于0;当计数器为0时,acquire()将阻塞线程至同步锁定状态,直到其他线程调用release()。基于这个特点,Semaphore经常用来同步一些有“访客上限”的对象,例如连接池。

  BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数器的值是否超过了计数器的初始值,如果超过了将抛出一个异常。

构造方法:

import threading
threading.Semaphore(value=1)  
  • value是计数器的初始值,可以写成threading.Semaphore(1)

实例方法:

  • acquire([timeout]):请求Semaphore。如果计数器为0,将阻塞线程至同步阻塞状态;否则将计数器-1并立即返回;
  • release():释放Semaphore,将计数器+1,如果使用BoundedSemaphore,还将进行释放次数检查。release()方法不检查线程是否已获得Semaphore。
import threading, time

# 计时器初始值为2
semaphore = threading.Semaphore(value=2)
def func():
    # 请求Semaphore,成功后计数器-1,计数器为0时阻塞
    print('%s acquire semaphore...' % threading.currentThread().getName())
    if semaphore.acquire():
        print('%s get semaphore' % threading.currentThread().getName())
        time.sleep(4)
        # 释放Semaphore,计数器+1
        print('%s release semaphore...' % threading.currentThread().getName())
        semaphore.release()

t1 = threading.Thread(target=func)
t2 = threading.Thread(target=func)
t3 = threading.Thread(target=func)
t4 = threading.Thread(target=func)
t1.start()
t2.start()
t3.start()
t4.start()
time.sleep(2)
# 没有获得semaphore的主线程也可以调用release;若没有BoundedSemaphore,t4释放semaphore时将抛出异常
print('MainThread release semophore without acquire')
semaphore.release()

# 执行结果Thread-1 acquire semaphore...
Thread-1 get semaphore
Thread-2 acquire semaphore...
Thread-3 acquire semaphore...
Thread-2 get semaphore
Thread-4 acquire semaphore...
MainThread release semophore without acquire
Thread-3 get semaphore
Thread-1 release semaphore...
Thread-2 release semaphore...
Thread-4 get semaphore
Thread-3 release semaphore...
Thread-4 release semaphore...

5.6.Event

  Event(事件)是最简单的线程通信机制之一,它是一个线程通知事件,其他线程等待事件。Event内置了一个初始为False的标识,当调用set()时设为True,调用clear()时重置为False。wait()将阻塞线程至等待阻塞状态。

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

构造方法:

import threading
threading.Event()  

实例方法:

  • isSet():当内置标识为True时返回True;
  • set():将标识设为True,并通知所有处于等待阻塞状态的线程恢复运行状态;
  • clear():将标识设为False;
  • wait([timeout]):如果标识为True将立即返回,否则阻塞线程至等待阻塞状态,等待其他线程调用set()。
import time, threading

event = threading.Event()
def func():
	# 等待事件,进入等待阻塞状态
    print('%s wait for event...' % threading.currentThread().getName())
    event.wait()
    # 收到事件后进入运行状态
    print('%s recv event...' % threading.currentThread().getName())

t1 = threading.Thread(target=func)
t2 = threading.Thread(target=func)
t1.start()
t2.start()
time.sleep(2)
# 发送事件通知
print('MainThread set event.')
event.set()

5.7.Timer

  Timer(定时器)是Thread的派生类,用于在指定时间后调用一个方法。

构造方法:

import threading
threading.Timer(interval,function,args=[],kwargs={})
  • interval:指定的事件;
  • function:要执行的方法;
  • args/kwargs:方法的参数。

Timer从Thread派生,没有增加实例方法。

import threading

def func():
    print('hello timer!')

timer = threading.Timer(5,func)
timer.start()

5.8.local

  local是一个小写字母开头的类,用于管理thread-local(线程局部的)数据。对于同一个local,线程无法访问其他线程设置的属性;线程设置的属性不会被其他线程设置的同名属性替换。

  可以把local看成是一个“线程-属性字典”的字典,local封装了从自身使用线程作为key检索对应的属性字典,再使用属性名作为key检索属性值的细节。

构造方法:

import threading
threading.Timer(interval,function,args=[],kwargs={})
  • interval:指定的事件;
  • function:要执行的方法;
  • args/kwargs:方法的参数。
import threading

local = threading.local()
local.tname = 'main'
def func():
    local.tname = 'notmain'
    print('%s get the local.tname is: %s' % (threading.currentThread().getName(), local.tname))

t1 = threading.Thread(target=func)
t1.start()
t1.join()
print('%s get the local.tname is: %s' % (threading.currentThread().getName(), local.tname))

6.守护线程

  在计算机科学中,守护(daemon)代表后台运行的进程。守护线程对于python的线程有着更具体的意义。程序退出时,守护线程会立即关闭,理解这种定义的另一种形式是将守护线程视为在后台运行的线程,而且不用手动去关闭它。如果程序正在运行的线程并不是守护线程,则程序需要等待这些线程完成之后才能终止。然而,当线程是守护线程时,程序关闭时该线程就会被直接杀死,而不会考虑它运行到了哪里。

  如果你设置一个线程为守护线程,就表示你在说这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。如果你的主线程在退出的时候,不用等待那些子线程完成,那就设置这些线程的daemon属性。即在线程开始(thread.start())之前,调用setDeamon()函数,设定线程的daemon标志。(thread.setDaemon(True))就表示这个线程“不重要”。

  如果你想等待子线程完成再退出,那就什么都不用做,或者显示地调用thread.setDaemon(False),设置daemon的值为false。新的子线程会继承父线程的daemon标志。整个Python会在所有的非守护线程退出后才会结束,即进程中没有非守护线程存在的时候才结束。

import threading
import time
def text(n):
    for i in range(n):
        print('thread %s is running >> %d.' % (threading.current_thread().name,i))
        if i == n-1:
            print('thread %s end.' % (threading.current_thread().name))
        time.sleep(1)
t = threading.Thread(target=text,args=(10,),name='守护线程')
t.daemon = True
t.start()
for i in range(5):
    print('thread %s is running >> %d' % (threading.current_thread().name,i))
    time.sleep(1)

# 执行结果
thread 守护线程 is running >> 0.
thread MainThread is running >> 0
thread MainThread is running >> 1
thread 守护线程 is running >> 1.
thread MainThread is running >> 2
thread 守护线程 is running >> 2.
thread 守护线程 is running >> 3.
thread MainThread is running >> 3
thread MainThread is running >> 4
thread 守护线程 is running >> 4.

  可以看到,守护线程只执行了5次循环,之后就被强制结束,因为主线程结束了,所以守护线程也就没有存在的必要了,即随之结束。

小结

  熟练掌握Thread、Lock、Condition就可以应对绝大多数需要使用线程的场合,某些情况下local也是非常有用的东西。文章最后使用这几个类展示线程基础中提到的场景。

import threading

alist = None
condition = threading.Condition()
def do_set():
    if condition.acquire():
        while alist is None:
            print('111set....')
            condition.wait()
            print('222set....')
        for i in range(len(alist))[::-1]:
            alist[i] = 1
        condition.release()

def do_print():
    if condition.acquire():
        while alist is None:
            print('111print....')
            condition.wait()
            print('222print....')
        for i in alist:
            print(i)
        condition.release()

def do_create():
    global alist
    if condition.acquire():
        if alist is None:
            alist = [0 for i in range(10)]
            condition.notifyAll()
        condition.release()

t_set = threading.Thread(target=do_set,name='t_set')
t_print = threading.Thread(target=do_print,name='t_print')
t_create = threading.Thread(target=do_create,name='t_create')
t_set.start()
t_print.start()
t_create.start()

  代码解读:t_set、t_print、t_create三个子进程创建后,t_set、t_print进入等待通知状态,子进程t_create先获得锁定,执行do_create方法,创建列表alist=[0,0,0,0,0,0,0,0,0,0],调用condition.notifyAll()方法,通知等待池中的所有子进程(t_set、t_print),所有子进程进入锁定池尝试获得锁定;

  • 若t_set先获取锁定,执行do_set()方法,将alist列表数据全置为1,t_set释放锁定,t_print获得锁定,执行do_print()方法,读取alist中的数据,结果全为1;
  • 若t_print先获取锁定,执行do_print()方法,读取alist中的数据,结果全为0;t_print释放锁定,t_set获得锁定,执行do_set()方法,将alist列表数据全置为1;

总结

(1)线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。
(2)多进程和多线程的程序涉及到同步、数据共享的问题,编写起来更复杂;
(3)在Linux/Unix下,可以使用fork()调用实现多进程;要实现跨平台的多进程,可以使用multiprocessing模块;进程间通信通过Queue、Pipes等实现的;
(4)多线程编程模型负责,容易发生冲突,必须用锁加以隔离,同时又要小心死锁的发生;python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核,多线程的并发在python中就是一个美丽的梦;

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:1024 设计师:我叫白小胖 返回首页
评论 1

打赏作者

〃旅行走失的猫〃

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值