我心中的王者:Python-多任务与多线程

我心中的王者:Python-多任务与多线程

如果我们打开计算机可以看到在Windows操作系统下可以同时执行多个应用程序,例如,当你使用浏览器下载数据期间,可以使用Word编辑文件,同时间可能Outlook告诉你收到了一封电子邮件,其实这种作业类型就称多任务作业。

相同的观念可以应用在程序设计,我们可以使用Python设计一个程序执行多个子程序,这个观念称一个程序内有好几个进程(process),然后我们也可以使用Python设计一个进程(process)内含有多个线程(threading)。过去我们使用Python所设计的程序只专注执行一件事情,我们也可以称之为单进程内有单线程,这一章我们将讲解一个程序可以执行多个工作进程(process)的概念,同时也介绍一个进程内含有多个线程。

另外,这一章也将讲解另一个时间模块datatime和如何从Python启动其他应用程序。

30-1 时间模块datetime

在13-6节笔者讲解了时间模块time,这一节将讲解另一个时间模块datetime,在使用前需导入此模块。

 import datetime

30-1-1 datetime模块的数据类型datetime

datetime模块内有一个数据类型datetime,可以用它代表一个特定时间,有一个now( )方法可以列出现在时间。

程序实例ch30_1.py:列出现在时间。

# ch30_1.py
import datetime

timeNow = datetime.datetime.now()
print(type(timeNow))
print("列出现在时间 : ", timeNow)

执行结果

<class 'datetime.datetime'>
列出现在时间 :  2024-08-03 18:14:48.083691

我们也可以使用属性year、month、day、hour、minute、second、microsecond(百万分之一秒),获得上述时间的个别内容。

程序实例ch30_2.py:列出时间的个别内容。

# ch30_2.py
import datetime

timeNow = datetime.datetime.now()
print(type(timeNow))
print("列出现在时间 : ", timeNow)
print("年 : ", timeNow.year)
print("月 : ", timeNow.month)
print("日 : ", timeNow.day)
print("时 : ", timeNow.hour)
print("分 : ", timeNow.minute)
print("秒 : ", timeNow.second)

执行结果

<class 'datetime.datetime'>
列出现在时间 :  2024-08-03 18:15:25.326441:  2024:  8:  3:  18:  15:  25

另一个属性百万分之一秒microsecond,程序一般比较少用。

30-1-2 设定特定时间

当你了解了获得现在时间的方式后,其实可以用下列方法设定一个特定时间。

 xtime = datetime.datetime(年, 月, 日, 时, 分, 秒)

上述xtime就是一个特定时间。

程序实例ch30_3.py:设定程序循环执行到2017年11月16日14点54分0秒将停止打印“program is sleeping.”,同时打印“Wake up”。

# ch30_3.py
import datetime

timeStop = datetime.datetime(2017, 11, 16, 14, 54, 0)
while datetime.datetime.now() < timeStop:
    print("program is sleeping.", end="")
print("Wake up")

执行结果

Wake up

30-1-3 一段时间timedelta

这是datetime的数据类型,代表的是一段时间,可以用下列方式指定一段时间。

 deltaTime=datetime.timedelta(weeks=xx,days=xx,hours=xx,minutes=xx,seocnds=xx)

上述xx代表设定的单位数。

一段时间的对象只有3个属性,days代表天数、seconds代表秒数、microseconds代表百万分之一秒。

程序实例ch30_4.py:打印一段时间的天数、秒数和百万分之几秒。

# ch30_4.py
import datetime

deltaTime = datetime.timedelta(days=3, hours=5, minutes=8, seconds=10)
print(deltaTime.days, deltaTime.seconds, deltaTime.microseconds)

执行结果

3 18490 0

上述5小时8分10秒被总计为18940秒。有一个方法total_second( )可以将一段时间转成秒数。

程序实例ch30_5.py:重新设计ch30_4.py,将一段时间转成秒数。

# ch30_5.py
import datetime

deltaTime = datetime.timedelta(days=3, hours=5, minutes=8, seconds=10)
print(deltaTime.total_seconds())

执行结果

277690.0

30-1-4 日期与一段时间相加的应用

Python允许时间相加,例如,想要知道过了n天之后的日期,可以使用这个应用。

程序实例ch30_6.py:列出过了100天后的日期。

# ch30_6.py
import datetime

deltaTime = datetime.timedelta(days=100)
timeNow = datetime.datetime.now()
print("现在时间是 : ", timeNow)
print("100天后是  : ", timeNow + deltaTime)

执行结果

现在时间是 :  2024-08-03 18:18:35.122008
100天后是  :  2024-11-11 18:18:35.122008

当然利用上述方法也可以推算100天前是几月几号。

30-1-5 将datetime对象转成字符串

strftime( )方法可以将datatime对象转成字符串,这个指令的参数定义如下:
在这里插入图片描述

程序实例ch30_7.py:将现在日期转成字符串格式,同时用不同格式显示。

# ch30_7.py
import datetime

