如何在Python中构建异步应用程序:探索多线程
#编程 #python #asynchronous

多线程的概念通常会使初学者感到困惑。这是因为学习它带来的新概念以及编写螺纹函数/应用程序的复杂性。

有些人可能会争辩说多线程不好,而另一些人同意这是好的。

但是,引入了多线程以使程序异步运行,尤其是涉及用户输入和输出的长期运行任务。

知道什么是多线程,何时使用它,其应用程序可以使您牢记速度和可扩展性编写更好的代码。

在本教程的末尾,您将构建一个异步运行的函数。从上一个教程中记住您的项目,您将提高并比较速度。

线程与线程

线程是现代计算机程序的基本构建块之一。它们就像较大程序中存在的小型,轻巧的过程,可以独立执行任务。线程共享程序的相同内存空间,这意味着他们可以访问和修改相同的数据。这使他们可以轻松交流和合作。

线程是过程的子集 - 它是执行的最小单位。

另一方面,线程是指在单个过程中使用线程的概念。
简单地说,线程允许程序执行多任务。它将不同的任务分配给同一组(过程)的不同工人(线程),从而使他们能够在这些任务上共同努力。

多线程与并发

多线程为名称所示,意味着在一个过程中运行多个线程。线程可以视为工人,每个线程都负责执行特定的任务。任务分为子任务,每个可用线程都占用这些子任务。线程共享相同的内存空间,这使线程之间的通信有效。

An image showing multithreading

它像劳动分工一样工作。每个线程一起工作以实现函数调用中的目标。因此,一旦一个线程忙碌,用户执行指令,另一个线程就在等待执行指令。也可能是另一个指令在队列中等待,因此一旦一个线程忙碌,另一个线程会占用指令并执行它。现在,让我们将重点转移到全球口译员锁(GIL)上。它在多线程中起着至关重要的作用。

全球口译员锁(GIL)

为了防止种族条件和deadlocks,Python使用了GIL。它的作用是防止多个线程同时执行。

gil阻止您在多线程中实现parallelism。它仅允许一个线程一次执行,这意味着程序是上下文切换。引入了这一点是为了确保线程安全。

多线程使用多个线程,但线程并未同时执行。

但是,如果不能,这是什么意义。这不是引入多线程的原因吗?您之前阅读的只是每个线程执行一组指令,所以现在有什么意义。

这是并发来播放的地方。这是可以完全理解上下文切换和多任务处理的地方。现在您了解我们要去哪里。让我们讨论并发。

什么是并发?

当我们的程序同时执行多个任务时,

并发发生。

Concurrency

在并发中,任务被分解为子任务。

例如,让我们想象一个I/O绑定任务中的方案。当程序正在等待用户单击按钮或提交表格时。而不是线程保持空闲,而是继续执行其他任务。因此,正在发生的事情是该程序在线程之间切换,从而给予同时运行的幻觉。

想象您的脑海中的线程,图片多个线程。现在您已经完成了,应该能够理解它,如果不理解,请重新阅读,绘制图表,也可以检查下面的图。

当我们在浏览器上打开多个选项卡时,或使用图形用户界面为几件事时,可以观察到并发性。实际上,这是上下文切换。

Concurrency

在并发中,几个线程可以依靠一个过程。

一些教程可以互换使用CPU,CORE和过程。不要困惑。 CPU容纳核心,一个过程就像一个用于线程的容器,并且核心是物理处理单元。

I/O绑定的任务

我们讨论了I/O的并发任务。

i/o绑定的任务是什么?
I/O结合的任务花费大部分时间等待完成输入/输出操作完成。

并发(例如,多线程)通常用于通过允许CPU在I/O等待期间切换到其他任务来提高效率。 I/O任务的示例是从文件中读取数据,提出网络请求或与数据库进行交互。

多线程的用例

  • 它用于GUIS应用程序。
  • 它用于游戏开发。
  • 它用于Web服务器和实时系统。

线程的实现

先决条件

我认为您是初学者,并且已经阅读了上一篇将您介绍给asynchronous applications的文章。

  • Python的基本知识,包括知道如何使用功能和模块。
  • 安装了python。
  • 代码编辑器。
  • 愿意学习和探索。

基本上,在Python中,有两种方法可以实现螺纹:

  • 使用线程模块
  • 使用consturrent.futures.threadpoolexecutorâ模块

使用线程模块

请记住,在上一篇文章中,我们编写了一个依次打印数字1-10的函数。在本文中,我们将通过使用线程进行改进。

以下是上一篇文章的结果。您应该拥有相当相似的东西。

import time
import threading

def print_numbers_sequentially(numbers):
    start = time.perf_counter()

    for number in range(1, numbers):
        if number in [8, 12]:
        # if number == 8 or number == 12:
            print(f'Sleeping {number} second(s)...')
            time.sleep(number)
            print(f'Done sleeping...{number}')
        else:
            print(number)

        print(number)

    end = time.perf_counter()
    elapsed_time = end - start  

    return elapsed_time

