五、python学习之多任务-线程

版权声明:浅弋、璃鱼的原创 https://blog.csdn.net/qq_35709559/article/details/82257056

之前学习的python编程只能实现程序的单一运行,如果碰到同时执行多个功能,运行多个任务,这样就使用多任务编程了。

一、多任务的介绍:

1.多任务的概念:

多任务是指在同一时间内执行多个任务。

2.生活中的多任务:
    <1>操作系统可以同时多个任务
    <2>单核cpu如何运行多个软件(并行与并发):

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

并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的

二、多线程:

1.线程的概念:
    线程就是在程序运行过程中,执行程序代码的一个分支,每个运行的程序至少都有一个线程。多线程是完成多任务方法之一。

python的多线程是操作提供的接口,因为Python自带的数据结构不能保证线程安全,所以需要解释器级别的锁,即:全局解释器锁(GIL锁)。同一时刻一个解释器进程只能有一个线程正在运行,多个线程就需要多个解释器,所以python的多线程并不是真正意义上的多线程。

2.单线程任务:

实力代码:

#!/usr/bin/python3
# coding=utf-8


# 定义一个唱歌方法,表示唱歌任务
def sing():
    for _ in range(15):
        print("唱歌...")

# 定义一个跳舞方法,表示跳舞任务
def dance():
    for _ in range(15):
        print("跳舞...")


if __name__ == '__main__':
    sing()
    dance()

运行结果:

唱歌...
唱歌...
...
唱歌...
跳舞...
跳舞...
...
跳舞...

这是一般形式的python代码,通过执行结果便可以看出,唱歌任务和跳舞任务是先后执行的。

3.多线程执行:

<1>导入线程模块:

import threading

<2>创建线程对象:

sub_thread = threading.Thread(group=None, target=None, name=None,args=(), kwargs=None, *, daemon=None)

线程对象Thread类参数说明:

group:线程组名,当前不需要设置,目前只能为None

target:线程实行的函数名(方法名)

nane:线程名

args:以元组类型,想要执行的函数(方法)中传参

kwargs:以字典类型,想要执行的函数(方法)中传参

daemon:是否设置守护主线程(True / False )

注意:

*后的参数只能使用关键字传参,不能使用位置参数传参

<3>启动子线程:

# 调用Thread.start(),启动子线程
sub_thread.start()

# 查看当前执行的线程名:
thread_obj = threading.current_thread()    # Return the current Thread object

<4>完成多线程执行多任务的代码:

#!/usr/bin/python3
# coding=utf-8

import time
import threading


# 唱歌任务
def sing():
    # 查看当前代码执行的任务
    current_thread = threading.current_thread()
    print("singthread:", current_thread)
    for i in range(5):
        print("唱歌中...")
        time.sleep(0.1)


# 跳舞任务
def dance():
    # 查看当前代码执行的任务
    current_thread = threading.current_thread()
    print("dancethread:", current_thread)
    for i in range(5):
        print("跳舞中...")
        time.sleep(0.1)


if __name__ == '__main__':

    # 查看当前代码执行的任务
    current_thread = threading.current_thread()
    print("mainthread:", current_thread)

    # 使用线程完成多任务,主线程负责创建两个子线程,子线程负责执行对应的任务,多线程完成多任务
    # group: 线程组,不需要设置,目前只能使用None
    # target: 线程执行的任务名(函数名/方法名)
    sing_thread = threading.Thread(target=sing ,name="mythread-1")
    dance_thread = threading.Thread(target=dance ,name="mythread-2")

    print("打印线程名:", sing_thread, dance_thread)
    # 启动线程, 必须启动线程才能执行任务
    sing_thread.start()
    dance_thread.start()

运行结果:

mainthread: <_MainThread(MainThread, started 17208)>
打印线程名: <Thread(mythread-1, initial)> <Thread(mythread-2, initial)>
singthread: <Thread(mythread-1, started 10352)>
唱歌中...
dancethread: <Thread(mythread-2, started 4172)>
跳舞中...
跳舞中...
唱歌中...
跳舞中...
唱歌中...
唱歌中...
跳舞中...
跳舞中...
唱歌中...

<5> 多线程执行带参数的任务:

#!/usr/bin/python3
# coding=utf-8

import threading
import time


# 定义唱歌任务
def sing(count):
    for i in range(count):
        print("在唱第%d句歌..." % i)
        time.sleep(0.2)

