为什么异步任务之间的切换比threading.Thread慢得多?

时间:2020-03-09 11:26:16

标签: python multithreading python-asyncio

众所周知,asyncio旨在加快服务器速度,增强了它作为Web服务器承载更多请求的能力。但是,根据今天的测试,我震惊地发现,出于任务之间切换的目的,使用Thread的速度比使用协程的速度要快得多(尽管在保证线程锁定的情况下)。这是否意味着使用协程是没有意义的?

想知道为什么,有人可以帮我弄清楚吗?

这是我的测试代码:依次在两个任务中添加一个全局变量2000000次。

from threading import Thread , Lock
import time , asyncio

def thread_speed_test():

    def add1():
        nonlocal count
        for i in range(single_test_num):
            mutex.acquire()
            count += 1
            mutex.release()

    mutex = Lock()
    count = 0
    thread_list = list()
    for i in range(thread_num):
        thread_list.append(Thread(target = add1))

    st_time = time.time()
    for thr in thread_list:
        thr.start()

    for thr in thread_list:
        thr.join()

    ed_time = time.time()
    print("runtime" , count)
    print(f'threading finished in {round(ed_time - st_time,4)}s ,speed {round(single_test_num * thread_num / (ed_time - st_time),4)}q/s' ,end='\n\n')

def asyncio_speed_test():

    count = 0

    @asyncio.coroutine
    def switch():
        yield

    async def add1():
        nonlocal count
        for i in range(single_test_num):
            count += 1
            await switch()

    async def main():

        tasks = asyncio.gather(     *(add1() for i in range(thread_num))
                        )
        st_time = time.time()
        await tasks
        ed_time = time.time()
        print("runtime" , count)
        print(f'asyncio   finished in {round(ed_time - st_time,4)}s ,speed {round(single_test_num * thread_num / (ed_time - st_time),4)}q/s')

    asyncio.run(main())

if __name__ == "__main__":
    single_test_num = 1000000
    thread_num = 2
    thread_speed_test()
    asyncio_speed_test()

在我的电脑中得到以下结果:

2000000
threading finished in 0.9332s ,speed 2143159.1985q/s

2000000
asyncio   finished in 16.044s ,speed 124657.3379q/s

追加:

我意识到,当线程数量增加时,线程模式变慢,而异步模式变快。 这是我的测试结果:

# asyncio #
thread_num        numbers of switching in 1sec     average time of a single switch(ns)
         2                              122296                                    8176
        32                              243502                                    4106
       128                              252571                                    3959
       512                              253258                                    3948 
      4096                              239334                                    4178

# threading #
thread_num        numbers of switching in 1sec     average time of a single switch(ns)
         2                             2278386                                     438
         4                              737829                                    1350
         8                              393786                                    2539
        16                              367123                                    2720
        32                              369260                                    2708
        64                              381061                                    2624
       512                              381403                                    2622

2 个答案:

答案 0 :(得分:0)

我不确定,您可能正在将苹果与橘子进行比较。

您基本上是在惩罚异步,这迫使它切换上下文,这需要时间,而允许线程自由运行。

asyncio被认为用于必须等待一段时间输入的任务。在您的基准测试中情况并非如此。

为了公平地比较,您应该模拟一些现实的延迟。

答案 1 :(得分:0)

为了进行更公平的比较,我对您的代码做了一些更改。

我用条件替换了您简单的锁。这使我可以在每次计数器迭代后强制进行线程切换。 Condition.wait()函数调用始终阻塞进行调用的线程;仅当另一个线程调用Condition.notify()时,该线程才继续。因此,必须进行线程切换。

测试不是这种情况。仅当线程调度程序导致任务切换时,才会发生任务切换,因为代码的逻辑永远不会导致线程阻塞。与Condition.wait()不同,Lock.release()函数不会阻止调用者。

有一个小困难:最后运行的线程在最后一次调用Condition.wait()时将永远阻塞。因此,我引入了一个简单的计数器来跟踪剩余的运行线程数。另外,当一个线程完成其循环时,它必须对Condition.notify()进行最后一次调用才能释放下一个线程。

我对异步测试所做的唯一更改是用await asyncio.sleep(0)替换了“ yield”语句。这是为了与Python 3.8兼容。我还将试用次数减少了10倍。

时间是在使用Python 3.8的相当老的Win10计算机上进行的。

如您所见,线程代码要慢很多。那就是我所期望的。拥有async / await的原因之一是因为它比线程机制轻巧。

from threading import Thread , Condition
import time , asyncio

def thread_speed_test():

    def add1():
        nonlocal count
        nonlocal thread_count
        for i in range(single_test_num):
            with mutex:
                mutex.notify()
                count += 1
                if thread_count > 1:
                    mutex.wait()
        thread_count -= 1
        with mutex:
            mutex.notify()

    mutex = Condition()
    count = 0
    thread_count = thread_num
    thread_list = list()
    for i in range(thread_num):
        thread_list.append(Thread(target = add1))

    st_time = time.time()
    for thr in thread_list:
        thr.start()

    for thr in thread_list:
        thr.join()

    ed_time = time.time()
    print("runtime" , count)
    print(f'threading finished in {round(ed_time - st_time,4)}s ,speed {round(single_test_num * thread_num / (ed_time - st_time),4)}q/s' ,end='\n\n')

def asyncio_speed_test():

    count = 0

    async def switch():
        await asyncio.sleep(0)

    async def add1():
        nonlocal count
        for i in range(single_test_num):
            count += 1
            await switch()

    async def main():

        tasks = asyncio.gather(*(add1() for i in range(thread_num))                        )
        st_time = time.time()
        await tasks
        ed_time = time.time()
        print("runtime" , count)
        print(f'asyncio   finished in {round(ed_time - st_time,4)}s ,speed {round(single_test_num * thread_num / (ed_time - st_time),4)}q/s')

    asyncio.run(main())

if __name__ == "__main__":
    single_test_num = 100000
    thread_num = 2
    thread_speed_test()
    asyncio_speed_test()

runtime 200000
threading finished in 4.0335s ,speed 49584.7548q/s

runtime 200000
asyncio   finished in 1.7519s ,speed 114160.9466q/s