Bootstrap

线程同步之报数

# 有关python线程同步的运用

任务描述

排队报数是现实生活中很常见的一种统计人数的手段。它需要队列中的每个人互相协作,后一名比前一名报的数字多一,最后一名报的数字即为队列的总人数。 本关利用线程模拟队列中的个人,最终实现一个队列报数的过程。

相关知识

多线程是为了充分利用硬件资源,来提高任务处理速度和效率的技术。将任务拆分成互相协作的多个线程,同时运行。那么属于同一个任务的多个线程之间,就需要有交互和同步。 例如,字处理软件使用一个线程,来接受用户键盘输入,而使用另一个后台线程,来进行拼写检查以及字数统计等功能。

Thread对象

Thread类支持使用两种方法,来创建线程:

  1. 为构造函数,传递一个可调用对象;
  2. 继承 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-195个整数,然后程序暂停,几秒钟后又继续输出5-95个整数。如果将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状态。

编程要求

本关的编程任务是,补全右侧编辑器中BeginEnd区间的代码,具体要求如下:

  • 本关将根据测试输入创建多个线程,每个线程相当于队列中的一个人,他们报的数用全局变量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)

代码解释

  1. 锁的创建与使用

    • 创建一个 threading.Lock() 对象 lock
    • run 方法中使用 lock.acquire() 获取锁,确保只有一个线程可以访问和修改全局变量 x
    • 使用 try 块确保在任何情况下都会释放锁 (lock.release()),即使在获取和修改 x 的过程中发生异常。
  2. 线程类 mythread

    • 继承自 threading.Thread 类。
    • 定义一个类变量 x 用于存储全局计数器。
    • 定义一个类变量 results 用于存储每个线程的输出值。
    • run 方法中获取锁,增加 x 并将其值存入局部变量 current_value
    • 使用 try 块确保在任何情况下都会释放锁。
    • time.sleep(0.1) 用于防止输出过快。
    • current_value 添加到 mythread.results 列表中。
  3. 线程的创建和启动

    • 从输入读取线程数量 num
    • 创建一个线程列表 t1,并向其中添加指定数量的 mythread 实例。
    • 初始化全局变量 mythread.x 为 0。
    • 启动所有线程,并使用 join 方法等待所有线程执行完毕。
  4. 输出结果

    • 由于 mythread.results 列表可能因为多线程的原因顺序不正确,使用 sort() 方法对其进行排序。
    • 使用 mapstr 函数将 results 列表中的所有数字转换为字符串并连接成一个完整的字符串。
    • 打印最终结果。

代码片段解释

output = ''.join(map(str, mythread.results))

output = ''.join(map(str, mythread.results)) 这一行代码是为了将 mythread.results 列表中的所有数字连接成一个连续的字符串,具体解释如下:

  1. 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']
  2. join 方法

    • join 是一个字符串方法,用于连接 iterable 中的每个元素,生成一个新的字符串。
    • ''.join(iterable)iterable 中的每个元素连接成一个字符串,并且在每个元素之间不添加任何分隔符(因为字符串 '' 是空的)。
    • 例如,''.join(['1', '2', '3', '4', '5']) 的结果是 '12345'
  3. 综合应用

    • 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'
  1. results 列表中的每个元素转换为字符串
    • map(str, results) 的作用是创建一个新的迭代器,其中每个元素都是原 results 列表中对应元素的字符串表示。例如,map(str, [1, 2, 3, 4, 5]) 会生成一个迭代器,其内容类似于 ['1', '2', '3', '4', '5']
  2. 将这些字符串连接成一个连续的字符串
    • ''.join(mapped_results) 将上述迭代器中的所有字符串按顺序连接成一个新的字符串。由于使用的是空字符串 '' 作为分隔符,所以这些字符串之间不会有任何额外的字符插入。例如,''.join(['1', '2', '3', '4', '5']) 的结果是 '12345'

补充说明

迭代器(iterator)是一个对象,它允许我们逐个访问一个集合中的元素,而不需要暴露其底层表示。迭代器遵循特定的协议,这个协议由两个核心方法组成:__iter__()__next__()

迭代器协议
  1. __iter__() 方法

    • 返回迭代器对象本身。这个方法允许一个对象被用在 for 循环或其他迭代上下文中。
    • 例如,一个列表在调用 __iter__() 时会返回一个列表迭代器对象。
  2. __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)

在上面的示例中:

  1. MyIterator

    • 这是一个自定义的迭代器类,它接受一个数据集合并跟踪当前索引。
    • __iter__() 方法返回迭代器对象本身。
    • __next__() 方法返回集合中的下一个元素,如果索引超出集合范围,抛出 StopIteration 异常。
  2. 使用迭代器

    • 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 异常

在这个示例中:

  1. iter(my_list)

    • 调用 iter() 函数将列表转换为迭代器对象。
  2. 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'

在这个示例中:

  1. map(str, numbers)

    • 返回一个迭代器,迭代器中的每个元素是 numbers 列表中的相应元素经过 str 函数转换后的结果。
  2. next(mapped)

    • 使用 next() 函数获取迭代器的下一个元素。
  3. 注意迭代器只能遍历一次,要将 mapped 迭代器的内容一次性不换行输出,我们可以将迭代器的内容转换为一个字符串并一次性输出。
  4. 这可以通过以下步骤实现:

    • 将迭代器转换为一个列表,使用 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'
  1. 将迭代器转换为列表

    • mapped_list = list(mapped)map 返回的迭代器转换为一个列表。这样做的原因是迭代器只能遍历一次,转换为列表后可以多次访问其内容。
  2. 连接字符串

    • output = ''.join(mapped_list) 使用空字符串 '' 作为分隔符,将列表中的所有元素连接成一个单一的字符串。由于 mapped 已经将整数转换为字符串,所以直接使用 join 方法即可。
  3. 打印输出

    • print(output) 打印结果时不会换行,因为 join 方法已经将所有元素连接成一个字符串。

如果不想显式地转换为列表,也可以直接使用 ''.join(map(str, numbers)) 来一次性完成所有操作:

# 示例:使用 map 将整数列表转换为字符串列表并连接输出
numbers = [1, 2, 3, 4, 5]

# 直接将 map 结果连接成一个字符串
output = ''.join(map(str, numbers))

# 打印输出,不换行
print(output)  # 输出: '12345'

这种方法更加简洁,不需要中间变量。

  1. map(str, numbers)

    • 生成一个将 numbers 列表中每个元素转换为字符串的迭代器。
  2. ''.join(map(str, numbers))

    • 使用 join 方法将 map 生成的迭代器直接转换并连接成一个字符串。
  3. print(output)

    • 打印结果时不会换行,因为所有元素已经连接成一个字符串。

这种方法在处理较大数据集时也更高效,因为避免了将整个迭代器转换为列表的中间步骤。

总结

迭代器是 Python 中非常强大且灵活的工具,能够高效地处理序列和其他集合数据。它们遵循简单的协议,通过 __iter__()__next__() 方法使得任何对象都可以实现迭代器行为,从而支持 for 循环和其他迭代上下文。使用迭代器能够使代码更具可读性和效率,尤其在处理大数据集时,迭代器的惰性求值特性可以显著提升性能。

;