#定义跳舞任务
def dance(count):
    for i in range(count):
        print("在完成第%d个动作..." % i)
        time.sleep(0.2)

# 定义打印个人信息的任务
def show_info(name, age):
    print("姓名:%s, 年龄:%d" %(name, age))


if __name__ == '__main__':
    # 创建三个子线程
    sing_thread = threading.Thread(target=sing,args=(5,))
    dance_thread = threading.Thread(target=dance,args=(5,))
    show_thread = threading.Thread(target=show_info, args=("小明",), kwargs={"age":18})

    # 启用线程
    show_thread.start()
    show_thread.join()  # 线程等待
    sing_thread.start()
    dance_thread.start()

执行结果:

姓名:小明, 年龄:18
在唱第0句歌...
在完成第0个动作...
在唱第1句歌...
在完成第1个动作...
在唱第2句歌...
在完成第2个动作...
在唱第3句歌...
在完成第3个动作...
在完成第4个动作...
在唱第4句歌...

注意:必须启动线程,才能执行任务

<4>获取线程列表:

thread_list = threading.enumerate()  # 获取了线程对象列表

三、线程的注意点:

1.线程的执行是无序的, 是由cpu调度决定的.

代码:

#!/usr/bin/python3
# coding=utf-8

import threading
import time


def task():
    # 任务执行完成大概需要1秒钟
    time.sleep(1)

    # 查看当前执行代码的线程
    current_thread = threading.current_thread()
    print(current_thread)


if __name__ == '__main__':
    # 模拟大量线程执行任务,查看线程的执行顺序
    for i in range(10):
        sub_thread = threading.Thread(target=task)
        sub_thread.start()

运行结果:

<Thread(Thread-1, started 7616)>
<Thread(Thread-4, started 11552)>
<Thread(Thread-2, started 5044)>
<Thread(Thread-5, started 4816)>
<Thread(Thread-3, started 14456)>
<Thread(Thread-7, started 14568)>
<Thread(Thread-6, started 17104)>
<Thread(Thread-8, started 13328)>
<Thread(Thread-9, started 632)>
<Thread(Thread-10, started 15908)>

2. 主线程会等待所有子线程结束之后才退出:

代码:

#!/usr/bin/python3
# coding=utf-8

import threading
import time


def task():
    time.sleep(2)
    print(threading.current_thread())
    print("函数执行完成...")


if __name__ == '__main__':

    # 创建线程
    sub_tread = threading.Thread(target=task)

    sub_tread.start()
    print(threading.current_thread())
    # 主线程等待0.2秒
    time.sleep(0.2)
    print("程序执行完了...")
    exit()  #强制程序退出

运行结果:

<_MainThread(MainThread, started 16352)>
程序执行完了...
<Thread(Thread-1, started 5900)>
函数执行完成...

3.守护进程:

守护进程概念:当主线程结束后,守护进程随即被销毁,不再执行后面的代码.

代码:

#!/usr/bin/python3
# coding=utf-8

import threading
import time


def task():
    time.sleep(2)
    print(threading.current_thread())
    print("函数执行完成...")


if __name__ == '__main__':

    # 创建线程
    sub_tread = threading.Thread(target=task)
    # 2.设置守护线程,主线程退出,子线程直接销毁,子线程不在执行后边的代码
    sub_tread.setDaemon(True)

    # 或者在创建线程时直接设置守护线程
    # sub_tread = threading.Thread(target=task, daemon=True)

    sub_tread.start()
    print(threading.current_thread())
    # 主线程等待0.2秒
    time.sleep(0.2)
    print("程序执行完了...")
    exit()  #强制程序退出

运行结果:

<_MainThread(MainThread, started 6028)>
程序执行完了...

由运行结果可以看出,主程序执行到exit()后,程序直接结束,并没有之前的,主进程等待紫禁城完成后再退出的情况.

四、自定义线程:

1.自定义线程代码:

<1>继承threading.Thread类

<2>封装线程执行相关复杂的任务,好处是代码进行封装,完成代码的解耦.

<3>自定义线程,以后任务的执行,统一在run方法中执行(重写的Thread中的run的方法)

<4>运行子线程不能直接调用run()方法,应该使用start()方法.

自定义线程代码:

#!/usr/bin/env python
# coding=utf-8

import threading
import time

