Python 在使用多线程时,要实现同时并发运行线程,一般都会用 Queue 队列来实现,但一用到 Queue 就难于控制线程与GUI 界面上的信息交互,特别是在用 tkinter 界面时,由于 tykinter 对线程安全并不友好,很容易出现卡壳情况。

简单的多线程模型

这里要实现的多线程模型仅用 threading 模块,这里先介绍网上最容易找到的实例,并进行说明其优缺点,后面再给出真正能并发多线程的实例。

下面多线程的例子随便在网上就能搜出一堆来

import threading
import time,random

class myThread(threading.Thread):#线程类
    def __init__(self,index,threadIndex):
        super().__init__()
        self.index=index
        self.threadIndex=threadIndex
    
    def run(self):
        print("线程【{}】:任务:{} ——开始".format(self.threadIndex,self.index))
        self.task()
        print("线程【{}】:任务:{} ——结束".format(self.threadIndex,self.index))
        
    def task(self):
        time.sleep( random.randint(1,50)/10) 
    
def main():
    print("【开始所有任务】")
    count=50  #任务总数
    threadCount=10  #线程数

    index=0 #任务序号
    
    while  count >index:
        threads=[]  #线程列表
        for i in range(threadCount):
            t=myThread(index,i)
            threads.append(t)
            index+=1
            if count<=index:
                break
            
        for t in threads: #创建线程后统一开启
            t.start()

        for t in threads: #全部开启后统一进行阻塞
            t.join()
            
    print("【结束所有任务】")
                        
if __name__ == '__main__':
    main()

上面为最简单的多线程模型,加入线程阻塞后能在所有线程结束处理一些事情。它的优点是简单易懂,可以对界面进行简单的信息交互;缺点是不是真正意义上的同时并发,而是按线程数进行分批次的并发。即按上例为将50个任务按每次10个分成5批,每批任务全部执行完后,才进入下一批任务,不能持续同时执行10个任务,因此不是真正意义上的并发多线模型。

并发执行的多线程模型

请耐心看完下面的说明后,才好理解后面的代码

要实现持续同时有10个任务在运行,需要实时判断是否有任务结束,结束后再立即创建新的线程来补充。要实时监控是否有任务结束,只靠线程的 join()方法无法做到及时监控,因此需要自制任务的阻塞机制,同时监控线程是否结束。

任务开始时创建的线程需要全部创建完成后,再统一开始执行,而后面结束任务后创建的线程则需要立即执行。

要替换已完成的线程,需要准确地区分是哪个线程结束,所以在创建线程列表时,还要加入线程号的概念,后面再根据结束的线程号替换到列表中的线程。完整的代码见下

import threading
import time,random


class myThread(threading.Thread):#线程类
    def __init__(self,index,threadIndex):
        super().__init__()
        self.index=index
        self.threadIndex=threadIndex
        self.isFinish=False

    
    def run(self):
        print("线程【{}】:任务:{} ——开始".format(self.threadIndex,self.index))
        self.task()
        print("线程【{}】:任务:{} ——结束".format(self.threadIndex,self.index))

        self.isFinish=True #线程结束
        
    def task(self):
        time.sleep( random.randint(1,50)/10) 
    
def main():
    print("【开始所有任务】")
    count=50  #任务总数
    threadCount=10  #线程数
    def getThreadIndex(threads,threadIndex):#根据线程号判断线程是否存在
        for i in range(len(threads)):
            if threads[i][0]==threadIndex:
                return i #返回线程列表中的序号(index)

        return -1 #线程不存在,可以创建

        #
    def addThread(threads,threadIndex,index,start=0):#创建线程
        local= getThreadIndex(threads,threadIndex)
        if local>-1:
            threads.pop(local) #删除原线程列表中已结事的线程

        if index<count:
            t=myThread(index,threadIndex)
            threads.append([threadIndex,t])

            if start: #是否立即开启线程
                t.start() #替换结束的线程时需要立即开启

        return threads
        #

    index=0 #任务序号
    threads=[]  #线程列表,格式 [ 线程号 , 线程 ]
    
    isStart=True  #用于区分开始时创建的线程
    
    while  count >index:
        threadIndex=0
        while threadIndex<=(threadCount - len(threading.enumerate())) and count >index:

            threads= addThread(threads,threadIndex,index)
            index+=1
            threadIndex+=1
            
        if isStart==True: #任务开始时创建规定数量的线程(创建所有线程后再统一开启)
            for t in threads:
                t[1].start()

        isStart=False   #不再创建新的线程

        while len(threads)>0: #自制阻塞,同时替换完成的线程
            for t in threads:
                if t[1].isFinish==True:#确定线程任务结束
                    threads= addThread(threads,t[0],index,1) #替换线程任务后立即开启线程
                    index+=1

            
    print("【结束所有任务】")
            
            
if __name__ == '__main__':
    main()

需要注意一点,在进行tkinter GUT 数据交互时,tkinter 界面时还是不能直接作用在小部件上,需要通过成员变量或合局变量传递信息,上面的 myThread 类可以增加传递数据的成员变量。