elapsed_time = print_numbers_sequentially(31)
print(f'Elapsed time: {elapsed_time} seconds')

这是我的结果:
过去的时间:20.0994832秒

现在,让我们改进它。

import time
import threading

start = time.perf_counter()

步骤1。
导入必要的模块:代码从导入必要的模块开始。 threading模块提供了用于使用线程的工具。

步骤2。
time.perf_counter()函数用于测量当前时间并将其存储在start变量中。

def print_numbers_async(seconds):
    if seconds in [8, 12]:
        print(f'Sleeping {seconds} second(s)...')
        time.sleep(seconds)
        print(f'Done sleeping...{seconds}')
    else:
        print(seconds)

步骤3。
print_numbers_async函数:它接收一个参数seconds。如果seconds的值为8或12,则该功能会打印一条消息,睡在指定的秒数中,然后打印出另一个消息,表明它已经睡觉了。否则,它只会打印seconds的值。

threads = []

for num in range(1, 31):  
    t = threading.Thread(target=print_numbers_async, args=[num])  
# args is used if the fn has an argument
    t.start()
    threads.append(t)

步骤4。
线程创建和启动:使用循环来创建和启动多个线程。循环从数字1到30。对于每个数字,使用threading.Thread()创建了一个新的线程ttarget参数指定线程将执行的函数,在这种情况下为print_numbers_asyncargs参数提供了传递该函数的参数,在这种情况下,num的当前值。

步骤5。
线程启动:创建线程后,在其上调用start()方法。这将在单独的线程中启动print_numbers_async函数的执行。然后将线程对象添加到threads列表中。

for thread in threads:
    thread.join()

第6步。
线程加入:另一个循环在前面创建的线程列表上迭代。对于列表中的每个线程,调用join()方法。该指令告诉主程序要等到线程在执行之前完成执行。这样可以确保所有线程在继续之前完成了工作。

end = time.perf_counter()
elapsed_time = end - start
print(f'Finished in {elapsed_time} second(s)')

第7步。
所有线程完成工作后,再次调用time.perf_counter()函数以测量当前时间并将其存储在end变量中。

第8步。
打印经过的时间:endstart之间的差异代表整个程序执行的经过的时间。

现在我们完成了。容易吧?我想这是很多工作理解整个概念的工作。但是,您不必担心您会重读它并继续练习,您很快就会习惯它。

最终代码现在应该如下:

import time
import threading

start = time.perf_counter()

def print_numbers_async(seconds):
    if seconds in [8, 12]:
        print(f'Sleeping {seconds} second(s)...')
        time.sleep(seconds)
        print(f'Done sleeping...{seconds}')
    else:
        print(seconds)

threads = []

for num in range(1, 31):  
    t = threading.Thread(target=print_numbers_async, args=[num])  # args is used if the fn has an argument
    t.start()
    threads.append(t)

for thread in threads:
    thread.join()  # waiting for the thread to finish

end = time.perf_counter()

elapsed_time = end - start

print(f'Finished in {elapsed_time} second(s)')

这是我的结果:
在12.0270528第二(S)

完成

使用ThreadPoolExecutor模块

步骤1。
导入模块:代码首先导入concurrent.futures模块,该模块为线程或进程池中的异步执行函数提供了高级接口。

步骤2。
threadPoolExecutorâ:使用with语句创建ThreadPoolExecutor。执行者管理一个工人线程。检查下面的资源以了解有关它的更多信息。

步骤3。
使用executor.map()执行:executor.map()函数用于安排print_numbers_async函数,以从secs范围中执行的每个值,从1到30。这意味着该函数将对范围内的每个值同时执行。

了解有关maps的更多信息。

import concurrent.futures
import time

start = time.perf_counter()

def print_numbers_async(seconds):
    if seconds == 8 or seconds == 12:
        print(f'Sleeping {seconds} second(s)...')
        time.sleep(seconds)
        print(f'Done sleeping...{seconds}')
    else:
        print(seconds)

with concurrent.futures.ThreadPoolExecutor() as executor:
    secs = range(1, 31)
    results = executor.map(print_numbers_async, secs)

end = time.perf_counter()

elapsed_time = end - start

print(f'Finished in {round(end - start, 2)} second(s)')

这是我的结果。您应该有几乎相似的东西。
在12.06秒完成(S)

就在那里。我们已经实现了线程。但是,等等,这似乎不像您想在现实生活中做的事情。查看此Github repo以了解更多信息并在现实生活中使用它。

结论

每个发明都不对每个人完全有利。每个人都有偏好,需要学习和实验以了解撰写异步应用程序时最适合您的方法。

在编程中,多线程为同时运行任务提供了强大的工具。虽然起初看起来似乎很复杂,但是掌握线程和并发的概念可以极大地增强您创建高效且响应迅速的应用程序的能力。通过了解何时以及如何使用多线程,您可以优化代码的性能并提供更好的用户体验。当您深入研究异步编程时,请记住,练习和探索是利用多线程潜力的关键。

资源