# 定义线程类
class MyThread(threading.Thread):

    # 初始化对象
    def __init__(self, var1, var2):
        # 调用父类方法
        # 根据指定类在类继承链中找到下一个类的对应方法, Thread.__init__()
        # super().__init__() ==> super(MyThread, self).__init__()
        super(MyThread, self).__init__()
        self.var1 = var1
        self.var2 = var2

    # 定义一个不需要参数的任务方法
    # 如果没有使用当前对象,可以吧方法定义成静态,此处忽略
    def task1(self):
        print("打印当前线程:",threading.current_thread())
        print("哈哈哈,这是第一个复杂的无参的任务...")
        time.sleep(2)

    # 定义一个需要参数的任务方法
    def task2(self):
        print("打印当前线程:", threading.current_thread())
        print("哈哈哈,这是第一个复杂的有参的任务...",self.var1)
        time.sleep(2)

    def task3(self):
        print("打印当前线程:", threading.current_thread())
        print("哈哈哈,这是第二个复杂的有参的任务...",self.var2)
        time.sleep(2)

    # 重写父类的run()方法
    def run(self):
        # 执行线程任务
        self.task1()
        self.task2()
        self.task3()


if __name__ == '__main__':
    # 创建线程对象
    custom_thread = MyThread("参数1", "参数2")
    #打印一下线程对象的继承关系
    print("MyThread类的继承关系:",MyThread.mro())
    # 启动线程
    custom_thread.start()

运行结果:

MyThread类的继承关系: [<class '__main__.MyThread'>, <class 'threading.Thread'>, <class 'object'>]
打印当前线程: <MyThread(Thread-1, started 16768)>
哈哈哈,这是第一个复杂的无参的任务...
打印当前线程: <MyThread(Thread-1, started 16768)>
哈哈哈,这是第一个复杂的有参的任务... 参数1
打印当前线程: <MyThread(Thread-1, started 16768)>
哈哈哈,这是第二个复杂的有参的任务... 参数2

五、共享全局变量:

1.多线程共享全局变量:
代码:

#!/usr/bin/env python
# coding=utf-8

import threading
import time


# 定义全局变量
g_list = list()


# global:加global表示全局变量要修改内存地址
# 定义添加数据的任务
def add_data():
    for i in range(5):
        g_list.append(i)
        print("添加的数据:", i)
        time.sleep(0.2)
    print("打印全局变量列表:", g_list)

def read_data():
    print("readData:", g_list)


if __name__ == '__main__':
    # 创建线程
    add_thread = threading.Thread(target=add_data)
    read_thread = threading.Thread(target=read_data)

    # 启动线程
    add_thread.start()
    read_thread.start()

运行结果:

添加的数据: 0
readData: [0]
添加的数据: 1
添加的数据: 2
添加的数据: 3
添加的数据: 4
打印全局变量列表: [0, 1, 2, 3, 4]

从运行结果中可以看出,read这个任务在add没有完成的时候就执行了,这不是我们想要的结果,我们的目的时add任务添加完所有数据之后,再由read任务读出.

2.多线程共享全局变量:存在的问题:

代码演示:

import threading

# 定义全局变量
g_num = 0

# 循环1000000次,每次给全局变量加1
def task1():
    # 声明要修改全局变量
    global g_num
    for i in range(1000000):
        g_num += 1
    print("task1:", g_num)


# 循环1000000次,每次给全局变量加1
def task2():
    # 声明要修改全局变量
    global g_num
    for i in range(1000000):
        g_num += 1
    print("task2:", g_num)

if __name__ == '__main__':
    first_thread = threading.Thread(target=task1)
    second_thread = threading.Thread(target=task2)

    # 启动线程执行对应的任务
    first_thread.start()
    second_thread.start()

执行结果:

task1: 1207978
task2: 1329645

由执行结果可以看出:当数据量达到一定数量级,任务有一定的复杂度之后,出现了资源竞争,数据错乱的情况.

解决办法:线程等待、使用互斥锁

线程同步: 保证同一时刻只能有一个线程去操作全局变量

同步: 就是协同步调,按预定的先后次序进行运行。

2.线程等待: sub_thread.join(): 

代码:

if __name__ == '__main__':
    first_thread = threading.Thread(target=task1)
    second_thread = threading.Thread(target=task2)

    # 启动线程执行对应的任务
    first_thread.start()
    # 主线程等待第一个线程执行完成以后程序再继续执行
    first_thread.join()
    second_thread.start()

