Python中的线程

Python中的线程

1.线程创建及参数传参
2.多线程的操作
3.互斥锁和死锁

多任务场景介绍

生活中的多任务场景介绍

1. 提问

利用现学知识能够让两个函数或者方法同时执行吗?

不能,因为之前所写的程序都是单任务的,也就是说一个函数或者方法执行完成另外一个函数或者方法才能执行,要想实现这种操作就需要使用多任务

多任务的最大好处是充分利用CPU资源,提高程序的执行效率

2. 多任务的概念

多任务是指在同一时间内执行多个任务,例如: 现在电脑安装的操作系统都是多任务操作系统,可以同时运行着多个软件。

3. 多任务的执行方式

  • 并发
  • 并行

并发:

在一段时间内交替去执行任务。

例如:

对于单核cpu处理多任务,操作系统轮流让各个软件交替执行,假如:软件1执行0.01秒,切换到软件2,软件2执行0.01秒,再切换到软件3,执行0.01秒……这样反复执行下去。表面上看,每个软件都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像这些软件都在同时执行一样,这里需要注意单核cpu是并发的执行多任务的。

并行:

对于多核cpu处理多任务,操作系统会给cpu的每个内核安排一个执行的软件,多个内核是真正的一起执行软件。这里需要注意多核cpu是并行的执行多任务,始终有多个软件一起执行

4. 小结

  • 使用多任务就能充分利用CPU资源,提高程序的执行效率,让你的程序具备处理多个任务的能力。
  • 多任务执行方式有两种方式:并发并行,这里并行才是多个任务真正意义一起执行。
Python中多任务场景介绍

在现实生活中,有很多的场景中的事情是同时进行的,比如跳舞和唱歌是同时进行的。

在程序中,可以使用代码来模拟唱歌和跳舞的功能:

from time import sleep

def sing():
    for i in range(3):
        print("正在唱歌...%d"%i)
        sleep(1)

def dance():
    for i in range(3):
        print("正在跳舞...%d"%i)
        sleep(1)

if __name__ == '__main__':
    sing() #唱歌
    dance() #跳舞

很显然刚刚的程序并没有完成唱歌和跳舞同时进行的要求

如果想要实现“唱歌跳舞”同时进行,那么就需要一个新的方法,叫做:多任务

线程介绍及创建线程

高级线程模块

Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

创建线程及传参

导入线程模块

#导入线程模块
import threading

线程类Thread参数说明

Thread([group [, target [, name [, args [, kwargs]]]]])

  • group: 线程组,目前只能使用None
  • target: 执行的目标任务名
  • args: 以元组的方式给执行任务传参
  • kwargs: 以字典方式给执行任务传参
  • name: 线程名,一般不用设置
启动线程

启动线程使用start方法

多线程操作

创建多线程
import threading
import time

# 唱歌任务
def sing():
    # 扩展: 获取当前线程
    # print("sing当前执行的线程为:", threading.current_thread())
    for i in range(3):
        print("正在唱歌...%d" % i)
        time.sleep(1)

# 跳舞任务
def dance():
    # 扩展: 获取当前线程
    # print("dance当前执行的线程为:", threading.current_thread())
    for i in range(3):
        print("正在跳舞...%d" % i)
        time.sleep(1)


if __name__ == '__main__':
    # 扩展: 获取当前线程
    # print("当前执行的线程为:", threading.current_thread())
    # 创建唱歌的线程
    # target: 线程执行的函数名
    sing_thread = threading.Thread(target=sing)

    # 创建跳舞的线程
    dance_thread = threading.Thread(target=dance)

    # 开启线程
    sing_thread.start()
    dance_thread.start()

执行结果:

正在唱歌...0
正在跳舞...0
正在唱歌...1
正在跳舞...1
正在唱歌...2
正在跳舞...2
Python中启动多线程

启动多线程使用start方法

Python多线程设置线程等待

假如我们现在创建一个子线程,这个子线程执行完大概需要2.5秒钟,现在让主线程执行1秒钟就退出程序,查看一下执行结果,示例代码如下:

import threading
import time


# 测试主线程是否会等待子线程执行完成以后程序再退出
def show_info():
    for i in range(5):
        print("test:", i)
        time.sleep(0.5)