timeNow = datetime.datetime.now()
print(timeNow.strftime("%Y/%m/%d %H:%M:%S"))
print(timeNow.strftime("%y-%b-%d %H-%M-%S"))

执行结果

2024/08/03 18:19:47
24-Aug-03 18-19-47

有关字符串转成日期观念可以参考23-7-6小节。

30-2 多线程

在商业化的应用设计时,通常会为一个程序设计多个线程,大都不会让一个线程占据系统所有资源,例如,Word设计时,有一个线程是处理编辑窗口随时监听是否有屏幕输入可实时编排版面,同一时间也有Word的线程在做编辑字数统计随时更新Word的窗口状态栏。这一节将讲解这方面的设计观念。

30-2-1 一个睡眠程序设计

在讲解多线程前,我们可以先看下列程序实例。

程序实例ch30_8.py:假设现在是2020年1月1日,你太在乎女朋友,想要程序在女朋友生日1月20日当天提醒自己送礼物,可能你的程序可以这样设计。

# ch30_8.py
import datetime

timeStop = datetime.datetime(2025, 1, 1, 8, 0, 0)
while datetime.datetime.now() < timeStop:
    pass
print("女朋友生日")

执行结果 这个程序要到2025年1月1日早上8点才会苏醒,可以用Ctrl-C键中断执行。

30-2-2 建立一个简单的多线程

为了解决程序被霸占资源无法执行的后果,我们可以使用多线程的观念,例如,给上述调用循环一个线程,然后我们程序可以作为主线程继续执行应有的工作。建立线程需要导入threading模块,如下所示:

 import threading

我们可以使用下列方式导入线程:

在这里插入图片描述

从上述我们可以发现要建立与执行一个线程,需要threading模块的Thread( )方法定义一个Thread对象,同时又需设定此Thread对象所要执行的工作,用函数设定工作内容。此处threadObj是一个对象名称,读者可以自己取任意名称,同时这个方法内需用关键词target设定所要调用的函数,此处threadWork是函数名称,读者可以自己取这个名称。所以这一行定义了线程的对象名称和所要执行的工作。

要启动线程则需使用start( )方法。

程序实例ch30_9.py:设计一个线程单独执行工作,程序本身也执行工作。

# ch30_9.py
import threading, time

def wakeUp():
    print("threadObj线程开始")
    time.sleep(10)              # threadObj线程休息10秒
    print("女朋友生日")
    print("threadObj线程结束")
    
print("程序阶段1")
threadObj = threading.Thread(target=wakeUp)
threadObj.start()               # threadObj线程开始工作
time.sleep(1)                   # 主线程休息1秒
print("程序阶段2")

执行结果

程序阶段1
threadObj线程开始
程序阶段2
女朋友生日
threadOb

其实在测试多线程工作时,通常会在命令提示符模式下执行,这也是未来本书使用方式。

30-2-3 参数的传送

从ch30_9.py可以看到在Thread( )调用函数时,只是填上函数的名称,如果函数需要有传递参数时应如何设计传递参数的方法呢?此时可以增加Thread( )的参数,如下所示:

 threadObj = threading.Thread(target=函数名称, args=[‘xx', … ,‘yy'])

程序实例ch30_10.py:线程调用函数传递参数的应用。

# ch30_10.py
import threading, time

def wakeUp(name, blessingWord):
    print("threadObj线程开始")
    time.sleep(10)              # threadObj线程休息10秒
    print(name, " ", blessingWord)
    print("threadObj线程结束")
    
print("程序阶段1")
threadObj = threading.Thread(target=wakeUp, args=['NaNa','生日快乐'])
threadObj.start()               # threadObj线程开始工作
time.sleep(1)                   # 主线程休息1秒
print("程序阶段2")

执行结果

程序阶段1
threadObj线程开始
程序阶段2
NaNa   生日快乐
threadObj线程结束

设计多线程程序最重要的观念是,各线程间不要使用相同的变量,每个线程最好使用本身的局部变量,这可以避免变量值互相干扰。

30-2-4 线程的命名与取得

每一个线程在产生的时候,如果我们没有给它命名,为了方便日后的管理,Python会自动给这个线程预设名称Thread-n,n是序列号,由1开始编号。可以使用currentThread().getName( )获得线程的名称。

程序实例ch30_11.py:建立线程同时列出线程的名称。

# ch30_11.py
import threading
import time

def worker():
    print(threading.currentThread().getName(), 'Starting')
    time.sleep(2)
    print(threading.currentThread().getName(), 'Exiting')

def manager():
    print(threading.currentThread().getName(), 'Starting')
    time.sleep(3)
    print(threading.currentThread().getName(), 'Exiting')

m = threading.Thread(target=manager)
w = threading.Thread(target=worker)
m.start()
w.start()

执行结果

Thread-1 Starting
Thread-2 Starting
Thread-2 Exiting
Thread-1 Exiting

当然我们也可以在使用Thread( )建立线程时,在参数字段用name=“名称”,直接输入线程的名称,这相当于为线程命名。