运行结果:

task1: 1000000
task2: 2000000


提示: 让一个线程先执行完成,再让另外一个线程执行。

使用join多线程变成单任务,是一个线程执行完成另外一个线程再去执行,执行效率下降了,数据安全了。

七、互斥锁:(抢占式锁)

1.线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

2.互斥锁为资源引入一个状态:锁定/非锁定

3.某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

4.threading模块中定义了Lock变量,这个变量本质上是一个函数,可以方便的处理锁定

具体那个线程抢到这个锁我们决定不了,是由cpu调度决定的

5.作用:保障全局共享数据在一个时刻只有一个时刻在使用,保证数据安全
创建锁:
lock = threading.Lock(): 创建全局的互斥锁对象,Lock其实就是变量指向一个函数

# 创建全局的互斥锁 , Lock 其实就是变量指向一个函数
lock = threading.Lock()

锁定:

# 上锁
lock.acquire()

释放锁:

# 释放锁
lock.release()

6.使用互斥锁完成对共享数据分别进行100w的次的加法
互斥锁是全局的
代码:

#!/usr/bin/env python
# coding=utf-8

import threading
import time

# 创建全局的互斥锁,Lock其实是变量指向一个函数
lock = threading.Lock()

# 定义全局变量
g_num = 0

#  循环100w次,每次给全局变量+1
def task1():
    # 上锁
    lock.acquire()
    global g_num
    for i in range(1000000):
        g_num += 1

    time.sleep(1)
    print("task1:", g_num)

    # 释放锁
    lock.release()

#  循环100w次,每次给全局变量+1
def task2():
    # 上锁
    lock.acquire()
    global g_num
    for i in range(1000000):
        g_num += 1

    time.sleep(1)
    print("task2:", g_num)

    # 释放锁
    lock.release()


if __name__ == '__main__':
    first_thread = threading.Thread(target=task1)
    second_thread = threading.Thread(target=task2)

    # 启动线程
    first_thread.start()
    second_thread.start()

运行结果:

task1: 1000000
task2: 2000000

7.使用互斥锁的目的

能够保证多个线程访问共享数据不会出现资源竞争及数据错误。

8.上锁、解锁过程

当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。

每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。

线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。

9.总结

锁的好处:

确保了某段关键代码只能由一个线程从头到尾完整地执行

锁的坏处:

多线程执行变成了包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了

锁使用不好就容易出现死锁情况

八、死锁:

1.死锁的概念:
    死锁:一直等待对方释放所得情景。

2.死锁的实例:
生产者消费者问题:

代码:

import threading
import time

# 创建互斥锁
lock = threading.Lock()


# 根据下标去取值, 保证同一时刻只能有一个线程去取值
def get_value(index):

    # 上锁
    lock.acquire()
    print(threading.current_thread())
    my_list = [3,6,8,1]
    # 判断下标释放越界
    if index >= len(my_list):
        print("下标越界:", index)
        return
    value = my_list[index]
    print(value)
    time.sleep(0.2)
    # 释放锁
    lock.release()


if __name__ == '__main__':
    # 模拟大量线程去执行取值操作
    for i in range(30):
        sub_thread = threading.Thread(target=get_value, args=(i,))
        sub_thread.start()

运行结果:

<Thread(Thread-1, started 1212)>
3
<Thread(Thread-2, started 15032)>
6
<Thread(Thread-3, started 9212)>
8
<Thread(Thread-4, started 10732)>
1
<Thread(Thread-5, started 10816)>
下标越界: 4
# 执行到此,程序既不向下执行,也不退出

看运行结果,出现了死锁的情况,程序既不向下执行,也不退出。也就是说被进程占用的资源没有被释放。

解除死锁,既需要再合适的位置释放锁,放开权限。

# 根据下标去取值, 保证同一时刻只能有一个线程去取值
def get_value(index):

    # 上锁
    lock.acquire()
    print(threading.current_thread())
    my_list = [3,6,8,1]
    # 判断下标释放越界
    if index >= len(my_list):
        print("下标越界:", index)
        # 判断下标越界,释放锁,跳出函数
        lock.release()
        return
    value = my_list[index]
    print(value)
    time.sleep(0.2)
    # 释放锁
    lock.release()

猜你喜欢

转载自blog.csdn.net/qq_35709559/article/details/82257056
今日推荐