30

简单明了的 Python 多线程来了 | 原力计划

 4 years ago
source link: https://blog.csdn.net/weixin_44895651/article/details/105877358
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

我和我的女朋友因为python而相识,同时也是因为python我才能把憨憨追到手。最近我和我女朋友在做一个项目,我负责 语音识别和TTS ,她负责 QT界面 设计。终于在上一个周我们都完成了各自预期的功能。到了两个代码整合的阶段,却发现了一个难题:怎么样才能实现 语音和界面同时工作 ,同时怎么样才能保证通过 语音来打开相关的界面 ,以及在 视频通话时语音不工作 ,这些问题让我俩抓狂。看看我女朋友的头发最近掉的厉害,作为一个男人我必须扛起责任!于是我拦下这活,并且给我女朋友说道:等我学会了python多线程我讲给你听!

RJviQr.png!web

文章目录

线程和进程

计算机的核心是CPU,它承担了所有的计算任务,就像是一座工厂在时刻运行

maERbyr.png!web

如果工厂的资源有限,一次只能供一个车间来使用,也就是说当一个车间开工时其它车间不能工作,也就是一个CPU一次只能执行一个任务。

  • 进程就好比工厂的车间,它代表CPU所能处理的单个任务。 任一时刻,CPU总是运行一个进程 ,其他进程处于非运行状态。

当然一个车间还有很多工人,他们互相协同完成一个工作

BvuEJv2.jpg!web
  • 而线程就好比工厂的工人, 一个进程可以包含多个线程

线程(Thread)也叫轻量级进程,是 操作系统能够进行运算调度的最小单位 ,它被包涵在进程之中,是进程中的实际运作单位。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程, 同一进程中的多个线程之间可以并发执行

多线程与多进程

通俗易懂的理解就是:

多进程:允许多个任务同时进行
多线程:允许单个任务分成不同的部分运行

python多线程的实现

Python3 通过两个标准库 thread (python2中是thread模块)和 threading 提供对线程的支持。

thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。

threading

import threading #导入threading库
import time

def run(n):
    print("task", n)
    time.sleep(1) #延时一秒
    print('2s')
    time.sleep(1)
    print('1s')
    time.sleep(1)
    print('0s')
    time.sleep(1)

if __name__ == '__main__':
    t1 = threading.Thread(target=run, args=("t1",))#创建线程1,取名为t1
    t2 = threading.Thread(target=run, args=("t2",))#创建线程2,取名为t2
    t1.start() #开启线程t1
    t2.start() #开启线程t2

输出结果:

task t1
task t2
2s
2s
1s
1s
0s
0s

可以看出先开启了线程t1,在开启t2然后每隔一秒打印数据

自定义线程

通过继承threading.Thread来自定义线程类,其 本质 是重构Thread类中的run方法

import threading
import time

class MyThread(threading.Thread):
    def __init__(self, n):
        super(MyThread, self).__init__()  # 重构run函数必须要写
        self.n = n

    def run(self):
        print("task", self.n)
        time.sleep(1)
        print('2s')
        time.sleep(1)
        print('1s')
        time.sleep(1)
        print('0s')
        time.sleep(1)

if __name__ == "__main__":
    t1 = MyThread("t1")
    t2 = MyThread("t2")
    t1.start()
    t2.start()

输出结果:

task t1
task t2
2s
2s
1s
1s
0s
0s

守护线程

下面这个例子,使用 setDaemon(True) 把所有的 子线程都变成了主线程的守护线程 ,因此当主进程结束后,子线程也会随之结束。所以当 主线程结束后,整个程序就退出了

import threading
import time

def run(n):
    print("task", n)
    time.sleep(1)       #此时子线程停1s
    print('3')
    time.sleep(1)
    print('2')
    time.sleep(1)
    print('1')

if __name__ == '__main__':
    t = threading.Thread(target=run, args=("t1",))
    t.setDaemon(True)   #把子进程设置为守护线程,必须在start()之前设置
    t.start()
    print("end")

输出结果:

task t1
end

可以看到,t1线程并没有执行完毕,而是直接结束了。说明设置子线程为守护线程之后, 主线程结束了,子线程也立即结束 不再执行。

程序中不是只创建了一个线程么?怎么会有主线程和子线程呢?

vaAjIvQ.jpg!web

其实呢 程序运行时就会创建一个线程,而这个线程就是主线程

主线程等待子线程运行结束

import threading
import time

def run(n):
    print("task", n)
    time.sleep(1)      
    print('3')
    time.sleep(1)
    print('2')
    time.sleep(1)
    print('1')

