其他分享
首页 > 其他分享> > The Truth About Threads(关于线程的真相 )

The Truth About Threads(关于线程的真相 )

作者:互联网

如果你从未听过线程,这是一个基本的描述。

  1. 线程是操作系统(OS)提供的特性
  2. 提供给软件开发人员,以便他们可以向操作系统表明程序的哪些部分可以并行运行
  3. 操作系统决定如何与每个部分共享CPU资源,就像操作系统决定与同时运行的所有其他不同程序(进程)共享CPU资源一样。

既然你正在读一本异步通讯的书,这一定是我要告诉你的部分,"线程是可怕的,你永远不应该使用它们,对吗?" 不幸的是,情况并非如此简单。我们需要权衡使用线程的好处和风险,就像任何技术选择一样。

这本书不应该是关于线程的.但是这里有两个问题:异步是作为线程的替代提供的,因此如果不进行一些比较,就很难理解其价值主张;即使在使用异步时,您仍然可能需要处理线程和进程,因此您需要了解一些关于线程的知识。

Benefits of Threading(线程的好处)

这些是线程的主要好处:

现在,对于Python,关于并行性的观点是有问题的,因为Python解释器使用全局锁,称为全局解释器锁(GIL),以保护解释器本身的内部状态。也就是说,它提供了保护,以避免多个线程之间竞争条件的潜在灾难性影响。锁的一个副作用是,它最终会将程序中的所有线程固定在一个CPU上。正如您可以想象的那样,这否定了任何并行性性能好处(除非您使用Cython或Numba之类的工具来绕过限制)。

然而,关于可感知的简单性,第一点很重要:在Python中使用线程非常简单,如果您以前没有遇到过难以置信的竞争条件错误,线程提供了一个非常吸引人的并发模型。即使您在过去经历过痛苦,线程仍然是一个引人注目的选择,因为您可能已经学会了(艰难的方法)如何保持代码简单和安全。

我在这里没有篇幅来讨论更安全的线程编程.但是一般来说,使用线程的最佳实践是从并发中使用ThreadPoolExecutor类。concurrent.futures.通过submit()传递所有需要的数据方法。

from concurrent.futures import ThreadPoolExecutor as Executor
import time
def worker(data):
    # process the data
    pass

data = 'your-input-data'
with Executor(max_workers=10) as exe:
    future = exe.submit(worker, data)

ThreadPoolExecutor提供了一个非常简单的接口,用于在线程中运行函数。如果需要,可以使用ProcessPoolExecutor将线程池转换为子进程池。它们有相同的api,这意味着对代码的改动是最小的。executor API 同样使用在异步中。

通常情况下,您会希望您的任务是短期的,这样当您的程序需要关闭时,您可以简单地调用Executor.shutdown(wait=True)并等待一两秒钟,以允许执行程序完成。

最重要的是:如果可能的话,您应该尽量防止您的线程代码(在前面的示例中,worker()函数)访问或写入任何全局变量!

线程的不足

线程的缺点已经在其他一些地方提到过,但为了完整起见,我们还是在这里收集它们:

import os
from time import sleep
from threading import Thread
threads = [
Thread(target=lambda: sleep(60)) for i in range(1000)
]
[t.start() for t in threads]
print(f'PID = {os.getpid()}')
[t.join() for t in threads]

图片.png

预先分配的虚拟内存是惊人的~ 80gb(因为每个线程有8mb的堆栈空间!),但是驻留内存只有~ 130mb。在32位Linux系统上,由于3 GB的用户空间地址空间限制,我无法创建这么多空间,无论实际物理内存的消耗是多少。为了在32位系统上解决这个问题,有时需要减少预配置的堆栈大小,现在在Python中仍然可以这样做,使用thread .stack_size([size]).显然,减少堆栈大小会影响运行时的安全性,这与函数调用可以嵌套的程度有关,包括递归.单线程协程没有这些问题,是并发I/O的一个更好的选择。

这都不是新的知识,线程作为编程模型的问题也不是特定于平台的。例如,这是关于线程的Microsoft Visual c++文档:

