# 有关python线程同步的运用
任务描述
排队报数是现实生活中很常见的一种统计人数的手段。它需要队列中的每个人互相协作,后一名比前一名报的数字多一,最后一名报的数字即为队列的总人数。 本关利用线程模拟队列中的个人,最终实现一个队列报数的过程。
相关知识
多线程是为了充分利用硬件资源,来提高任务处理速度和效率的技术。将任务拆分成互相协作的多个线程,同时运行。那么属于同一个任务的多个线程之间,就需要有交互和同步。 例如,字处理软件使用一个线程,来接受用户键盘输入,而使用另一个后台线程,来进行拼写检查以及字数统计等功能。
Thread对象
Thread
类支持使用两种方法,来创建线程:
- 为构造函数,传递一个可调用对象;
- 继承
Thread
类,并在派生类中重写__init__()
方法和run()
方法。
Thread
对象成员如表:
成员 | 对象 |
---|---|
start() | 自动调用run()方法,启动线程,执行线程代码 |
run() | 线程代码,用来实现线程的功能与业务逻辑,可以在子类中重写该方法来自定义线程的行为 |
init() | 构造函数 |
name | 用来读取或设置线程的名字 |
ident | 线程标识,非0数字或None(线程未被启动) |
isAlive() | 测试线程是否处于alive状态 |
daemon | 布尔值,表示线程是否为守护线程 |
join(timeout = None) | 等待线程结束或超时返回,timeout用来指定最长等待时间 |
创建线程对象以后,可以调用其start()
方法来启动,该方法自动调用该类对象的run()
方法,此时该线程处于alive
状态,直至线程的run()
方法运行结束。
import threading
import time
class mythread(threading.Thread):
def __init__(self, x, y):
threading.Thread.__init__(self)
self.x = x
self.y = y
def run(self):
for i in range(self.x, self.y):
print(i)
time.sleep(10)
t1 = mythread(15, 20)
t2 = mythread(5, 10)
t1.start()
t1.join(5)
t2.start()
t2.join()
保存并运行上面的程序,输出如下:
15 16 17 18 19 5 6 7 8 9
首先输出15-19
这5
个整数,然后程序暂停,几秒钟后又继续输出5-9
这5
个整数。如果将t1.join(5)
这一行注释掉,两个线程的输出将会重叠在一起,这是因为两个线程并发运行,而不是第一个结束后,再运行第二个。这是线程同步的一个最简单的形式。
Lock/RLock对象
资源总是有限的,多个线程如果对同一个对象进行操作,则有可能造成资源的争用,甚至导致死锁,也可能导致读写混乱。因此就需要同步技术进行限制。 Lock
是比较低级的同步原语,一个锁有两个状态: locked 和 unlocked。
- 如果处于
unlocked
状态,acquire()
方法将其修改为locked
,并立即返回;如果锁已经处于locked
状态,则阻塞当前线程,并等待其他线程释放锁,然后将其修改为locked
并立即返回; release()
方法用来将锁的状态,由locked
修改为unlocked
,并立即返回,如果锁状态本来就是unlocked
,调用该方法会抛出异常。
使用示例:
lock = threading.Lock()
lock.acquire()
#代码段
lock.release()
#以上代码可以保证lock中间的代码段执行过程中不被其他的线程影响。
锁RLock
对象可被同一个线程acquire()
多次。RLock
对象的acquire()/release()
调用可以嵌套,仅当最外层的release()
执行结束后,锁才会被设置为unlocked
状态。
编程要求
本关的编程任务是,补全右侧编辑器中Begin
至End
区间的代码,具体要求如下:
- 本关将根据测试输入创建多个线程,每个线程相当于队列中的一个人,他们报的数用全局变量
x
存储; - 学员需要编写
run()
方法,使得每个线程将自己该报的数输出; - 注意在输出语句之前,加入
time.sleep(0.1)
防止输出过快造成顺序混乱的情况。
测试说明
测试过程:
-
平台将运行用户补全的代码文件,并生成若干组测试数据;
-
接着根据程序的输出判断程序是否正确。
以下是测试样例:
测试输入:10
预期输出:12345678910
实验代码
import threading
import time
# 创建一个锁对象
lock = threading.Lock() # 创建一个 threading.Lock() 对象 lock
# 定义一个线程类,继承自 threading.Thread
class mythread(threading.Thread):
x = 0 # 全局变量 x
results = [] # 存储每个线程的输出值
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global x
#*********begin*********#
# 使用 lock.acquire() 获取锁,确保只有一个线程可以修改 x
lock.acquire()
# 使用 try 块确保在任何情况下都会释放锁 (lock.release()),即使在获取和修改 x 的过程中发生异常
try:
# 增加全局变量 x
mythread.x += 1
# 将当前值存入局部变量
current_value = mythread.x
finally:
# 无论是否发生异常,确保释放锁
lock.release()
# 等待 0.1 秒,防止输出过快
time.sleep(0.1)
# 将当前值加入结果列表中
mythread.results.append(current_value)
#********* end*********#
# 获取输入的线程数量
num = int(input())
# 创建线程列表
t1 = []
for i in range(num):
t = mythread()
t1.append(t)
# 初始化全局变量 x
mythread.x = 0
# 启动所有线程
for i in t1:
i.start()
# 等待所有线程执行完毕
for i in t1:
i.join()
# 确保结果按照顺序输出
mythread.results.sort()
output = ''.join(map(str, mythread.results)) # 将 mythread.results 列表中的所有数字连接成一个连续的字符串
print(output)
代码解释
-
锁的创建与使用:
- 创建一个
threading.Lock()
对象lock
。 - 在
run
方法中使用lock.acquire()
获取锁,确保只有一个线程可以访问和修改全局变量x
。 - 使用
try
块确保在任何情况下都会释放锁 (lock.release()
),即使在获取和修改x
的过程中发生异常。
- 创建一个
-
线程类
mythread
:- 继承自
threading.Thread
类。 - 定义一个类变量
x
用于存储全局计数器。 - 定义一个类变量
results
用于存储每个线程的输出值。 - 在
run
方法中获取锁,增加x
并将其值存入局部变量current_value
。 - 使用
try
块确保在任何情况下都会释放锁。 time.sleep(0.1)
用于防止输出过快。- 将
current_value
添加到mythread.results
列表中。
- 继承自
-
线程的创建和启动:
- 从输入读取线程数量
num
。 - 创建一个线程列表
t1
,并向其中添加指定数量的mythread
实例。 - 初始化全局变量
mythread.x
为 0。 - 启动所有线程,并使用
join
方法等待所有线程执行完毕。
- 从输入读取线程数量
-
输出结果:
- 由于
mythread.results
列表可能因为多线程的原因顺序不正确,使用sort()
方法对其进行排序。 - 使用
map
和str
函数将results
列表中的所有数字转换为字符串并连接成一个完整的字符串。 - 打印最终结果。
- 由于
代码片段解释
output = ''.join(map(str, mythread.results))
output = ''.join(map(str, mythread.results))
这一行代码是为了将 mythread.results
列表中的所有数字连接成一个连续的字符串,具体解释如下:
-
map
函数:map(function, iterable)
是一个内置函数,用于将function
应用于iterable
中的每个元素,返回一个包含结果的迭代器。- 在这个例子中,
map(str, mythread.results)
将mythread.results
列表中的每个元素都应用str
函数,将每个整数转换为字符串。 - 例如,如果
mythread.results
是[1, 2, 3, 4, 5]
,那么map(str, mythread.results)
的结果相当于['1', '2', '3', '4', '5']
。
-
join
方法:join
是一个字符串方法,用于连接iterable
中的每个元素,生成一个新的字符串。''.join(iterable)
将iterable
中的每个元素连接成一个字符串,并且在每个元素之间不添加任何分隔符(因为字符串''
是空的)。- 例如,
''.join(['1', '2', '3', '4', '5'])
的结果是'12345'
。
-
综合应用:
map(str, mythread.results)
将mythread.results
列表中的每个整数转换为字符串。''.join(map(str, mythread.results))
将这些字符串连接成一个单一的字符串。- 因此,如果
mythread.results
是[1, 2, 3, 4, 5]
,那么output = ''.join(map(str, mythread.results))
最终会得到output = '12345'
。
举例说明一下
# 示例代码
results = [1, 2, 3, 4, 5] # 假设这是线程运行后的结果
# 第一步:将整数转换为字符串
mapped_results = map(str, results)
# 第二步:将字符串连接成一个单一的字符串
output = ''.join(mapped_results)
# 打印最终的输出
print(output) # 输出 '12345'
- 将
results
列表中的每个元素转换为字符串:map(str, results)
的作用是创建一个新的迭代器,其中每个元素都是原results
列表中对应元素的字符串表示。例如,map(str, [1, 2, 3, 4, 5])
会生成一个迭代器,其内容类似于['1', '2', '3', '4', '5']
。
- 将这些字符串连接成一个连续的字符串:
''.join(mapped_results)
将上述迭代器中的所有字符串按顺序连接成一个新的字符串。由于使用的是空字符串''
作为分隔符,所以这些字符串之间不会有任何额外的字符插入。例如,''.join(['1', '2', '3', '4', '5'])
的结果是'12345'
。
补充说明
迭代器(iterator)是一个对象,它允许我们逐个访问一个集合中的元素,而不需要暴露其底层表示。迭代器遵循特定的协议,这个协议由两个核心方法组成:__iter__()
和 __next__()
。
迭代器协议
-
__iter__()
方法:- 返回迭代器对象本身。这个方法允许一个对象被用在
for
循环或其他迭代上下文中。 - 例如,一个列表在调用
__iter__()
时会返回一个列表迭代器对象。
- 返回迭代器对象本身。这个方法允许一个对象被用在
-
__next__()
方法:- 返回集合中的下一个元素。如果没有更多的元素,
__next__()
方法会抛出StopIteration
异常,表示迭代结束。 - 每次调用
__next__()
方法时,迭代器都会返回集合中的下一个元素。
- 返回集合中的下一个元素。如果没有更多的元素,
迭代器的基本示例
# 一个简单的迭代器类
class MyIterator:
def __init__(self, data):
self.data = data
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index < len(self.data):
result = self.data[self.index]
self.index += 1
return result
else:
raise StopIteration
# 使用自定义迭代器
data = [1, 2, 3, 4, 5]
iterator = MyIterator(data)
for item in iterator:
print(item)
在上面的示例中:
-
MyIterator
类:- 这是一个自定义的迭代器类,它接受一个数据集合并跟踪当前索引。
__iter__()
方法返回迭代器对象本身。__next__()
方法返回集合中的下一个元素,如果索引超出集合范围,抛出StopIteration
异常。
-
使用迭代器:
for item in iterator:
语句触发迭代器协议,for
循环自动调用迭代器的__iter__()
和__next__()
方法。
内置的迭代器
Python 内置的许多对象都实现了迭代器协议,例如列表、元组、字典、集合和文件对象。它们都可以用 iter()
函数来获得一个迭代器,并使用 next()
函数来获取下一个元素。
# 列表迭代器
my_list = [1, 2, 3]
iterator = iter(my_list)
print(next(iterator)) # 输出: 1
print(next(iterator)) # 输出: 2
print(next(iterator)) # 输出: 3
# print(next(iterator)) # 抛出 StopIteration 异常
在这个示例中:
-
iter(my_list)
:- 调用
iter()
函数将列表转换为迭代器对象。
- 调用
-
next(iterator)
:- 调用
next()
函数获取下一个元素,直到没有更多元素为止,此时会抛出StopIteration
异常。
- 调用
使用 map
函数返回迭代器
map
函数应用一个函数到一个或多个序列(或其他可迭代对象)的每个元素上,并返回一个迭代器。
# 示例:使用 map 将整数列表转换为字符串列表
numbers = [1, 2, 3, 4, 5]
mapped = map(str, numbers)
# mapped 是一个迭代器,可以用 next() 获取元素
print(next(mapped)) # 输出: '1'
print(next(mapped)) # 输出: '2'
# 也可以用 for 循环遍历
for item in mapped:
print(item) # 输出: '3', '4', '5'
在这个示例中:
-
map(str, numbers)
:- 返回一个迭代器,迭代器中的每个元素是
numbers
列表中的相应元素经过str
函数转换后的结果。
- 返回一个迭代器,迭代器中的每个元素是
-
next(mapped)
:- 使用
next()
函数获取迭代器的下一个元素。
- 使用
- 注意迭代器只能遍历一次,要将
mapped
迭代器的内容一次性不换行输出,我们可以将迭代器的内容转换为一个字符串并一次性输出。 -
这可以通过以下步骤实现:
- 将迭代器转换为一个列表,使用
list(mapped)
。 - 将列表中的每个元素转换为字符串,并连接成一个单一的字符串,使用
''.join()
。
- 将迭代器转换为一个列表,使用
具体代码如下:
# 示例:使用 map 将整数列表转换为字符串列表
numbers = [1, 2, 3, 4, 5]
mapped = map(str, numbers)
# 将迭代器转换为列表
mapped_list = list(mapped)
# 将列表中的字符串连接成一个单一的字符串
output = ''.join(mapped_list)
# 打印输出,不换行
print(output) # 输出: '12345'
-
将迭代器转换为列表:
mapped_list = list(mapped)
将map
返回的迭代器转换为一个列表。这样做的原因是迭代器只能遍历一次,转换为列表后可以多次访问其内容。
-
连接字符串:
output = ''.join(mapped_list)
使用空字符串''
作为分隔符,将列表中的所有元素连接成一个单一的字符串。由于mapped
已经将整数转换为字符串,所以直接使用join
方法即可。
-
打印输出:
print(output)
打印结果时不会换行,因为join
方法已经将所有元素连接成一个字符串。
如果不想显式地转换为列表,也可以直接使用 ''.join(map(str, numbers))
来一次性完成所有操作:
# 示例:使用 map 将整数列表转换为字符串列表并连接输出
numbers = [1, 2, 3, 4, 5]
# 直接将 map 结果连接成一个字符串
output = ''.join(map(str, numbers))
# 打印输出,不换行
print(output) # 输出: '12345'
这种方法更加简洁,不需要中间变量。
-
map(str, numbers)
:- 生成一个将
numbers
列表中每个元素转换为字符串的迭代器。
- 生成一个将
-
''.join(map(str, numbers))
:- 使用
join
方法将map
生成的迭代器直接转换并连接成一个字符串。
- 使用
-
print(output)
:- 打印结果时不会换行,因为所有元素已经连接成一个字符串。
这种方法在处理较大数据集时也更高效,因为避免了将整个迭代器转换为列表的中间步骤。
总结
迭代器是 Python 中非常强大且灵活的工具,能够高效地处理序列和其他集合数据。它们遵循简单的协议,通过 __iter__()
和 __next__()
方法使得任何对象都可以实现迭代器行为,从而支持 for
循环和其他迭代上下文。使用迭代器能够使代码更具可读性和效率,尤其在处理大数据集时,迭代器的惰性求值特性可以显著提升性能。