程序实例ch30_12.py:扩充设计ch30_11.py自行为线程命名,读者可以留意第16行为线程的命名方式。

# ch30_12.py
import threading
import time

def worker():
    print(threading.currentThread().getName(), 'Starting')
    time.sleep(2)
    print(threading.currentThread().getName(), 'Exiting')
def manager():
    print(threading.currentThread().getName(), 'Starting')
    time.sleep(3)
    print(threading.currentThread().getName(), 'Exiting')

m = threading.Thread(target=manager)
w = threading.Thread(target=worker)
w2 = threading.Thread(name='Manager',target=worker)
m.start()
w.start()
w2.start()

执行结果

Thread-1 Starting
Thread-2 Starting
Manager Starting
Thread-2 Exiting
Manager Exiting
Thread-1 Exiting

另外也可以使用currentThread( ).setName( )为线程命名。

30-2-5 Daemon线程

在预设情况下,所有的线程皆不是Daemon线程(可以翻译为守护线程)。在默认情况下,如果一个程序建立了主线程与其他子线程,在所有线程工作结束后,程序才会结束。因为如果主线程若是先结束,将退回所有所占据的资源给操作系统,如果子线程仍在执行将会因没有资源造成程序崩溃。

但是当我们设计一个线程是Daemon线程时,主线程若是想要结束执行会检查Daemon线程的属性。

(1)如果此时Daemon线程的属性是True,即使Daemon线程仍在执行,其他非Daemon线程执行结束,程序将不等待Daemon线程,也会自行结束同时终止此Daemon线程工作。

(2)如果此时Daemon线程的属性是False,主线程会等待Daemon线程结束,再结束工作。

程序实例30_13.py:这个程序在执行时,将不等待daemon线程结束,而自行结束工作,由于程序已经结束,所以我们看不到第8行daemon Exiting的输出。

# ch30_13.py
import threading
import time

def daemonFun():                                                # 定义Daemon
    print(threading.currentThread().getName(), 'Starting')
    time.sleep(5)
    print(threading.currentThread().getName(), 'Exiting')
def non_daemon():                                               # 定义非Daemon
    print(threading.currentThread().getName(), 'Starting')
    print(threading.currentThread().getName(), 'Exiting')

d = threading.Thread(name='daemon', target=daemonFun)           # 建立Daemon
d.setDaemon(True)                                               # 设为True
nd = threading.Thread(name='non-daemon', target=non_daemon)     # 建立非Daemon

d.start()
nd.start()

执行结果

daemon Starting
non-daemon Starting
non-daemon Exiting

程序实例ch30_14.py:重新设计ch30_13.py,但是将Daemon线程的属性设为False,在观察执行结果时可以发现主线程等待Daemon线程结束后才结束工作。

# ch30_14.py
import threading
import time

def daemonFun():                                                # 定义Daemon
    print(threading.currentThread().getName(), 'Starting')
    time.sleep(5)
    print(threading.currentThread().getName(), 'Exiting')
def non_daemon():                                               # 定义非Daemon
    print(threading.currentThread().getName(), 'Starting')
    print(threading.currentThread().getName(), 'Exiting')

d = threading.Thread(name='daemon', target=daemonFun)           # 建立Daemon
d.setDaemon(False)                                              # 设为False
nd = threading.Thread(name='non-daemon', target=non_daemon)     # 建立非Daemon

d.start()
nd.start()

执行结果

daemon Starting
non-daemon Starting
non-daemon Exiting
daemon Exiting

30-2-6 堵塞主线程join( )

主线程在工作时,如果想要安插一个子线程进来,可以使用join( ),这时安插进来的子线程可以先工作,直到所邀请的子线程结束,主线程才开始工作。

程序实例ch30_15.py:这个程序执行时会因为worker线程的加入(第13行),主线程会等待此worker线程工作结束,再开始往下工作。

# ch30_15.py
import threading
import time

def worker():
    print(threading.currentThread().getName(), 'Starting')
    time.sleep(3)
    print(threading.currentThread().getName(), 'Exiting')

w = threading.Thread(name='worker',target=worker)
w.start()
print('start join')
w.join()                # worker线程工作完成才往下执行
print('end join')

执行结果

worker Starting
start join
worker Exiting
end join

为了怕等待所安插进来的子线程工作太久,可以在join( )内增加秒数的实数参数,代表所等待的时间,当时间到时主线程恢复工作,这时所安插进来的子线程仍是继续工作。

程序实例ch30_16.py:重新设计ch30_15.py,设计等待时间是1.5秒,当等待时间超过1.5秒后,主线程将恢复工作,所以在执行结果可以看到会先打印end join字符串,然后worker Exiting才被打印。
在这里插入图片描述

执行结果

worker Starting
start join
end join
worker Exiting

30-2-7 检查子线程是否仍在工作isAlive( )