if __name__ == '__main__':
    sub_thread = threading.Thread(target=show_info)
    sub_thread.start()

    # 主线程延时1秒
    time.sleep(1)
    print("over")

执行结果:

test: 0
test: 1
over
test: 2
test: 3
test: 4

说明:

通过上面代码的执行结果,我们可以得知: 主线程会等待所有的子线程执行结束再结束

Python中的线程无序性
import threading
import time


def task():
    time.sleep(1)
    print("当前线程:", threading.current_thread().name)


if __name__ == '__main__':

   for _ in range(5):
       sub_thread = threading.Thread(target=task)
       sub_thread.start()

执行结果:

当前线程: Thread-1
当前线程: Thread-2
当前线程: Thread-4
当前线程: Thread-5
当前线程: Thread-3

说明:

  • 线程之间执行是无序的,它是由cpu调度决定的 ,cpu调度哪个线程,哪个线程就先执行,没有调度的线程不能执行。
  • 进程之间执行也是无序的,它是由操作系统调度决定的,操作系统调度哪个进程,哪个进程就先执行,没有调度的进程不能执行。
Python中的守护主线程

守护主线程:

  • 守护主线程就是主线程退出子线程销毁不再执行

设置守护主线程有两种方式:

  1. threading.Thread(target=show_info, daemon=True)
  2. 线程对象.setDaemon(True)

设置守护主线程的示例代码:

import threading
import time


# 测试主线程是否会等待子线程执行完成以后程序再退出
def show_info():
    for i in range(5):
        print("test:", i)
        time.sleep(0.5)


if __name__ == '__main__':
    # 创建子线程守护主线程 
    # daemon=True 守护主线程
    # 守护主线程方式1
    sub_thread = threading.Thread(target=show_info, daemon=True)
    # 设置成为守护主线程,主线程退出后子线程直接销毁不再执行子线程的代码
    # 守护主线程方式2
    # sub_thread.setDaemon(True)
    sub_thread.start()

    # 主线程延时1秒
    time.sleep(1)
    print("over")

执行结果:

test: 0
test: 1
over
Python中多线程共享全局变量

需求:

  1. 定义一个列表类型的全局变量

  2. 创建两个子线程分别执行向全局变量添加数据的任务和向全局变量读取数据的任务

  3. 查看线程之间是否共享全局变量数据

    import threading
    import time
    
    
    # 定义全局变量
    my_list = list()
    
    # 写入数据任务
    def write_data():
        for i in range(5):
            my_list.append(i)
            time.sleep(0.1)
        print("write_data:", my_list)
    
    
    # 读取数据任务
    def read_data():
        print("read_data:", my_list)
    
    
    if __name__ == '__main__':
        # 创建写入数据的线程
        write_thread = threading.Thread(target=write_data)
        # 创建读取数据的线程
        read_thread = threading.Thread(target=read_data)
    
        write_thread.start()
        # 延时
        # time.sleep(1)
        # 主线程等待写入线程执行完成以后代码在继续往下执行
        write_thread.join()
        print("开始读取数据啦")
        read_thread.start()
        
    执行结果:
    
    write_data: [0, 1, 2, 3, 4]
    开始读取数据啦
    read_data: [0, 1, 2, 3, 4]
    
Python中多线程共享全局变量的注意点

假设两个线程t1和t2都要对全局变量g_num(默认是0)进行加1运算,t1和t2都各对g_num加10次,g_num的最终的结果应该为20。

但是由于是多线程同时操作,有可能出现下面情况:

  1. 在g_num=0时,t1取得g_num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得g_num=0
  2. 然后t2对得到的值进行加1并赋给g_num,使得g_num=1
  3. 然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给g_num。
  4. 这样导致虽然t1和t2都对g_num加1,但结果仍然是g_num=1

测试1

import threading
import time

g_num = 0

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


def work2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work2, g_num is %d---"%g_num)


print("---线程创建之前g_num is %d---"%g_num)

t1 = threading.Thread(target=work1, args=(100,))
t1.start()

t2 = threading.Thread(target=work2, args=(100,))
t2.start()

