(十三)python之并发编程(线程)

本文介绍了并发和并行的概念,指出单核CPU通过任务调度实现多任务执行,而并行是在多核CPU上真正同时执行任务。同步与异步的区别在于是否等待资源返回结果。接着,文章详细讲解了Python中的线程,包括如何创建线程、线程的同步与异步执行,并通过实例展示了线程间共享资源可能导致的问题及解决方案——使用锁避免资源竞争。最后,提到了线程死锁的现象及其原因。
摘要由CSDN通过智能技术生成

一、并发和并行

1、多任务的概念

  • 简单来说,就是操作系统可以同时运行多个任务

 cpu与多任务的关系:

单核CPU可不可以执行多任务?

  • 也可以执行多任务。由于CPU执行代码都是按顺序执行的,那么,单核CPU是怎么执行多任务的呢?答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒...这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

重点: 真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

2、并发和并行

  • 并发:指的是任务数多于CPU核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
  • 并行:指的是任务数小于等于CPU核数,即任务真的是一起执行。

 

 3.同步和异步

  • 同步(协同步调):是指线程在访问某一资源时,获得了资源的返回结果之后才会执行其他操作(先做某件事,再做第二件事)
  • 异步(步调各异):与同步相对,是指线程在访问某一资源时,无论是否取得返回结果,都进行下一步操作;当有了资源返回结果时,系统自会通知线程。

来个demo给大家看一下:能看懂什么意思吗?同步就是先执行work1,work1执行完过后才会去执行work2函数,这就是同步。

import time


def work1():
    for i in range(5):
        time.sleep(1)
        print("work1-打孔--{}".format(i))


def work2():
    for i in range(6):
        time.sleep(1)
        print("work2--浇花-{}".format(i))


if __name__ == '__main__':
    s_t = time.time()
    work1()
    work2()
    e_t = time.time()
    print(e_t - s_t)


# 打印结果:
work1-打孔--0
work1-打孔--1
work1-打孔--2
work1-打孔--3
work1-打孔--4
work2--浇花-0
work2--浇花-1
work2--浇花-2
work2--浇花-3
work2--浇花-4
work2--浇花-5
11.008723258972168

二、线程

import time


def work1():
    for i in range(5):
        time.sleep(1)
        print("work1-打孔--{}".format(i))


def work2():
    for i in range(6):
        time.sleep(1)
        print("work2--浇花-{}".format(i))


if __name__ == '__main__':
    s_t = time.time()
    work1()
    work2()
    e_t = time.time()
    print(e_t - s_t)


# 打印结果:
work1-打孔--0
work1-打孔--1
work1-打孔--2
work1-打孔--3
work1-打孔--4
work2--浇花-0
work2--浇花-1
work2--浇花-2
work2--浇花-3
work2--浇花-4
work2--浇花-5
11.008723258972168

问题二:一个人完成打孔和浇花,很明显需要11秒,那我们是不是可以再喊个小王同学,帮你去做呢,你打孔,小王去浇花。如果是两个人来做呢?

那么两个事情同时做,怎么才能同时做呢?多线程....

1、threading模块介绍

python中的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用。

  • 创建线程对象:threading.Thread(target=func)
  • 参数target指定线程执行的任务(函数)

Thread类提供了以下方法:

  • run()         用以标识线程活动的方法
  • start()       启动线程活动
  • join(time)  设置主线程会等待time秒后再往下执行,time默认为子线程结束,多个子线程之间设置的值会叠加。      

 我们直接上demo:

(1)我们修改了一些代码以后,发现work1并没有被调用,这是为什么呢?

那是因为我们没有执行这个线程。我们t1.start()执行这个线程看看执行结果。

很明显,work1 和work2执行完只花了6秒钟。

 (2)那我们可不可以创建三个线程去分别执行work1、work2、work3函数?当然可以。

import time
from threading import Thread


def work1():
    for i in range(5):
        time.sleep(1)
        print("work1-打孔--{}".format(i))


def work2():
    for i in range(6):
        time.sleep(1)
        print("work2--浇花-{}".format(i))


def work3():
    for i in range(6):
        time.sleep(1)
        print("work2--写代码-{}".format(i))


if __name__ == '__main__':
    s_t = time.time()
    # 创建线程组t1去执行work1
    t1 = Thread(target=work1)
    t1.start()
    # 创建线程组t2去执行work2
    t2 = Thread(target=work2)
    t2.start()
    # 创建线程组t3去执行work3
    t3 = Thread(target=work3)
    t3.start()
    e_t = time.time()
    print(e_t - s_t)


# 打印结果
0.0004954338073730469
work2--浇花-0work1-打孔--0work2--写代码-0


work2--写代码-1work2--浇花-1
work1-打孔--1

work2--写代码-2
work1-打孔--2work2--浇花-2

work2--浇花-3work1-打孔--3work2--写代码-3


work2--浇花-4
work2--写代码-4work1-打孔--4

work2--浇花-5work2--写代码-5