通常在使用join(time unit)方法,同时设定等待一段时间后程序设计师会在join( )后面加上isAlive( )方法,检查子线程是否工作结束了,如果是则传回False或因为时间到交出执行的权利给主线程,表示仍在工作此时会传会True。

程序实例ch30_17.py:扩充设计ch30_16.py,列出主线程取得控制权时,子线程是否仍在工作,读者应注意第14和17行。

# ch30_17.py
import threading
import time

def worker():
    print(threading.currentThread().getName(), 'Starting')
    time.sleep(3)
    print(threading.currentThread().getName(), 'Exiting')

w = threading.Thread(name='worker',target=worker)
w.start()
print('start join')
w.join(1.5)             # 等待worker线程1.5秒工作完成才往下执行
print("是否working线程仍在工作 ? ", w.isAlive())
time.sleep(2)           # 主线程休息2秒
print("是否working线程仍在工作 ? ", w.isAlive())
print('end join')

执行结果

worker Starting
start join
是否working线程仍在工作 ?  True
worker Exiting
是否working线程仍在工作 ?  False
end join

30-2-8 了解正在工作的线程

下列是与正在工作线程相关的方法。
在这里插入图片描述

程序实例ch30_18.py:列出在工作中的线程数量和这些线程名称。

# ch30_18.py
import threading
import time

def worker():
    print(threading.currentThread().getName(), 'Starting')
    time.sleep(5)
    print(threading.currentThread().getName(), 'Exiting')
def manager():
    print(threading.currentThread().getName(), 'Starting')
    time.sleep(5)
    print(threading.currentThread().getName(), 'Exiting')
    
w = threading.Thread(name='worker',target=worker)
w.start()
print('worker start join')
w.join(1.5)             # 等待worker线程1.5秒工作完成才往下执行
print('worker end join')
m = threading.Thread(name='manager',target=worker)
m.start()
print('manager start join')
w.join(1.5)             # 等待manager线程1.5秒工作完成才往下执行
print('manager end join')
print("目前共有 %d 线程在工作" % threading.active_count())
for thread in threading.enumerate():
    print("线程名称 : ", thread.name)

执行结果

worker Starting
worker start join
worker end join
manager Starting
manager start join
manager end join
目前共有 3 线程在工作
线程名称 :  MainThread
线程名称 :  worker
线程名称 :  manager
worker Exiting
manager Exiting

30-2-9 自行定义线程和run( )方法

其实threading.Thread是threading模块内的一个类别,我们可以自行设计一个类别,让这个类别继承threading.Thread类别,接着需在def init( )内调用threading_Thread._init( )方法,然后在所设计的类别内可以设计run( )方法,这个观念就称自行定义线程。假设所设计的类别是MyThread,未来只要声明所设计类别的对象,如下所示:

 obj = MyThread( )  # 建立自行定义线程对象

然后执行run( )方法,就可以启动自行定义的线程。

 obj.run( )  # 启动自行定义的线程

过去几节我们使用threading.Thread( )声明一个线程对象时,再执行start( )可以建立一个线程,其实start( )就是辗转调用此threading.Thread类别的run( )方法开始执行工作。不过这种方式线程只能调用一次start( )方法,重复调用时会有错误。我们使用自定义的线程时,可以调用run( )方法多次,不会引发错误。

程序实例ch30_19.py:测试自行定义的线程a,启动run( ),2次,结果可以正常执行。测试自行定义的线程b,启动start( ),1次,可以正常。

# ch30_19.py
import threading  
  
class MyThread(threading.Thread):       # 这是threading.Thread的子类别
    def __init__(self):  
        threading.Thread.__init__(self) # 建立线程       
    def run(self):                      # 定义线程的工作
        print(threading.Thread.getName(self))
        print("Happy Python")
  
a = MyThread()                          # 建立线程对象a
a.run()                                 # 启动线程a
a.run()                                 # 启动线程a       
b = MyThread()                          # 建立线程对象b
b.start()                               # 启动线程b

执行结果

Thread-1
Happy Python
Thread-1
Happy Python
Thread-2
Happy Python

程序实例ch30_20.py:如果我们在第16行再增加一个b.start( ),就会产生错误。
在这里插入图片描述

执行结果
在这里插入图片描述

错误原因是start( )只能被启动一次。

30-2-10 资源锁定与解锁Threading.Lock

在多线程的程序设计中,可能会有多个线程皆要存取相同的资源,为了确保线程在处理资源期间,可以完成处理不被干扰,此时可以使用Python的锁定功能Threading.Lock,这个锁定功能有锁定与未锁定2种状态。在未锁定状态可以使用acquire( )方法进入锁定状态,此时所锁定的资源别的线程无法存取,当处理资源完成,可以使用release( )方法,将锁定状态改为未锁定状态。
在这里插入图片描述

程序实例ch30_21.py:这个程序会对全局变量进行存取,为了保护顺序处理原则,存取前先锁定全局变量,处理完成后再解锁。

# ch30_21.py
import threading
  