while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)

运行结果:

---线程创建之前g_num is 0---
----in work1, g_num is 100---
----in work2, g_num is 200---
2个线程对同一个全局变量操作之后的最终结果是:200

测试2

import threading
import time

g_num = 0

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


def work2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work2, g_num is %d---"%g_num)


print("---线程创建之前g_num is %d---"%g_num)

t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()

t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()

while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)

运行结果:

---线程创建之前g_num is 0---
----in work1, g_num is 1088005---
----in work2, g_num is 1286202---
2个线程对同一个全局变量操作之后的最终结果是:1286202

结论

  • 如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确
互斥锁

1.互斥锁的概念

互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。

注意:

  • 互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。

为了更好的理解互斥锁,请看下面的图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gq1N5nUX-1648170850619)(./image/7-3.png)]

2. 互斥锁的使用

threading模块中定义了Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。

互斥锁使用步骤:

# 创建锁
mutex = threading.Lock()

# 上锁
mutex.acquire()
...这里编写代码能保证同一时刻只能有一个线程去操作, 对共享数据进行锁定...

# 释放锁
mutex.release()

注意点:

  • acquire和release方法之间的代码同一时刻只能有一个线程去操作
  • 如果在调用acquire方法的时候 其他线程已经使用了这个互斥锁,那么此时acquire方法会堵塞,直到这个互斥锁释放后才能再次上锁。

3. 使用互斥锁完成2个线程对同一个全局变量各加100万次的操作

import threading


# 定义全局变量
g_num = 0

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


# 循环一次给全局变量加1
def sum_num1():
    # 上锁
    lock.acquire()
    for i in range(1000000):
        global g_num
        g_num += 1

    print("sum1:", g_num)
    # 释放锁
    lock.release()


# 循环一次给全局变量加1
def sum_num2():
    # 上锁
    lock.acquire()
    for i in range(1000000):
        global g_num
        g_num += 1
    print("sum2:", g_num)
    # 释放锁
    lock.release()


if __name__ == '__main__':
    # 创建两个线程
    first_thread = threading.Thread(target=sum_num1)
    second_thread = threading.Thread(target=sum_num2)
    # 启动线程
    first_thread.start()
    second_thread.start()

    # 提示:加上互斥锁,那个线程抢到这个锁我们决定不了,那线程抢到锁那个线程先执行,没有抢到的线程需要等待
    # 加上互斥锁多任务瞬间变成单任务,性能会下降,也就是说同一时刻只能有一个线程去执行

执行结果:

sum1: 1000000
sum2: 2000000

说明:

通过执行结果可以得知互斥锁能够保证多个线程访问共享数据不会出现数据错误问题

5. 小结

  • 互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题
  • 使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行
  • 使用互斥锁会影响代码的执行效率,多任务改成了单任务执行
  • 互斥锁如果没有使用好容易出现死锁的情况
死锁 ★★★★★

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()

3. 避免死锁

  • 在合适的地方释放锁
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)
        # 当下标越界需要释放锁,让后面的线程还可以取值
        lock.release()
        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()

4. 小结

  • 使用互斥锁的时候需要注意死锁的问题,要在合适的地方注意释放锁。
  • 死锁一旦产生就会造成应用程序的停止响应,应用程序无法再继续往下执行了。

五、总结

1.掌握线程创建及参数传递
2.掌握多线程间的相关操作和特性
3.掌握互斥锁和死锁应用原理

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()

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


**4. 小结**

- 使用互斥锁的时候需要注意死锁的问题,要在合适的地方注意释放锁。
- 死锁一旦产生就会造成应用程序的停止响应,应用程序无法再继续往下执行了。

------

## 五、知识总结

1.掌握线程创建及参数传递
2.掌握多线程间的相关操作和特性
3.掌握互斥锁和死锁应用原理