Process finished with exit code 0

(3) 这个时候我们发现打印的结果很奇怪,为什么e_t-s_t时间先打印出来呢?

答:因为一个程序启动的时候都有一个主线程,我们看代码,t1,t2,t3都是分别创建三个线程,

那这个时候主线程不会去等待其他线程执行完毕后再执行,主线程会直接往下走,走到打印e_t-s_t时间,所以我们会发现打印的结果很奇怪,e_t-s_t时间先打印出来。

(4)那么来个需求,我希望主线程执行到这里的时候就不要再往下执行了,等子线程执行完毕之后在去执行下面的代码,目的是计算执行的时间怎么办?

答:Thread.join()  设置主线程等待子线程执行的时间,join里面的参数不传的话默认等待该子线程执行完毕之后才继续往下执行,如果Thread.join(1),就是等待一秒,不管该子线程有没有执行完毕,等待一秒之后就不会继续去等待。

 我们设置  t1.join(2.5)之后,发现主线程会等待2.5秒,过了2.5秒之后就直接打印了执行时间。打印完之后,子线程继续执行。

那我们需求就是等待三个子线程全部执行完毕之后,再去执行主线程,该怎么做叻?

"""

Thread(target=指定线程执行的任务函数):创建线程

Thread.start() :启动线程


注意点:默认的情况下主线程时不会等待子线程执行的

Thread.join():设置主线程等待子线程执行的时间
    join不传参数默认等待改子线程执行结束,可以通过参数设置指定的等待时间


"""

import time
from threading import Thread


def work1():
    for i in range(5):
        time.sleep(1)
        print("work1-打孔--{}".format(i))


def work2():
    for i in range(6):
        time.sleep(1)
        print("work2--浇花-{}".format(i))


def work3():
    for i in range(6):
        time.sleep(1)
        print("work3--写代码-{}".format(i))


if __name__ == '__main__':
    s_t = time.time()
    # 创建一个线程
    t1 = Thread(target=work1)
    t1.start()
    # 创建一个线程:
    t2 = Thread(target=work2)
    t2.start()
    # 创建一个线程:
    t3 = Thread(target=work3)
    t3.start()

    # 主线程等待子线程执行
    t1.join()
    t2.join()
    t3.join()
    e_t = time.time()
    print(e_t - s_t)




# 打印结果
work1-打孔--0work2--浇花-0

work3--写代码-0
work2--浇花-1
work1-打孔--1
work3--写代码-1
work2--浇花-2
work1-打孔--2
work3--写代码-2
work2--浇花-3work1-打孔--3

work3--写代码-3
work2--浇花-4
work1-打孔--4
work3--写代码-4
work2--浇花-5
work3--写代码-5
6.005569219589233

Process finished with exit code 0

(5) 那如果我们想多个线程去执行同一个函数呢?

方法1:我们可以按照上面写的一样,分别创建三个线程,然后target参数都是指向同一个函数名。

方法2:自定义线程类

那么怎么自定义线程类呢?我们直接上demo:

我们创建了一个类,继承Thread这个线程类,然后我们创建一个对象t1,去执行t1.start()的时候,发现他执行的起始是类里面的run方法。

from threading import Thread


class Mythread(Thread):
    def run(self):
        for i in range(10):
            print(i)


t1 = Mythread()
t1.start()


# 打印结果
0
1
2
3
4
5
6
7
8
9

我们通过这段代码不难看出来,是不是五个线程一起去执行的?

我们可以加个self.name打印线程数,然后在创建线程类的时候  t=MyThread(name='线程{}'.format(i+1))

"""
import time
from threading import Thread

"""
自定义线程类:
1、可以重写线程类的run方法,在run方法中写线程执行的具体任务代码。



2、Tread的其他初始化参数:
target:指定任务函数
name:指定线程名
group:指定线程分组



"""


class MyThread(Thread):
    def run(self):
        for i in range(100):
            time.sleep(0.01)
            print("{}请求第{}次".format(self.name, i))


if __name__ == '__main__':
    for i in range(5):
        t = MyThread(name='线程{}'.format(i + 1))
        t.start()

(6)线程分组和任务函数参数传递

"""
import time
from threading import Thread

"""
自定义线程类:
1、可以重写线程类的run方法,在run方法中写线程执行的具体任务代码。



2、Tread的其他初始化参数:
target:指定任务函数
name:指定线程名
group:指定线程分组



"""
from threading import Thread


def work1(name, age):
    for i in range(5):
        time.sleep(1)
        print("{}work1-打孔--{}-年龄{}".format(name, i, age))


class MyThread(Thread):
    def __init__(self, url):
        super().__init__()
        self.url = url

    def run(self):
        args = self.url
        print(args)
        # for i in range(10):
        #     time.sleep(0.1)
        #     print("{}请求第{}次".format(self.name, i))


