学习笔记-多线程

多线程

线程与进程存在一定区别,每一个进程必须有一个线程,线程才是程序执行的最小单元进程实际上会在已有的进程空间中执行,在同一个进程里面,线程与线程之间是相互独立,都可以访问到进程空间里面的公共变量,而进程与进程之间完全独立,没有任何的共享空间,从而导致进程与进程之间的通信非常麻烦,需要依靠队列进行完成.而线程与线程之间则不需要,同在一个进程空间中,存在全局变量进行相互通信

使用的是threading库

import threading
import time


def show1():
    # 线程函数输出
    time.sleep(2)
    print('线程1')

def show2():
    # 线程函数输出
    print('线程2')

t1 = threading.Thread(target=show1)
t1.start()
t2 = threading.Thread(target=show2)
t2.start()

线程t1和线程t2都是创建的,其中target参数是填写对应的执行函数名,后面跟进程一样可以进行传参数,args传入的必须是元组,kwargs传入的是字典,说白了就是对应的位置参数和关键字参数

多线程里面存在一定缺陷,如果使用的是公共变量进行操作的时候会出现计算混乱或者碰撞的情况,这种情况只有在数值比较大的时候会出现,因为它出现的概率很低

根本原因在于底层的cpu是使用的时间片轮转的方式进行计算的,当一个程序在执行某个操作全局变量的语句时,可能执行到一半,还没有对变量发生改写,也就是最后的赋值操作,cpu就将其弹出,让另外的一个程序进行运算,如果此时另外一个程序在cpu内完成了对全局变量的改写,此时的全局变量已经发生改变,cpu再次调用原来没有执行完的程序继续执行的时候,最后的赋值操作又会对全局变量进行改写,这就导致正常运算的程序结果被覆盖,从而引发计算冲突

import threading

def add1():
    # 进行加法计算
    global num
    for i in range(1000000):
        num += 1
    print(f'add1完成:{num}')

def decrease():
    # 进行减法运算
    global num
    for i in range(1000000):
        num -= 1
    print(f'decrease方法完成{num}')

num = 0

# 创建线程
t1 = threading.Thread(target=add1)
t2 = threading.Thread(target=decrease)
# 执行线程
t1.start()
t2.start()
print('完成')

最后的执行结果并不是想象中的0,num += 1这种操作实际上分为三步,第一步取出num,第一步计算num+1,第三步将num+1的值赋值给num,如果在第二步的时候就停止了,cpu执行下面的减法,那么实际上num的值已经出现更改,但是加法还停留在最后的阶段没有赋值,此时它对应的num不是现在已经更改之后的,下一次cpu执行赋值的时候就会导致减法的计算结果被覆盖,是的这个减法就像没有被执行过一样,从而引发计算冲突

互斥锁

锁的应用就是解决计算冲突,在程序的执行过程中cpu可能会导致程序执行步骤中断的情况,这里python设计了一种锁的概念,当某个程序一旦加上锁,在没有释放锁之前该程序将会一执行,直到释放锁,这个时候才会遵循cpu的运算时间片轮转规则.锁的意义在于锁住一个最小的执行单元,这个单元不可分割,防止执行到一半被弹出cpu的情况.这就能保证每一次的运算都能够完成

import threading

def add_function():
    global num
    lock.acquire() # 上锁
    for i in range(1000000):
        num += 1
    lock.release() # 执行完成解锁
    print(f'加法完成,结果为{num}')

def decrease():
    global num #申明全局变量
    lock.acquire() # 上锁
    for i in range(1000000):
        num -= 1
    lock.release() # 执行完成解锁
    print(f'减法完成,结果为{num}')



num = 0
lock = threading.Lock()
t1 = threading.Thread(target=add_function)
t2 = threading.Thread(target=decrease)
# 开启线程
t1.start()
t2.start()
print('程序完成')

注意这里面的锁没有上到num+=1上面,按照规范确实是应该加到上面,然而外面就是for循环,循环次数非常大,每次循环都要进行上锁解锁,重复非常多次,这会影响整个程序的运行速度,所以将锁加到for循环外面,然而这就导致必须是某个for循环结束才能执行另外一个for循环,这跟单线程几乎没有什么差别,在这个代码中确实是这样,如果两个函数非常复杂的时候,情况就不同了

死锁

如果在项目中是多个人进行编程执行某个功能的时候,通常都不是单线程做的,而是多线程做的,每个人在编程的过程中都会使用互斥锁,很多时候出现锁死的情况,这种情况下系统并不报错,只是都在等待,如果没有及时处理,就跟死机了一样,这就是死锁,现在进行死锁的代码演示

import threading
import time