class MyThread(threading.Thread):       # 这是threading.Thread的子类别
    def __init__(self):  
        threading.Thread.__init__(self) # 建立线程       
    def run(self):
        global data                     # 定义全局数据
        datalock.acquire()              # 锁定
        data += 5
        print("data = ", data)
        datalock.release()              # 解锁

data = 10                               # 全局最初值
datalock = threading.Lock()             # 建立对象
ts = []                                 # 建立线程列表
for t in range(10):
    t = MyThread()
    ts.append(t)                        # 将持行绪加入线程列表

for t in ts:                            # 启动所有线程
    t.start()

for t in ts:                            # 等待所有线程退出
    t.join()

执行结果 这个程序在Python Shell窗口更可以看出差异。

data =  15
data =  20
data =  25
data =  30
data =  35
data =  40
data =  45
data =  50
data =  55
data =  60

从上图可以看到数据符合预期,依序列出。下列实例是,我们不对数据进行锁定,各线程无法预期谁会先取得资源然后进行数据处理,这种现象称为竞速(race condition)。

程序实例ch30_22.py:重新设计ch30_21.py,但是取消第9和12行。
在这里插入图片描述

执行结果 每次执行都获得不一样的结果。

在这里插入图片描述

30-2-11 产生锁死

在使用Threading.Lock时,如果目前是锁定状态(locked),再执行一次acquire( )这样会产生锁死(dead lock),造成程序错误。

程序实例ch30_23.py:程序产生锁死(dead lock)测试,笔者使用15-7节的logging模块做追踪。

# ch30_23.py
import threading, logging
logging.basicConfig(level=logging.DEBUG)
datalock = threading.Lock()     # Lock物件
datalock.acquire()              # 进入锁定
logging.debug('Enter locked mode')
datalock.acquire()              # 进入死锁程序当机
logging.debug('Trying to locked again')
datalock.release()
datalock.release()

执行结果 由于锁死产生,所以无法显示Trying to lock again字符串。

DEBUG:root:Enter locked mode

30-2-12 资源锁定与解锁Threading.RLock

这是另一种资源锁定与解锁,在相同线程下这种锁允许在锁定状态时,再度执行一次acquire( ),差异是acquire( )和release( )需要成对出现,如果使用n次acquire( ),就必须使用n次release( )解锁。

程序实例ch30_24.py:使用Threading.Rlock重新设计ch30_23.py,程序不会产生锁死(dead lock)测试。

执行结果

DEBUG:root:Enter locked mode
DEBUG:root:Trying to locked again

30-2-13 高级锁定threading.Condition

这是Python的另一种锁定,就像它的名称一样是可以有条件的(condition)。首先程序使用acquire( )进入锁定状态,如果需要符合一定的条件才处理数据,此时可以调用wait( ),让自己进入睡眠状态。程序设计时需用notify( )通知其他线程,然后放弃锁定release( )。

此时其他在等待的线程因为收到通知notify( ),这时被激活了,就可以开始运作。

程序实例ch30_25.py:生产者和消费者的设计,这个程序用producer( )方法叙述生产者运作方式,基本上是需要生产5个数据(在data列表)才让自己进入睡眠状态,然后通知其他线程(第14行),再解锁(第15行)。consumer( )方法则是当data列表没有数据时,才让自己进入睡眠状态,然后通知其他线程(第27行),再解锁(第28行)。这个程序首先建立threading.Condition( )(第30行),然后设定资源列表data是空的(第31行),程序接着是建立线程与启动线程。由于producer( )和consumer( )方法皆是一个无限循环(第5~15行,第18~28行)所以程序将持续进行。

# ch30_25.py
import threading, time, random

def producer():                                         # 生产者状况                      
    while True:
        condition.acquire()                             # 锁定
        if len(data) >= 5:                              # 如果产品满了
            print("生产线是 waiting ...")                   
            condition.wait()                            # 生产者等待
        else:
            data.append(random.randint(1, 100))         # 将产品放入库存
            print("生产线库存          ", data)         # 打印库存
            time.sleep(1)
        condition.notify()                              # 通知
        condition.release()                             # 解锁

def consumer():                                         # 消费者状况
    while True:
        condition.acquire()                             # 锁定
        if not data:                                    # 如果没有产品
            print("消费者是 waiting ...")
            condition.wait()                            # 消费者等待
        else:
            print("消费者取走商品 : ", data.pop(0))
            print("目前库存            ", data)         # 打印库存       
            time.sleep(1)
        condition.notify()                              # 通知
        condition.release()                             # 解锁

condition = threading.Condition()                       # 建立Condition对象
data = []                                               # 最初化库存

p = threading.Thread(name='producer',target=producer)   # 建立producer线程   
c = threading.Thread(name='consumer',target=consumer)   # 建立consumer线程

p.start()
c.start()
p.join()
c.join()

执行结果 下列是部分执行过程。