if __name__ == '__main__':
    # work1('张三 ')

    # 给任务函数传递参数:
    #  方式一:args(args是一个元组)
    # Thread(target=work1, args=('张三',19)).start()
    # 方式二: kwargs (kwargs是一个字典)
    # Thread(target=work1, kwargs={'name': "张三", "age": 18}).start()

    # 自定义线程类 参数 传递  需要重写init方法  在init方法中保存参数即可
    # t1 = MyThread(url='www.baidu.com')
    # t1.start()

    Thread(target=work1, args=('张三', 19)).start()

 线程分组参数group用不了了,python做了升级,那这个参数咱们就不用管了。

(7) 多线程遇到的问题(多线程共享全局资源和资源竞争的问题)

我们看下面demo,可以发现,主线程的a变量和两个子线程的变量a都是全局变量,因为全局的资源是共享的,那为什么a打出来会不对呢?这就是资源共享的问题。

python的多线程都是并发执行的。 多线程是无法利用多核CPU的资源,在同一时间 只能有一个线程正真执行(原因是python解释器有一把锁GIL(全局解释器锁))。

python线程执行的切换机制:     1、线程执行遇到IO耗时操作(input、output输入输出、强制等待time.sleep、网络请求等待响应)     2、线程执行时间达到一个阈值

from threading import Thread

"""
多线程是共享全局变量的

python的多线程都是并发执行的。
多线程是无法利用多核CPU的资源,在同一时间 只能有一个线程正真执行(原因是python解释器有一把锁GIL(全局解释器锁))。


python线程执行的切换机制:
    1、线程执行遇到IO耗时操作
    2、线程执行时间达到一个阈值


"""

a = 0


def work1():
    global a
    for i in range(1000000):
        a += 1
    print("work1------a:", a)


def work2():
    global a
    for i in range(1000000):
        a += 1
    print("work2------a:", a)


if __name__ == '__main__':
    t1 = Thread(target=work1)
    t1.start()
    t2 = Thread(target=work2)
    t2.start()
    t1.join()
    t2.join()

    print("主线程---全局a:", a)



# 打印结果
work1------a: 1581986
work2------a: 2000000
主线程---全局a: 2000000

 

(8)通过锁解决资源竞争的问题

我们看到上面demo,找到具体是哪行代码会出现问题,就是在引用全局变量的时候会出现资源竞争的问题,所以在线程切换的时候就会导致数据可能不准确。

解决方法:对全局数据有修改的代码,你全部上锁。

from threading import Thread, Lock
  • 怎么创建锁:mutex = Lock()
  • 怎么用?     在a += 1的代码上面添加代码:
    mutex.acquire()  ,当你获取了这把锁之后,你才能执行下面的 a += 1这行代码,执行完之后你把这把锁释放 mutex.release()

我们看下修改后的代码,当线程执行work1然后获取到这把锁之后,其他线程执行work2是获取不到这把锁的,只有等这个线程把work1释放这把锁之后,work2才能获取到这把锁。


a = 0
# 创建一把锁
mutex = Lock()


def work1():
    global a
    for i in range(100000):
        # 上锁
        mutex.acquire()
        a += 1
        # 释放锁
        mutex.release()
    print("work1------a:", a)


def work2():
    global a
    for i in range(100000):
        # 上锁
        mutex.acquire()
        a += 1
        # 释放锁
        mutex.release()
    print("work2------a:", a)


if __name__ == '__main__':
    t1 = Thread(target=work1)
    t1.start()
    t2 = Thread(target=work2)
    t2.start()
    t1.join()
    t2.join()

    print("主线程---全局a:", a)

注意:单把锁不会出现死锁情况。

(9)死锁

单看每一个线程,逻辑没有任何问题,但是如果执行起来就会出问题,为什么呢?

原因:我们看线程1获取了锁A,线程2获取了锁B,每个人各拿了一把锁,线程1要继续往下执行的话,要获取锁B,但是锁B已经被线程2获取到了,所以线程1获取不到这把锁,那么线程1就无法继续往下执行了。线程2跟线程1相反, 现象就是你在等我释放,我在等你释放锁,这就会造成死锁的情况。

这就跟一个面试场景一下,面试官说我需要你告诉我你什么技术,面试者说你给我发offer我就告诉你我什么技术,这就会造成死锁的状态,你在等我,我在等你。。。。

import threading
import time


class MyThread1(threading.Thread):
    def run(self):
        # 对mutexA上锁
        mutexA.acquire()
        print(self.name + '----上锁----')
        time.sleep(1)

        mutexB.acquire()
        print(self.name + '----上锁----')
        mutexB.release()

        mutexA.release()


class MyThread2(threading.Thread):
    def run(self):
        # 对mutexB上锁
        mutexB.acquire()
        print(self.name + '----上锁----')
        time.sleep(1)

        mutexA.acquire()
        print(self.name + '----上锁----')
        mutexA.release()

        mutexB.release()


mutexA = threading.Lock()
mutexB = threading.Lock()

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

三、下节预告

进程、队列、协程、进程和协程和队列的区别

  • 11
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值