Windows API中的中央并发机制是线程。通常使用CreateThread函数创建线程。虽然线程的创建和使用相对容易,但是操作系统会分配大量的时间和其他资源来管理它们。此外,尽管每个线程都保证与具有相同优先级的任何其他线程接收相同的执行时间,但是相关的开销要求您创建足够大的任务。对于更小或更细粒度的任务,与并发相关的开销可能会超过并行运行任务的好处。

但是——我听说你们抗议了——这是Windows,对吧?Unix系统肯定没有这些问题吧?下面是来自Mac开发者库的线程编程指南的类似建议:

在内存使用和性能方面,线程对程序(和系统)有实际的成本。每个线程都需要在内核内存空间和程序的内存空间中分配内存。管理线程和协调其调度所需的核心结构使用有线内存存储在内核中。线程的堆栈空间和每个线程的数据存储在程序的内存空间中。大多数这些结构都是在您第一次创建thread-a进程时创建并初始化的,由于需要与内核进行交互,这个进程的开销相对较大。

它们在并发编程指南中有更进一步说明:

在过去,将并发引入应用程序需要创建一个或多个额外的线程。不幸的是,编写线程代码具有挑战性。线程是一种必须手动管理的低级工具。考虑到应用程序的最佳线程数量可以根据当前系统负载和底层硬件动态更改,实现正确的线程解决方案变得非常困难,但不是不可能实现的。此外,通常与线程一起使用的同步机制增加了软件设计的复杂性和风险,却不能保证提高性能。

这些主题贯穿始终:

接下来,让我们看一个关于线程的案例研究,它强调了第一点,也是最重要的一点。

案例研究:机器人和餐具

图片.png

其次,也是更重要的一点,我们过去(现在也仍然不相信)标准的多线程模型,它是共享内存抢占式并发:我们仍然认为没有人能够在“a = a + 1”是不确定的语言中编写正确的程序。

我讲了一个餐厅的故事,里面的类人机器人——ThreadBots——做了所有的工作。在这个比喻里,每个工人健康就是一个线程。在下面的案例中,我们将了解为什么线程被认为是不安全的。

# ThreadBot for table service
import threading
from queue import Queue
from attr import attrs, attrib # making class creation easy.

# attrs 初始化装饰器
@attrs
class Cutlery:
    knives = attrib(default=0)
    forks = attrib(default=0)

    def give(self, to:'Cutlery', knives=0, forks=0):
        self.change(-knives, -forks)
        to.change(knives, forks)

    def change(self, knives, forks):
        self.knives += knives
        self.forks += forks

# Thread 子类
class ThreadBot(threading.Thread):
    tasks = Queue()
    def __int__(self):
        super().__init__(target=self.manage_table)
        # bot 等待table,同时响应cutlery
        self.cutlery = Cutlery(knives=0, forks=0)
        # 机器人还将被分配任务。它们将被添加到这个任务队列
        # 然后机器人将在其主处理循环期间执行它们
        # self.tasks = Queue()

    # 这个机器人的主要程序是这个无限循环。
    # 如果你需要关闭一个bot,你必须给他们关闭任务。
    def manage_table(self):
        while True:
            task = self.tasks.get()
            if task == 'prepare table':
                kitchen.give(to=self.cutlery, knives=4, forks=4)
            elif task == 'clear table':
                self.cutlery.give(to=kitchen, knives=4, forks=4)
            elif task == 'shutdown':
                return



kitchen = Cutlery(knives=100, forks=100)
bots = [ThreadBot() for i in range(10)]  # 创建10个

import sys


for bot in bots:
    for i in range(int(sys.argv[1])):
        # print(bot,type(bot))
        bot.tasks.put('prepare table')
        bot.tasks.put('clear table')
    bot.tasks.put('shutdown')

print('Kitchen inventory before service: ', kitchen)
for bot in bots:
    bot.start()

for bot in bots:
    bot.join()
print("kitchen inventory after service: ", kitchen)

标签:About,self,bot,并发,knives,Truth,线程,forks
来源: https://www.cnblogs.com/zyl007/p/12995697.html