目前库存             [12, 64, 16]
生产线库存           [12, 64, 16, 64]
生产线库存           [12, 64, 16, 64, 6]
生产线是 waiting ...
消费者取走商品 :  12
目前库存             [64, 16, 64, 6]
消费者取走商品 :  64
目前库存             [16, 64, 6]
消费者取走商品 :  16
目前库存             [64, 6]
消费者取走商品 :  64
目前库存             [6]
消费者取走商品 :  6

在程序设计中也可以在wait( )设定等待秒数的参数,另外,以上述实例而言若是另外增加一个消费者时,则可以在通知时可以使用notifyAll( )。

30-2-14 queue

在Python内有一个queue模块,这是一种先进先出的数据结构,可以使用put( )方法插入元素,使用get( )方法取得元素,元素取得后此元素将在queue内被移除,它的基本观念图如下:

在这里插入图片描述

由于在设计queue的逻辑上,要在queue中使用put( )插入元素时,系统处理锁定逻辑,另外,如果queue空间已满,put( )会在内部调用wait( )进行等待。使用get( )取得元素和移除元素时,系统内部也会进行锁定。如果queue空间是空的,get( )会在内部调用wait( )进行等待。

基于以上特性,一般也可以使用queue处理生产者(producer)和消费者(consumer)的问题。另外,使用queue时,需要导入queue。

程序实例ch30_26.py:使用queue观念,应用到生产者和消费者的问题。这个程序在执行时,首先定义queue最大空间是10(第4~5行),第7~13行是producer线程设计,只要queue空间尚未满,就会生产数据然后存入queue(第10~11行)。第15~20行是consumer线程设计,只要queue空间不是空的,就会读取和移除数据(第18行)。

# ch30_26.py
import threading, time, random, queue

bufSize = 10
q = queue.Queue(bufSize)                                # 建立queue,最多10笔

def producer():                                         # 生产者状况                      
    while True:
        if not q.full():                                # 如果queue有空间
            item = random.randint(1,100)                # 生产产品
            q.put(item)                                 # 将产品放入库存
            print('生产者Putting存入 %2s : queue数量 %s ' % (str(item), str(q.qsize()))) 
            time.sleep(2)                               # 休息2秒
            
def consumer():                                         # 消费者状况
    while True:
        if not q.empty():                               # 如果queue不是空的
            item = q.get()                              # 消欸产品
            print('消费者Getting取得 %2s : queue数量 %s ' % (str(item), str(q.qsize())))      
            time.sleep(2)                               # 休息2秒

p = threading.Thread(name='producer',target=producer)   # 建立producer线程   
c = threading.Thread(name='consumer',target=consumer)   # 建立consumer线程
p.start()
time.sleep(2)
c.start()
time.sleep(2)

执行结果 这是一个无限循环的设计。

生产者Putting存入 89 : queue数量 1 
生产者Putting存入 18 : queue数量 2 
消费者Getting取得 89 : queue数量 1 
生产者Putting存入 79 : queue数量 2 
消费者Getting取得 18 : queue数量 1 
消费者Getting取得 79 : queue数量 0 
生产者Putting存入 36 : queue数量 1 
生产者Putting存入 61 : queue数量 2 
消费者Getting取得 36 : queue数量 1 

30-2-15 Semaphore

Semaphore可以翻译为信号量,这个信号量代表最多允许线程访问的数量,可以使用Semaphore(n)设定,n是信号数量。这是一个更高级的锁机制,Semaphore管理一个计数器,每次使用acquire( )计数器将减1,表示可允许线程访问的数量少了一个。使用release( )计数器将加1,表示可允许线程访问的数量增加了一个。只有占用信号量的线程数量超过信号量时,才会阻塞,也就是说计数器为0时,若还有线程要访问,则发生阻塞。

发生阻塞后就需要等待其他线程使用release( ),这时计数器会加1,然后被阻塞的线程就可以访问了。

在应用Semaphore过程,有时候可能会因为bug造成调用多次release( ),因此有所谓的BoundedSemaphore,可以保证计数器次数不超过特定值,这时使用BoundedSemaphore(n)设定,n是信号数量。

程序实例ch30_27.py:这个程序在建立semaphore时就设定了最大计数值是3,程序第8~13行记录了计数值响应线程取得资源的情形。

# ch30_27.py
import time
import threading
 
semaphore = threading.BoundedSemaphore(3)                       # 限制计数器最大值
 
def func():
    if semaphore.acquire():                                     # 如果取得锁
        print (threading.currentThread().getName() + ' 取得锁')
        print("Working ...")
        time.sleep(2)
        semaphore.release()
        print (threading.currentThread().getName() + ' 释出锁')
  
for i in range(5):
    t = threading.Thread(target=func)
    t.start()

执行结果

Thread-1 取得锁
Working ...
Thread-2 取得锁
Working ...
Thread-3 取得锁
Working ...
Thread-1 释出锁
Thread-4 取得锁
Working ...
Thread-3 释出锁
Thread-2 释出锁
Thread-5 取得锁
Working ...
Thread-5 释出锁
Thread-4 释出锁

30-2-16 Barrier