def threading1():
    if lock1.acquire():
        print('锁上lock1')
        time.sleep(1)
        # 如果想解锁,那就设置在拿锁的时候进行实践设置
        if lock2.acquire(blocking=True,timeout=2): # 这里就会卡死,因为lock2在threading2中还没有释放
            print('锁上lock2')
        lock1.release()


def threading2():
    if lock2.acquire():
        print('锁上lock2')
        time.sleep(1)
        # 如果想解锁,那就设置在拿锁的时候进行实践设置
        if lock1.acquire(blocking=True,timeout=2): # 这里就会卡死,因为lock1在threading1中还没有释放
            # 如果两秒钟还没有拿到就跳过
            print('锁上lock1')
        lock2.release()


if __name__ == '__main__':
    # 创建两把锁
    lock1 = threading.Lock()
    lock2 = threading.Lock()
    # 执行两个线程
    t1 = threading.Thread(target=threading1)
    t2 = threading.Thread(target=threading2)
    t1.start()
    t2.start()

如果不进行时间参数设置,则两个线程都会卡死,锁时间的参数设置意义就在于,当线程遇到需要拿的锁,而这个锁正好被其他线程占用,那么当前线程则进入等待状态,如果另外一个线程是个死循环或者是某个bug导致没有释放锁,则这个线程就死了,永远不会执行,除非锁被释放.时间参数就是设置等待对应的时间,如果时间到了还没有解锁,则跳过该语句继续执行后面的代码,这就能够实现防止锁死的情况

线程的同步

线程之间是相互独立执行代码,有的时候存在线程同步的需求,比如某个线程前期执行一大堆代码,当对共同的某个全局变量做变更的时候需要进行相应的确认,确认这个全局变量已经满足需要执行的多线程条件的时候进行更改,此时需要等待其他线程,这才是线程的同步,当然这里可以使用线程.join的方法进行控制,但是这个方法是必须等对应线程执行完成,那如果是线程中间的某个计算步骤需要确认,后续的代码很多,执行时间很长,这里用线程.join()等待显然不现实

扫描二维码关注公众号,回复: 4486215 查看本文章

如果建立另外一个判断用的全局变量当然是可以进行线程之间的公用,但是在判断语句里面没有办法实现暂停的方式进行等待,如果在判断的瞬间没有通过,则程序要么跳过判断,要么终止,因此这种方法也不行.

这里考虑到需要暂停的特性,使用互斥锁的方式能够达到这种效果,线程之间是可以通过锁的方式进行相互通信,锁本身可以看出是相互通信的信号或者是判断条件.锁的特性在于如果处于上锁状态,程序就会进行sleep状态等待,这种状态不会浪费cpu资源,比使用死循环进行监听要方便的多

import threading
import time


def show1():
    while True:
        if lock1.acquire(): # 如果lock1锁没有被锁则先执行这里
            print("1")
            time.sleep(1)
            lock2.release() # 执行完成后释放一个锁


def show2():
    while True:
        if lock2.acquire(): # show1执行后lock2锁被释放,所以这里lock2.acquire是真
            print("2")
            time.sleep(1)
            lock3.release()


def show3():
    while True:
        if lock3.acquire():
            print("3")
            time.sleep(1)
            lock1.release()


if __name__ == '__main__':
    # 创建三个锁,先将2和3锁定
    lock1 = threading.Lock()
    lock2 = threading.Lock()
    lock3 = threading.Lock()
    # 锁定2和3
    lock2.acquire() # 如果不写则看到的是错乱的输出现象
    lock3.acquire()
    # 创建线程
    t1 = threading.Thread(target=show1)
    t2 = threading.Thread(target=show2)
    t3 = threading.Thread(target=show3)
    t1.start()
    t2.start()
    t3.start()

如果都去掉if判断,不进行锁限定,所有的while里面进行死循环

while True:
    print('1')

这种情况则会在输出的结果中看到123,321,312等等错乱的顺序结果,这就是因为线程执行相互独立,我们这里没有办法规定哪个函数先执行.

加上锁之后就能够实现以我们想要的顺序执行,当一个锁开启后必然关闭另外一个锁,保持三个锁中随时只有一个锁开启,这种方式就能够实现线程同步

当然有人会绝对这没有什么卵用,如果要进行顺序规定输出,使用单线程的面向过程编程思想就可以了,而这里使用多线程多此一举,我必须承认在这里是这样的,但是需要注意的是,这里每一个函数都很简单,如果每一个函数都是复杂的功能,在判断锁之前各自执行非常复杂的运算,那么此时使用单线程则会导致cpu和内存资源的浪费,本来三线程跑完用1个小时,单线程则需要3个小时

猜你喜欢

转载自blog.csdn.net/weixin_43959953/article/details/84898072