if __name__ == '__main__':
    t = threading.Thread(target=run, args=("t1",))
    t.setDaemon(True)   #把子进程设置为守护线程,必须在start()之前设置
    t.start()
    t.join() # 设置主线程等待子线程结束
    print("end")

输出结果:

task t1
3
2
1
end

运行.join()后的程序表明 等待所有线程结束以后再进行.join()之后的操作 结合以上代码就是,等待t1结束以后再执行end

多线程共享全局变量

线程是进程的执行单元,进程是系统分配资源的最小单位,所以在同一个进程中的多线程是共享资源的。那么共享资源时就需要用到全局变量。

import threading
import time

num = 100

def work1():
    global num
    for i in range(3):
        num += 1
    print("in work1 num is : %d" % num)

def work2():
    global num
    print("in work2 num is : %d" % num)

if __name__ == '__main__':
    t1 = threading.Thread(target=work1)
    t1.start()
    time.sleep(1)
    t2 = threading.Thread(target=work2)
    t2.start()

运行结果如下:

in work1 num is : 103
in work2 num is : 103

可以看到两者输出的结果是相同的,说明是可以共享全局变量的。

由于 线程之间是进行随机调度 ,并且每个线程可能只执行n条,当多个线程同时修改同一条数据时可能会出现脏数据,因而,出现了线程锁,即同一时刻只允许一个线程执行操作。 线程锁用于锁定资源 ,可以定义多个锁, 在下面的实例中, 当你需要独占某一资源时,任何一个锁都可以锁这个资源,就好比你用不同的锁都可以把相同的一个门锁住是一个道理。

由于线程之间是进行随机调度,如果有多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,我们也称此为“线程不安全”。

为了方式上面情况的发生,就出现了互斥锁(Lock)

import threading

def work1():
	global A,lock#定义A和lock为全局变量
	lock.acquire()#上锁
	for i in range(5):
		A+=1
		print('work1',A)
	lock.release()#解锁
def work2():
	global A,lock
	lock.acquire()
	for i in range(5):
		A+=10
		print('work2',A)
	lock.release()
if __name__=='__main__':
	lock=threading.Lock()#定义锁
	A=0
	t1=threading.Thread(target=work1)
	t2=threading.Thread(target=work2)
	t1.start()
	t2.start()
	t1.join()
	t2.join()

输出结果:

work1 1
work1 2
work1 3
work1 4
work1 5
work2 15
work2 25
work2 35
work2 45
work2 55

可以发现对两组数据是没有影响的,感兴趣的可以尝试一下不加锁会有什么情况。

RLcok类的用法和Lock类一模一样,但它支持嵌套,在多个锁没有释放的时候一般会使用RLcok类。

import threading
import time

def Func(lock):
    global gl_num
    lock.acquire()
    gl_num += 1
    time.sleep(1)
    print(gl_num)
    lock.release()

if __name__ == '__main__':
    gl_num = 0
    lock = threading.RLock()
    for i in range(10):
        t = threading.Thread(target=Func, args=(lock,))
        t.start()

输出结果:

信号量(BoundedSemaphore类)

互斥锁同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。

实际中博主还没有用到过,所以理解不是特别透彻。

import threading
import time

def run(n, semaphore):
    semaphore.acquire()   #加锁
    time.sleep(1)
    print("run the thread:%s\n" % n)
    semaphore.release()     #释放

if __name__ == '__main__':
    num = 0
    semaphore = threading.BoundedSemaphore(5)  # 最多允许5个线程同时运行
    for i in range(22):
        t = threading.Thread(target=run, args=("t-%s" % i, semaphore))
        t.start()
    while threading.active_count() != 1:
        pass  # print threading.active_count()
    else:
        print('-----all threads done-----')

输出结果有点长,就不贴输出结果了。

事件(Event类)

python线程的事件用于主线程控制其他线程的执行,事件是一个简单的线程同步对象,其主要提供以下几个方法:

  • clear 将flag设置为“False”
  • set 将flag设置为“True”
  • is_set 判断是否设置了flag
  • wait 会一直监听flag,如果没有检测到flag就一直处于阻塞状态

事件处理的机制:全局定义了一个“Flag”,当flag值为“False”,那么event.wait()就会阻塞,当flag值为“True”,那么event.wait()便不再阻塞

import threading
import time
event = threading.Event()
def lighter():
    count = 0
    event.set()     #初始值为绿灯
    while True:
        if 5 < count <=10 :
            event.clear()  # 红灯,清除标志位
            print("1mred light is on...")
        elif count > 10:
            event.set()  # 绿灯,设置标志位
            count = 0
        else:
            print("mgreen light is on...")
        time.sleep(1)
        count += 1