Barrier可以翻译为栅栏,可以想成赛马的栅栏,当线程抵达时需等待其他线程,当所有线程抵达时,才放开栅栏,这些线程才可以往下执行。

程序实例ch30_28.py:这个程序第11行会使用Barrier( )将等待线程数量设为4,这时会建立Barrier对象b,然后可以使用b.wait( )执行等待。

# ch30_28.py
import random, time
import threading
                                            
def player():
    name = threading.current_thread().getName()
    time.sleep(random.randint(2,5))
    print('%s 抵达栅栏时间 : %s' % (name, time.ctime()))
    b.wait()

b = threading.Barrier(4)                            # 等待的线程数量    
print('比赛开始 …')
for i in range(4):
    t = threading.Thread(target=player)
    t.start()
for i in range(4):                                  # 等待线程结束
    t.join()
print('比赛结束!')

执行结果

比赛开始 …
Thread-4 抵达栅栏时间 : Sat Aug  3 18:47:45 2024
Thread-3 抵达栅栏时间 : Sat Aug  3 18:47:46 2024
Thread-2 抵达栅栏时间 : Sat Aug  3 18:47:46 2024
Thread-1 抵达栅栏时间 : Sat Aug  3 18:47:47 2024

比赛结束!

30-2-17 Event

这是一种线程的通信技术,通常会有2个线程,一个线程主要是设定Event的flag,可以使用set( )设定flag。另一个线程则是等待Event的flag,可以使wait( )等待,当接收到flag信号工作完成后,可以使用clear( )清除flag信号。操作上用Event( )建立Event对象。

程序实例ch30_29.py:分别建立w线程(waiter)和s线程(setter),w线程会等待s线程将flag打开(第14行),打开后第7行w线程的等待就结束,第8行列出等待完成时间,第9行将flag重置,所以下一个循环新的w线程又会进入等待状态。

# ch30_29.py
import random, time
import threading 

def waiter(event, loop):
    for i in range(loop):
        print('%s. 等待flag被设定' % (i+1))
        event.wait()                        # 等待flag
        print('等待完成时间 : ', time.ctime())
        event.clear()                       # 重置flag.
        print()

def setter(event, loop):
    for i in range(loop):
        time.sleep(random.randint(2, 5))    # 休息一段时间再工作
        event.set()                         # 设定flag

event = threading.Event()                   # 建立Event对象
loop = random.randint(3, 6)                 # 循环次数

w = threading.Thread(target=waiter, args=(event,loop))
w.start()
s = threading.Thread(target=setter, args=(event,loop))    
s.start()
w.join()
s.join
print('工作完成!')

执行结果

1. 等待flag被设定
等待完成时间 :  Sat Aug  3 18:48:40 2024

2. 等待flag被设定
等待完成时间 :  Sat Aug  3 18:48:45 2024

3. 等待flag被设定
等待完成时间 :  Sat Aug  3 18:48:48 2024

工作完成!

30-3 启动其他应用程序subprocess模块

subprocess是Python的内置模块,主要是可以在程序内建立子进程,使用前需导入此模块。

 import subprocess

30-3-1 Popen( )

Popen( )方法可以打开计算机内其他应用程序,有的是Windows系统内置的应用程序或是自己开发的应用程序。当我们所设计的Python程序使用Popen( )打开其他应用程序时,我们也可以将所设计的Python程序称是多进程的应用程序。

当我们安装Windows操作系统后,在C:\Windows\System32文件夹内可以看到许多Windows应用程序,这一节将使用下列3个应用程序为实例说明。

计算器:calc.exe

记事本:notepad.exe

写字板:write.exe

由于C:\Windows\System32在Windows安装时已经主动被设在path路径内,所以我们应用时,直接使用文件名即可。如果打开的是其他应用程序,其路径未被设在path,则需要填上完整的路径名称。

程序实例ch30_30.py:打开计算器、记事本、写字板(WordPad)应用程序,这个程序同时会列出应用程序的数据类型,当打印程序时,可以看到这个程序在内存的位置。

# ch30_30.py
import subprocess

calcPro = subprocess.Popen('calc.exe')      # 传回值是子行程
notePro = subprocess.Popen('notepad.exe')   # 传回值是子行程
writePro = subprocess.Popen('write.exe')    # 传回值是子行程
print("数据型态           = ", type(calcPro))
print("打印calcPro  = ", calcPro)
print("打印notePro  = ", notePro)
print("打印writePro = ", writePro)

执行结果 下列分别是Python Shell窗口与所打开应用程序的结果。

数据型态           =  <class 'subprocess.Popen'>
打印calcPro  =  <subprocess.Popen object at 0x000001C0C32C9148>
打印notePro  =  <subprocess.Popen object at 0x000001C0C32C9248>
打印writePro =  <subprocess.Popen object at 0x000001C0C3366E88>

在这里插入图片描述

其实上述3个应用程序皆是独立的子进程,而主进程则是先执行结束了。

30-3-2 poll( )

这个方法会传回子进程是否已经完成工作结束了。如果仍在继续工作会传回None,如果已经执行结束且正常结束会传回0,如果已经执行结束但不正常结束会传回1。

下列是在执行完ch30_1.py后,立即执行poll( )的结果,因为calcPro(计算器)仍在屏幕执行,所以传回None。

在这里插入图片描述

如果我们现在关闭计算器应用程序,再执行poll( ),可以得到下列结果。

在这里插入图片描述

30-3-3 wait( )

这个方法会让这个子进程暂停执行,直到启动它的进程结束才开始工作。下列是验证记事本notePro仍在工作的实例。

在这里插入图片描述

下列是执行wait( )时,整个暂停,你只看见游标在闪烁。

在这里插入图片描述

假设我们现在关闭窗口的记事本应用程序,将看到下列结果。
在这里插入图片描述

如果子进程正常结束执行,在我们执行wait( )后也将传回0,如上所示。这一节的观念在设计大型多进程时是很有帮助的,因为可以了解各进程的工作状态,也可以控制是否让某个进程暂停工作。

30-3-4 Popen( )方法参数的传递

使用Popen( )方法时,也可以传递参数,此时会将所传递的参数用列表(list)处理,列表的第一个元素是想要打开的应用程序,第二个元素是这个应用程序相关的文件,下列将以实例解说。

程序实例ch30_31.py:打开画图mspaint.exe应用程序时,同时打开位于ch30文件夹内的winter.jpg。

# ch30_31.py
import subprocess

paintPro = subprocess.Popen(['mspaint.exe', 'winter.jpg'])
print(paintPro)

执行结果 下列分别是Python Shell窗口与所打开应用程序的执行结果。
在这里插入图片描述

当然在使用时Python程序也可以打开其他Python程序执行工作,这时彼此的变量独立运作行,不会互相干扰也无法共享。

程序实例ch30_32.py:在程序内启动ch30_30.py,程序执行后计算器、记事本、写字板(WordPad)应用程序将被启动。这个程序在执行时,读者需将第4行改为自己计算机的python.exe的路径。

# ch30_32.py
import subprocess

path = r'C:\Users\Jiin-Kwei\AppData\Local\Programs\Python\Python36-32\python.exe'
pyPro = subprocess.Popen([path, 'ch30_30.py'])
print(pyPro)

执行结果
在这里插入图片描述

所打开应用程序的结果可参考ch30_30.py的执行结果。

30-3-5 使用默认应用程序打开文件

当我们在使用Windows操作系统时,若是连按某个文件图标两下,系统会自动打开相关联的应用程序,然后将此文件图示打开。这是因为操作系统已经将常见类型的文件与相关应用程序做关联,在Windows操作系统这个程序是start,在Mac OS操作系统是open。在Windows操作系统下,我们也可以利用这个特性打开文件。

程序实例ch30_33.py:在Windows操作系统下,使用start程序打开trip.txt、book.jpg和pegium.m4v文件。

# ch30_33.py
import subprocess

txtPro = subprocess.Popen(['start', 'trip.txt'], shell=True)
pictPro = subprocess.Popen(['start', 'book.jpg'], shell=True)
m4vPro = subprocess.Popen(['start', 'pegiun.m4v'], shell=True)
print("txt档案程序  = ", txtPro)
print("pict档案程序 = ", pictPro)
print("m4v档案程序  = ", m4vPro)

执行结果 下列分别是Python Shell窗口与各应用程序的执行结果。
在这里插入图片描述

记住这个程序执行时,需要在Popen( )内增加shell=True参数。

30-3-6 subprocess.run( )

从Python 3.5版起,新增可以使用run( )调用子进程。

程序实例ch30_34.py:使用run( )调用子进程。

# ch30_34.py
import subprocess

calcPro = subprocess.run('calc.exe')      
print("数据型态           = ", type(calcPro))
print("打印calcPro  = ", calcPro)

执行结果 可以启动计算器,关闭计算器时可以看到下列结果。

数据型态           =  <class 'subprocess.CompletedProcess'>
打印calcPro  =  CompletedProcess(args='calc.exe', returncode=0)

请读者留意返回值是CompletedProcess数据类型,如果启动的是命令字符模式的指令,需增加参数shell=True,未来这个命令模式指令的返回值会存入CompletedProcess数据类型结构内,如果想要未来获得执行结果,可以增加stdout=subprocess.PIPE参数。

程序实例ch30_35.py:列出目前系统时间。

# ch30_35.py
import subprocess

ret = subprocess.run('echo %time%', shell=True, stdout=subprocess.PIPE)
print("数据型态           = ", type(ret))
print("打印ret  = ", ret)
print("打印ret.stdout", ret.stdout)

执行结果

数据型态           =  <class 'subprocess.CompletedProcess'>
打印ret  =  CompletedProcess(args='echo %time%', returncode=0, stdout=b'18:58:32.78\r\n')
打印ret.stdout b'18:58:32.78\r\n'

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值