<think>首先,用户的问题是:“我想了解Python线程执行顺序的相关信息 请问Python线程执行顺序”。这直接涉及到Python线程的执行顺序。 从系统级指令来看: - 我必须使用中文回答。 - 行内数学表达式用 `$...$`,独立公式用 `$$...$$`,但这个问题是关于线程的,可能不需要数学公式,除非有相关计算。 - 保持LaTeX语法正确。 - 生成相关问题。 - 在引用的段落末尾添加引用标识,如 [^1]。 - 回答结构清晰,逐步解决问题。 回顾引用内容: - 引用[1]:展示了守护线程的创建和使用,强调守护线程在主程序结束时自动终止。 - 引用[2]:演示了线程执行的无序性,通过多个线程启动并打印信息,显示执行顺序是随机的。 - 引用[3]:讨论了改变线程执行顺序的尝试,使用守护线程属性,并指出daemon属性必须在start()前设置。 用户的问题焦点在“线程执行顺序”,所以核心是解释Python线程的执行顺序如何工作。 关键点: - Python线程(在CPython解释器中)由于全局解释器锁(GIL),同一时刻只有一个线程在执行Python字节码。 - 线程的执行顺序是由操作系统调度器决定的,不是由Python程序控制的。这意味着线程启动后,它们的执行顺序是不可预测的。 - 引用[2]直接说明了线程执行的无序性。 - 守护线程(daemon threads)在主程序结束后会自动终止,但这不直接影响执行顺序。 我的回答需要: 1. **逐步解释线程执行顺序**:从基础开始,解释为什么顺序无序。 2. **引用相关部分**:使用提供的引用证据。例如,引用[2]显示无序性,引用[3]试图改变顺序但daemon属性不是用于顺序控制。 3. **生成相关问题**:在回答末尾添加相关问题。 4. **结构清晰**: - 引言:简述问题。 - 主体:分步骤解释。 - Python线程调度机制。 - 为什么顺序无序(GIL和操作系统调度)。 - 如何尝试控制顺序(通过同步机制如锁、条件变量等)。 - 守护线程的角色。 - 结论:总结要点。 - 相关问题。 生成相关问题: - 基于主题,如:如何确保线程同步?守护线程的使用场景?线程安全的最佳实践? 确保使用中文: - 整个回答用中文撰写。 引用标识: - 当引用提供的文本时,在末尾加 [^1], [^2], [^3]。 最终回答结构: - 标题或开头简述。 - 解释线程执行顺序。 - 代码示例(可选,但引用中有)。 - 总结。 - 相关问题。 草拟回答大纲: 1. **引言**:Python线程的执行顺序是由操作系统调度器决定的,因此通常是不可预测的。这意味着线程启动后,它们的运行顺序无法保证。[^2] 2. **原因**:由于全局解释器锁(GIL)和操作系统的线程调度机制,线程在不同时间被CPU执行,导致无序性。 3. **演示**:如引用[2]所示,通过创建多个线程并观察它们的输出,可以看到执行顺序是随机的。 4. **控制顺序**:无法直接控制线程的执行顺序,但可以使用同步原语如锁(Lock)、事件(Event)或屏障(Barrier)来协调线程的执行。[^3] 提到设置daemon属性,但这主要用于线程生命周期控制,而不是顺序控制。 5. **守护线程**:守护线程(daemon threads)在主线程结束时自动终止,但这不影响它们的执行顺序;它们仍然可能在任何时间点被执行。[^1] 6. **最佳实践**:使用threading模块提供的工具来管理线程同步。 7. **相关问题**:列出几个相关问题。 相关问题例子: 1. 如何确保Python线程安全执行? 2. 守护线程和非守护线程的区别是什么? 3. Python线程编程中有哪些常见的同步机制? </think>### Python线程执行顺序的详解 在Python中,线程的执行顺序是由操作系统的线程调度器决定的,而不是由Python程序本身控制。这意味着线程启动后,它们的执行是无序和不可预测的,这主要源于Python的全局解释器锁(GIL)和操作系统的底层调度机制。下面我将逐步解释线程执行顺序的相关概念、原因和实际影响。 #### 1. **线程执行顺序为什么无序?** - **核心原因**:Python的GIL(Global Interpreter Lock)确保同一时刻只有一个线程执行Python字节码,但线程的调度依赖于操作系统。操作系统根据CPU资源、优先级等因素动态分配执行时间,导致线程的执行顺序随机变化。 - **演示示例**:引用[2]中提供了一个代码实例,通过创建多个线程并观察输出,可以看到线程的启动和执行顺序不一致(例如,某些线程可能先于主线程打印信息)。这是因为每个线程在 `start()` 后被调度器随机唤醒: ```python import threading, time def print_info(): time.sleep(0.5) # 模拟延时 print(threading.current_thread().name) # 输出线程名称 if __name__ == '__main__': for i in range(5): t = threading.Thread(target=print_info) t.start() print("主线程开始!") ``` 运行此代码时,输出类似: ``` 主线程开始! Thread-1 Thread-3 Thread-2 ... ``` 这表明线程执行顺序是随机的,无法保证先启动的线程先执行[^2]。 - **数学表示**:线程执行时间可建模为随机过程。设线程启动时间为 $t_0$,实际执行时间为 $t_{\text{exec}}$,则 $t_{\text{exec}}$ 服从操作系统的调度分布,通常近似泊松分布:$P(t_{\text{exec}}) \sim \text{Poisson}(\lambda)$,其中 $\lambda$ 取决于系统负载。 #### 2. **如何尝试影响线程执行顺序?** - **无法直接控制顺序**:Python没有内置机制强制指定线程顺序。设置线程属性(如 `daemon`)仅影响线程的生命周期,而不是执行顺序。引用[3]中尝试使用守护线程(`daemon=True`),但这只确保主线程结束时守护线程自动终止,并不改变调度顺序: ```python import threading def action(len): for i in range(len): print(f"{threading.current_thread().name}, {i}") def main(): t1 = threading.Thread(target=action, args=(5,), daemon=True) # 守护线程 t1.start() for i in range(3): print(f"{threading.current_thread().name}, {i}") if __name__ == "__main__": main() ``` 输出可能为: ``` MainThread, 0 Thread-1, 0 MainThread, 1 Thread-1, 1 ... ``` 守护线程 `t1` 的输出混杂在主线程中,顺序不可控[^3]。 - **间接控制方法**:虽然不能改变调度顺序,但可以通过同步机制协调线程: - **锁(Lock)**:使用 `threading.Lock()` 确保关键代码块一次只被一个线程执行,避免竞态条件。 - **屏障(Barrier)**:使用 `threading.Barrier` 强制多个线程在特定点等待同步。 - **事件(Event)**:使用 `threading.Event()` 通知线程何时开始或停止。 例如,用锁实现有序输出: ```python import threading lock = threading.Lock() def ordered_task(id): with lock: # 获取锁 print(f"Thread {id} executed") threads = [threading.Thread(target=ordered_task, args=(i,)) for i in range(3)] for t in threads: t.start() for t in threads: t.join() ``` 输出顺序固定(Thread 0, Thread 1, Thread 2),但这是通过序列化执行实现的,并非改变调度顺序。 #### 3. **守护线程对执行顺序的影响** - **守护线程的作用**:如引用[1]所示,守护线程(`daemon=True`)用于后台任务(如监控或日志写入),当主线程结束时它们自动终止。但这不影响执行顺序:守护线程仍可能在任何时间点被调度执行。 ```python import threading import time def background_task(): count = 0 while True: print(f"Background task {count}") count += 1 time.sleep(1) t = threading.Thread(target=background_task, daemon=True) t.start() print("Main thread is running...") time.sleep(2) print("Main thread has completed") ``` 输出中,守护线程的输出可能出现在主线程语句之间,证明其执行顺序是穿插无序的[^1]。 - **关键区别**:非守护线程(默认)会阻止程序退出直到完成;守护线程不会,但两者都在同一调度池中无序执行。 #### 4. **总结与最佳实践** - **核心结论**:Python线程执行顺序本质上是**无序的**,由操作系统调度器随机决定。GIL限制了并行效率,但调度不可控性主要源于OS层。 - **何时需关注顺序**:在并发任务需要严格顺序时(如数据处理流水线),优先使用同步工具(锁或队列)而非依赖调度。对于后台任务,守护线程是高效选择。 - **性能影响**:无序调度可能引发竞态条件(Race Conditions),建议使用 `threading` 模块的同步原语或升级到多进程(`multiprocessing`)绕过GIL限制。 ###
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值