def car(name):
    while True:
        if event.is_set():      #判断是否设置了标志位
            print("[%s] running..."%name)
            time.sleep(1)
        else:
            print("[%s] sees red light,waiting..."%name)
            event.wait()
            print("[%s] green light is on,start going..."%name)
light = threading.Thread(target=lighter,)
light.start()

car = threading.Thread(target=car,args=("MINI",))
car.start()

这段代码模拟红绿灯,很形象。

Qthread

本以为我学完了多线程就完事了,就可以将语音和QT界面进行整合了。当我去实现的时候发现问题不是这么简单,通过语音控制打开一个特定的界面可以实现,但是为什么只要这个特定的界面关闭了,我语音的线程也就结束了。

jyyUj2Q.png!web

困惑了我好久,最后终于在某社区发现了答案!原来QT自带的有Qthread,当 多线程涉及到界面交互时最好用Qthread 来实现。然后又查阅大量博客,看了大量代码

在使用继承QThread的run方法之前需要了解一条规则:

QThread 只有 run 函数是在新线程里的,其他所有函数都在 QThread 生成的线程里

QThread 只有 run 函数是在新线程里的

QThread 只有 run 函数是在新线程里的

QThread 只有 run 函数是在新线程里的

方法 描述 start() 启动线程 wait() 阻塞线程,直到满足如下条件之一:1. 与此QThread对象关联的线程完成执行,此函数将返回True;如果线程尚未启动,此函数也返回True。 2. 等待时间的单位是毫秒。如果时间是ULONG_MAX(默认值),则等待,永远不会超时;如果等待超时,则返回False started() 开始执行run()之前,与相关线程发射此信号 finished() 当程序完成任务时,发射此信号 sleep() 强制线程休眠(单位:秒)

那么我就在网上找到了这个计时器的例子:

#coding=utf-8
import sys

from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

count = 0


# 工作线程
class WorkThread(QThread):
    # pyqtSignal是信号类
    timeout = pyqtSignal()  # 每隔一秒发送一个信号
    end = pyqtSignal()  # 计数完成后发送一个信号

    def run(self):
        while True:
            # 休眠1秒
            self.sleep(1)
            if count == 5:
                self.end.emit()  # 发送end信号,调用和end信号关联的方法
                break
            self.timeout.emit()  # 发送timeout信号


class Counter(QWidget):
    def __init__(self):
        super(Counter, self).__init__()

        self.setWindowTitle("用QThread编写计数器")
        self.resize(600, 400)

        layout = QVBoxLayout()

        # QLCDNumber 用于模拟LED显示效果,类似于Label
        self.lcdNumber = QLCDNumber()
        layout.addWidget(self.lcdNumber)

        button = QPushButton("开始计数")
        layout.addWidget(button)

        self.workThread = WorkThread()
        self.workThread.timeout.connect(self.countTime)
        self.workThread.end.connect(self.end)
        button.clicked.connect(self.work)

        self.setLayout(layout)

    def countTime(self):
        global count
        count += 1
        self.lcdNumber.display(count)

    def end(self):
        QMessageBox.information(self, '消息', '计数结束', QMessageBox.Ok)
        global count
        count =0

    def work(self):
        self.workThread.start()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    main = Counter()
    main.show()
    sys.exit(app.exec_())

点击开始计时就会出现类似LCD的显示,计时到5秒结束后弹窗提醒。

运行结果如下:

iuEV7rJ.png!webJRzmAnI.png!web

通过这个例程让我对Qthread有了更好的理解,经管理解的不是特别透彻但是我知道怎么来改出来我想用的代码。之前提到的打开窗口线程阻塞,关闭窗口线程重启,其实这个计时器是一个很好的例子,但是关于线程阻塞.wait不好使。我的方法是定义一个全局变量mode=0(用来判断是否需要阻塞线程),如果 窗口打开后那么给这个全局赋值mode=1 ,在 run函数 里对这个mode进行判断,如果mode等于1那么可以用一个循环来延时实现。

if mode:
	while(mode):
		self.sleep(1)

窗口关闭以后给mode 赋值等于0 通过这种方法可以实现,很多小伙伴又会问怎么判断窗口打开和关闭,其实在自己写的窗口函数最前面加mode=1和最后面mode=0就可以了不用进行判断。

在看完这篇文章后,我女朋友终于给我发来了下面的表情,对我投来羡慕的眼神

be2uArn.gif

FRJZn2u.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK