Chapter7_通过线程处理阻塞任务
1、守护进程daemon的概念、作用理解
2、如何理解这个 " 线程在操作系统级别具有上下文切换成本。"? [fromo page 183]
线程是在操作系统级别创建的,创建起来比协程消耗更多的资源。此外,线程在操作系统
级别具有上下文切换成本。在上下文切换发生时,保存和回复线程状态会消耗掉使用线程获得的一些性能增益。
3、为什么如下代码,开启了time.sleep(1)后便会发生死锁,而注释掉time.sleep(1)后再执行,便可以避免死锁?请详细解释原因
from threading import Lock, Thread import time lock_a = Lock() lock_b = Lock() def a(): with lock_a: print('Acquired lock a from method a!') # time.sleep(1) with lock_b: print('Acquired both locks from method a!') def b(): with lock_b: print('Acquired lock b from method b!') with lock_a: print('Acquired both locks from method b!') thread1 = Thread(target=a) thread2 = Thread(target=b) thread1.start() thread2.start() thread1.join() thread2.join()
这个问题涉及到了线程同步中的一个重要概念:资源获取顺序。
当您注释掉 time.sleep(1) 时,两个线程都在尝试获取锁的顺序是相同的,即先获取 lock_a,然后获取 lock_b。这种情况下不会发生死锁。让我们来分析一下:
- 线程
thread1首先获取了lock_a。 - 在
thread1尝试获取lock_b之前,线程thread2已经获取了lock_b。 - 现在
thread1正在等待lock_b,而thread2正在等待lock_a。 - 由于线程获取锁的顺序是一致的,因此
thread1会先释放lock_a,然后thread2可以获取lock_a,继续执行并释放lock_b。 - 最后
thread1可以获取lock_b,两个线程都可以顺利完成。
但是,当您取消注释 time.sleep(1) 时,情况就不同了。这时,线程获取锁的顺序发生了变化,导致了死锁:
- 线程
thread1首先获取了lock_a。 - 在
thread1尝试获取lock_b之前,它会休眠 1 秒钟。 - 在这 1 秒钟内,线程
thread2已经获取了lock_b,并开始等待lock_a。 - 1 秒钟后,
thread1尝试获取lock_b,但是被阻塞了,因为lock_b已经被thread2持有。 - 现在
thread1持有lock_a并等待lock_b,而thread2持有lock_b并等待lock_a。 - 两个线程相互等待对方释放锁,因此发生了死锁。
这种情况下,程序将永远挂起,无法继续执行。
总的来说,当多个线程试图以不同的顺序获取多个锁时,很容易发生死锁。为了避免死锁,所有线程必须以相同的顺序获取锁。
在这个例子中,当您注释掉 time.sleep(1) 时,两个线程以相同的顺序获取锁,因此可以避免死锁。
但是,当您取消注释 time.sleep(1) 时,线程获取锁的顺序发生了变化,导致了死锁的发生。
4、熟悉下队列 Queue的 .get() 方法
from queue import Queue # 创建一个队列对象 q = Queue() # 向队列中插入元素 q.put(1) q.put(2) q.put(3) # 依次获取队列中的元素 first = q.get() # 获取并移除第一个元素,即 1 second = q.get() # 获取并移除第二个元素,即 2 third = q.get() # 获取并移除第三个元素,即 3 print(first) # 输出: 1 print(second) # 输出: 2 print(third) # 输出: 3
5、poll这个单词啥意思?【计算机语境下】
poll 这个单词在计算机术语中通常指的是反复检查某个状态或获取某些数据。
它源自于英语中的 poll,意思是“投票”或“调查”,但在编程中有其特定的含义。
在编程中的含义
- 反复检查:不断地检查某个条件是否满足或某个状态是否发生变化。例如,反复检查一个变量的值,直到它变成期望的值。
- 获取数据:从某个数据源(如队列、文件、网络连接等)中不断地获取数据。
在 LoadTester 类中的使用
在 LoadTester 类中,_poll_queue 方法的作用就是“轮询”队列中的进度更新。具体地说,它不断地检查队列是否有新的进度更新,如果有就处理,没有则设置一个定时器稍后再检查。
更通俗的解释
可以把 poll 理解为“不断查看某个邮箱,看有没有新的邮件”。假设你每隔一段时间去查看一次邮箱,如果有新邮件就取出来处理;如果没有,就稍后再去查看。这个动作就是“轮询”。
总结
- 计算机术语中的
poll:指不断地检查某个条件或获取某些数据。 - 在代码中的作用:在
LoadTester类中,_poll_queue方法不断检查队列中的进度更新,及时更新进度条。
通过这种轮询机制,程序能够及时处理新的数据或变化的状态,从而保持对系统状态的实时响应。
6、如何理解如下代码的 _poll_queue()方法?[from example7_14.py]
from queue import Queue from tkinter import Tk from tkinter import Label from tkinter import Entry from tkinter import ttk from typing import Optional from chapter_07.listing_7_13 import StressTest class LoadTester(Tk): def __init__(self, loop, *args, **kwargs): # A Tk.__init__(self, *args, **kwargs) self._queue = Queue() self._refresh_ms = 25 self._loop = loop self._load_test: Optional[StressTest] = None self.title('URL Requester') self._url_label = Label(self, text="URL:") self._url_label.grid(column=0, row=0) self._url_field = Entry(self, width=10) self._url_field.grid(column=1, row=0) self._request_label = Label(self, text="Number of requests:") self._request_label.grid(column=0, row=1) self._request_field = Entry(self, width=10) self._request_field.grid(column=1, row=1) self._submit = ttk.Button(self, text="Submit", command=self._start) # B self._submit.grid(column=2, row=1) self._pb_label = Label(self, text="Progress:") self._pb_label.grid(column=0, row=3) self._pb = ttk.Progressbar(self, orient="horizontal", length=200, mode="determinate") self._pb.grid(column=1, row=3, columnspan=2) def _update_bar(self, pct: int): # C if pct == 100: self._load_test = None self._submit['text'] = 'Submit' else: self._pb['value'] = pct self.after(self._refresh_ms, self._poll_queue) def _queue_update(self, completed_requests: int, total_requests: int): # D self._queue.put(int(completed_requests / total_requests * 100)) def _poll_queue(self): # E if not self._queue.empty(): percent_complete = self._queue.get() self._update_bar(percent_complete) else: if self._load_test: self.after(self._refresh_ms, self._poll_queue) def _start(self): # F if self._load_test is None: self._submit['text'] = 'Cancel' test = StressTest(self._loop, self._url_field.get(), int(self._request_field.get()), self._queue_update) self.after(self._refresh_ms, self._poll_queue) test.start() self._load_test = test else: self._load_test.cancel() self._load_test = None self._submit['text'] = 'Submit'
_poll_queue 方法的详细解释
def _poll_queue(self): # E # 检查队列中的进度更新 if not self._queue.empty(): percent_complete = self._queue.get() # 获取进度百分比 self._update_bar(percent_complete) # 更新进度条 else: if self._load_test: self.after(self._refresh_ms, self._poll_queue) # 若测试未完成,继续定时检查
1. 检查队列是否为空
if not self._queue.empty():
- 首先,方法检查队列是否为空。
self._queue.empty()返回True表示队列为空,返回False表示队列中有元素。 not self._queue.empty()如果队列中有元素,则进入if代码块;否则,进入else代码块。
2. 获取队列中的进度百分比
percent_complete = self._queue.get() # 获取进度百分比
self._queue.get()从队列中获取一个元素,该元素是队列中的第一个元素(先进先出)。- 例如,如果队列中的元素为
[10, 20, 30],调用self._queue.get()后,percent_complete将是10,队列状态变为[20, 30]。
3. 更新进度条
self._update_bar(percent_complete) # 更新进度条
- 调用
self._update_bar(percent_complete)方法,传递获取到的percent_complete值。 _update_bar方法更新进度条并检查是否需要继续更新。
4. 处理队列为空的情况
else: if self._load_test: self.after(self._refresh_ms, self._poll_queue) # 若测试未完成,继续定时检查
- 如果队列为空,进入
else代码块。 - 检查
self._load_test是否存在,如果存在,表示压力测试仍在进行。 - 调用
self.after(self._refresh_ms, self._poll_queue),设定一个定时器,在self._refresh_ms毫秒后再次调用_poll_queue方法。这种方式可以继续定期检查队列中的进度更新。
举例说明
假设我们在进行一个压力测试,并且在不同时间点调用 _queue_update 方法,将进度百分比放入队列。
- 初始状态,队列为空:
[] - 压力测试完成了 10% 的请求,调用
_queue_update:
self._queue.put(10)
- 队列状态:
[10] - 压力测试完成了 20% 的请求,调用
_queue_update:
self._queue.put(20)
- 队列状态:
[10, 20] - 压力测试完成了 30% 的请求,调用
_queue_update:
self._queue.put(30)
- 队列状态:
[10, 20, 30]
当 _poll_queue 方法被调用时,以下是详细的执行过程:
- 检查队列是否为空,队列状态为
[10, 20, 30],不为空。 - 获取队列中的第一个元素:
percent_complete = self._queue.get()
- 此时,
percent_complete为10,队列状态变为[20, 30]。 - 调用
_update_bar方法更新进度条:
self._update_bar(10)
- 再次调用
_poll_queue方法,检查队列,队列状态为[20, 30],不为空。 - 获取队列中的第一个元素:
percent_complete = self._queue.get()
- 此时,
percent_complete为20,队列状态变为[30]。 - 调用
_update_bar方法更新进度条
self._update_bar(20)
- 再次调用
_poll_queue方法,检查队列,队列状态为[30],不为空。 - 获取队列中的第一个元素:
percent_complete = self._queue.get()
- 此时,
percent_complete为30,队列状态变为[](空)。 - 调用
_update_bar方法更新进度条:
self._update_bar(30)
1.再次调用 _poll_queue 方法,检查队列,队列状态为空,进入 else 分支:
else: if self._load_test: self.after(self._refresh_ms, self._poll_queue)
- 设置一个定时器,在
self._refresh_ms毫秒后再次调用_poll_queue方法,继续检查队列。
这种机制保证了即使队列中没有元素时,也会定期检查队列,从而确保所有进度更新都能被及时处理。
7、如何理解这句话
“当创建一个StreetTest对象,将_queue_update方法作为回调传递进来。每当有进度要更新发布时,StreetTest对象将调用该方法”
不理解回调到底是什么意思,应该如何理解?
def _start(self): # F if self._load_test is None: self._submit['text'] = 'Cancel' test = StressTest(self._loop, self._url_field.get(), int(self._request_field.get()), self._queue_update) self.after(self._refresh_ms, self._poll_queue) test.start() self._load_test = test else: self._load_test.cancel() self._load_test = None self._submit['text'] = 'Submit'
回调函数(callback)是编程中的一个重要概念,
指的是将一个函数作为参数传递给另一个函数,并在适当的时候由后者调用前者。
回调函数的主要目的是允许某个函数在完成某些任务后通知或触发另一个函数的执行。
通俗解释
想象你在家里等快递,但你不想一直站在门口等着快递员来。于是,你告诉快递员:“当你到达我家门口时,请打我的电话”。
这里,“打电话”就是一个回调。当快递员到达你家门口时,他会打电话通知你。这种机制允许你在等待快递的过程中做其他事情,而不是一直站在门口等。
在编程中,回调函数也类似:当某个任务完成或某个事件发生时,系统会自动调用你预先指定的函数,来处理后续的工作。
回调函数在 LoadTester 和 StressTest 中的应用
在 LoadTester 类中,有一个 _queue_update 方法,这个方法就是用作回调的。它的作用是更新进度,将进度百分比放入队列中。
def _queue_update(self, completed_requests: int, total_requests: int): self._queue.put(int(completed_requests / total_requests * 100))
创建 StressTest 对象时传递回调函数
当你创建一个 StressTest 对象时,将 _queue_update 方法作为回调函数传递进去:
test = StressTest(self._loop,
self._url_field.get(),
int(self._request_field.get()),
self._queue_update)
这意味着,每当 StressTest 需要发布进度更新时,它会调用 _queue_update 方法。
回调函数的实际使用
让我们看一下 StressTest 类的相关部分:
class StressTest: def __init__(self, loop, url, total_requests, callback): self._completed_requests = 0 self._loop = loop self._url = url self._total_requests = total_requests self._callback = callback # 回调函数被存储在实例变量中 async def _get_url(self, session, url): try: await session.get(url) except Exception as e: print(e) self._completed_requests += 1 if self._completed_requests % self._refresh_rate == 0 or self._completed_requests == self._total_requests: self._callback(self._completed_requests, self._total_requests) # 调用回调函数 async def _make_requests(self): async with ClientSession() as session: reqs = [self._get_url(session, self._url) for _ in range(self._total_requests)] await asyncio.gather(*reqs)
具体流程
-
初始化
StressTest对象:在初始化时,将_queue_update方法传递给StressTest对象,并保存为self._callback。 -
请求处理:在
_get_url方法中,每当一个请求完成时,_completed_requests递增。如果达到了需要更新进度的条件,就调用self._callback,也就是_queue_update方法。 -
更新进度:
_queue_update方法将当前的进度计算为百分比,并放入队列。
总结
回调函数是一种允许代码异步执行的重要机制。在 LoadTester 和 StressTest 中,_queue_update 作为回调函数传递给 StressTest 对象。
每当 StressTest 完成一些请求并需要更新进度时,它会调用 _queue_update,这保证了 LoadTester 可以及时接收到进度更新并更新界面。
举例
为了进一步说明回调的概念,下面是一个简单的例子:
def print_message(message): print(message) def execute_callback(callback): print("Doing some work...") # 工作完成后,调用回调函数 callback("Work is done!") # 使用回调函数 execute_callback(print_message)
在这个例子中,print_message 函数被作为回调传递给 execute_callback 函数。
当 execute_callback 函数完成一些工作后,它调用回调函数 print_message,并传递消息 “Work is done!”。结果,print_message 输出了这个消息。
这种机制允许 execute_callback 完成其工作后,不直接处理输出,而是通过回调函数处理。
这与 StressTest 调用 _queue_update 来更新进度的方式类似。
8、请给我举例对比说明daemon=True和False时,代码执行带来的结果差异。便于更好理解守护进程的作用。
python守护线程简介
守护线程的理解:
如果当前python线程是守护线程,那么意味着这个线程是“不重要”的,“不重要”意味着如果他的主进程结束了但该守护线程没有运行完,守护进程就会被强制结束。如果线程是非守护线程,那么父进程只有等到守护线程运行完毕后才能结束。
在python中,线程通过threadName.setDaemon(True|False)来设置是否为守护线程。
守护线程的作用:
守护线程作用是为其他线程提供便利服务,守护线程最典型的应用就是 GC (垃圾收集器)。
守护线程的特点:
只要当前 主线程中尚存任何一个非守护线程没有结束,守护线程就全部工作;
只有当最后一个非守护线程结束是,守护线程随着主线程一同结束工作。
代码实例:
情况一:线程为非守护线程
import time import threading def fun(): print("start fun") time.sleep(2) print("end fun") def main(): print("main thread") t1 = threading.Thread(target=fun,args=(),daemon=False) #t1.setDaemon(False) t1.start() time.sleep(1) print("main thread end") if __name__ == '__main__': main()
运行结果:
main thread
start fun
main thread end
end fun
说明:程序在等待子线程结束,才退出了。
情况二:线程为守护线程
import time import threading def fun(): print("start fun") time.sleep(2) print("end fun") def main(): print("main thread") t1 = threading.Thread(target=fun,args=()) t1.setDaemon(True) t1.start() time.sleep(1) print("main thread end") if __name__ == '__main__': main()
结果:
main thread
start fun
main thread end
说明:程序在主线程结束后,直接退出了。 导致子线程没有运行完。
9、我想问下,在如下代码中,设置了守护进程。实际执行效果是,程序在主线程结束后,直接退出了。 导致子线程没有运行完。
这种,难道不是会有安全隐患吗?子线程都没有运行完,专门搞出个守护进程到底是为什么?能带来哪些益处?
import time import threading def fun(): print("start fun") time.sleep(2) print("end fun") def main(): print("main thread") t1 = threading.Thread(target=fun,args=()) t1.setDaemon(True) t1.start() time.sleep(1) print("main thread end") if __name__ == '__main__': main()
在您提供的示例代码中,主线程在子线程(守护线程)执行完成之前就结束了,导致子线程被强制终止,这确实可能会带来一些安全隐患或数据丢失的风险。
然而,守护线程的存在是为了满足某些特定的需求和场景,它们确实能带来一些益处。让我来解释一下:
- 守护线程的作用
守护线程的主要作用是为其他线程提供服务,比如日志记录、监控、后台任务等。它们通常不会执行关键的业务逻辑,而是充当辅助角色。
- 主线程退出后,守护线程自动退出的优势
如果主线程退出后,守护线程继续运行,就可能导致无主线程的孤立线程存在,浪费系统资源。通过自动退出守护线程,可以避免这种情况,提高系统的稳定性和效率。
- 守护线程的使用场景
守护线程更适合于一些不重要或可重试的任务,例如:
- 日志记录:即使日志记录线程被中断,也不会影响主要业务逻辑
- 网络连接维护:断开连接后,可以重新建立连接
- 定期清理缓存:如果中断,下次再清理即可
但对于一些关键任务,如数据库事务、文件写入等,确实不适合使用守护线程,因为中断可能导致数据丢失或不一致。
- 改进您的示例代码
针对您的示例,我们可以进行以下改进,避免主线程过早退出:
import time import threading def fun(): print("start fun") time.sleep(10) # 这里改成10秒,输出结果也是如下,这样更好体会 .join()的阻塞作用; print("end fun") def main(): print("main thread") t1 = threading.Thread(target=fun) t1.start() t1.join() # 让主线程等待子线程结束 print("main thread end") if __name__ == '__main__': main()
# 返回结果
main thread
start fun
end fun
main thread end
在这个改进版本中,我们没有设置t1为守护线程, 而是在主线程中使用t1.join()等待子线程执行完成后,再退出主线程。这样可以确保子线程的任务能够完整执行。
总的来说,守护线程是为了满足特定的需求而设计的,在适当的场景下使用它们是有益的。
但对于一些关键任务,我们需要更加谨慎,确保线程能够安全地执行完成,避免潜在的风险。在使用守护线程时,需要权衡利弊,并根据具体情况进行选择。
10、还有这个t1.join() # 等待子线程结束?我不太理解.join()这个方法,按照英文翻译,join()应该是参加的意思,
但是这里执行的含义却是_等待子线程结束,这就有些违反人的直觉,应该如何理解?
join()这个方法的命名确实有点违反直觉,让人难以直观理解它的作用。下面我来详细解释一下join()方法的含义和使用方式:
在Python的threading模块中,Thread类的join()方法是用于阻塞当前线程,直到被join()的线程终止运行。
更具体地说,当我们在主线程中调用某个子线程的join()方法时,主线程会被阻塞(暂停执行),直到这个子线程完成运行才会继续执行主线程后面的代码。
例如,在您的改进代码中:
t1 = threading.Thread(target=fun) t1.start() t1.join() # 等待子线程结束 print("main thread end")
这段代码的执行流程如下:
- 首先,
t1线程被创建和启动。 - 接着,
t1.join()被调用,主线程会在这里被阻塞,等待t1线程运行结束。 - 当
t1线程运行完fun()函数后终止时, 主线程解除阻塞,继续执行。 - 最后,
"main thread end"被打印出来。
所以,join()的作用就是使主线程等待子线程完成,避免主线程先行退出。这对于需要保证线程任务完整性的情况是非常有用的。
您指出join这个单词本身的含义是"加入"或"参加",确实与这个方法的实际用途有点违背直觉。
可能当初设计这个方法时,是希望主线程加入(join)子线程的执行,等待子线程完成后才继续,但是这种命名确实不太直观。
总的来说,join()方法提供了一种简单的线程同步机制,让主线程等待子线程的完成,保证了程序执行的正确性。
尽管命名可能有些违反直觉,但了解它的实际作用还是很有必要的。
11、追问:为什么需要守护进程,线程?
为什么需要守护进程?
守护进程的主要作用是为其他进程服务,它自己不能独立运行,必须依附于其他进程。当没有其他进程需要它时,守护进程会自动退出。
在上面的例子中,主线程结束后,就没有其他进程需要守护进程t2了,所以它被强制终止。相反,非守护进程t1则一直运行直到完成。
一些典型的守护进程应用包括:
- 后台服务器: 比如Web服务器、数据库服务器等,它们作为守护进程运行,为其他进程提供服务。
- 日志记录进程: 记录其他进程的日志信息。
- 监控进程: 监视其他进程的运行状态。
- 辅助进程: 为主进程提供一些辅助功能,如编码解码、压缩解压等。
通过使用守护进程,我们可以更好地管理和控制进程的生命周期,避免出现僵尸进程,提高系统的稳定性和效率。
12、asyncio.get_event_loop()和asyncio.get_running_loop()的异同理解
asyncio.get_event_loop() 和 asyncio.get_running_loop() 都是用于获取当前事件循环的方法,但它们存在一些区别和应用场景。
asyncio.get_event_loop()
get_event_loop() 函数用于获取当前线程的事件循环实例。如果当前线程中还没有事件循环实例,它会创建一个新的实例。这个函数适用于在一个单独的线程中运行异步代码。
示例:
import asyncio async def hello_world(): print("Hello, World!") # 获取当前线程的事件循环 loop = asyncio.get_event_loop() # 运行协程 loop.run_until_complete(hello_world()) loop.close()
asyncio.get_running_loop()
get_running_loop() 函数用于获取当前线程的正在运行的事件循环实例。如果当前线程中没有正在运行的事件循环,它会引发 RuntimeError 异常。
这个函数通常在异步函数内部使用,因为异步函数必须在一个正在运行的事件循环中执行。
示例:
import asyncio async def hello_world(): loop = asyncio.get_running_loop() print("Hello, World!") async def main(): await hello_world() loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close()
区别和异同:
get_event_loop()可以在任何线程中使用, 而get_running_loop()只能在有事件循环正在运行的线程中使用。get_event_loop()如果当前线程中没有事件循环实例,会创建一个新实例。而get_running_loop()如果当前线程中没有正在运行的事件循环实例,会引发异常。- 两个函数都返回当前线程的事件循环实例, 但
get_running_loop()要求事件循环必须正在运行。
总结:
- 如果你需要在一个单独的线程中运行异步代码, 并且需要创建一个新的事件循环实例, 那么使用
asyncio.get_event_loop()。 - 如果你需要在一个异步函数内部获取当前正在运行的事件循环实例,那么使用
asyncio.get_running_loop()。
通常情况下,在主程序中使用 get_event_loop() 来创建和运行事件循环,而在异步函数内部使用 get_running_loop() 来获取当前的事件循环实例。
13、asyncio.run()和run_until_complete()的异同,后者到底是什么时候使用?
asyncio.run() 和 asyncio.run_until_complete() 都是在 asyncio 中运行协程的方法,但它们有一些区别和不同的使用场景。
asyncio.run()
asyncio.run() 是 Python 3.7 中引入的一种更简洁、更方便的方式来运行 asyncio 程序。
它会自动创建一个事件循环,运行传入的协程,并在协程执行完毕后自动关闭事件循环。
示例:
import asyncio async def main(): print("Hello, World!") asyncio.run(main())
asyncio.run_until_complete()
asyncio.run_until_complete() 是一种更低层次的方法, 需要手动创建事件循环并传入协程。它会一直运行协程直到协程执行完毕或发生异常。
示例:
import asyncio async def main(): print("Hello, World!") loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close()
区别和异同:
asyncio.run()是一个高层次的函数,自动处理事件循环的创建和关闭,而run_until_complete()需要手动创建和关闭事件循环。asyncio.run()只能在主线程中使用,而run_until_complete()可以在任何线程中使用。asyncio.run()在协程执行完毕后会自动关闭事件循环,而run_until_complete()需要手动关闭事件循环。- 两个函数都可以用于运行协程,但
asyncio.run()更加简洁方便。
什么时候使用 run_until_complete()?
通常情况下,如果你只是想运行一个简单的 asyncio 程序,可以优先使用 asyncio.run()。但是,在以下情况下,你可能需要使用 run_until_complete():
- 当你需要在其他线程中运行协程时,因为
asyncio.run()只能在主线程中使用。 - 当你需要自定义事件循环的行为或配置时,例如设置一个不同的事件循环策略。
- 当你需要在一个现有的事件循环中运行多个协程时,可以使用
run_until_complete()来运行每个协程。
总的来说,asyncio.run() 更加简洁方便,适用于大多数情况。而 run_until_complete() 提供了更多的灵活性和控制权,可以在一些特殊情况下使用。
14、追问:我平常在pycharm中写的一个脚本比如, test.py 这个模块,在执行时这个模块是算一个进程呢?还是一个线程?
在 PyCharm 中运行一个 Python 脚本时,例如 test.py,它实际上是作为一个单独的进程运行的。
在 Python 中,每个运行的程序都被视为一个进程。当您执行 test.py 时,Python 解释器会创建一个新的进程来运行这个脚本。这个进程是操作系统级别的概念, 具有自己的内存空间、文件描述符等资源。
在这个进程内部,Python 使用线程来实现并发。Python 解释器启动时会自动创建一个主线程,也就是主执行线程。所有的代码都在这个主线程中执行,除非您显式地创建了其他线程。
因此,当您运行 test.py 时,它是作为一个单独的进程启动的,但在这个进程内部只有一个主线程在运行,除非您在代码中创建了其他线程。
值得注意的是,如果您的 test.py 脚本中使用了像 asyncio 这样的异步编程库,那么虽然在逻辑上看起来像是并发执行多个任务,但实际上它们仍然是在同一个主线程中运行的,只是通过事件循环来实现协作式多任务的效果。
所以,总的来说,一个 Python 脚本就相当于一个进程,在这个进程内部至少有一个主线程在执行,除非您显式地创建了其他线程。
15、追问:那上面问题13在解释其他问题时,有说到因为 asyncio.run() 只能在主线程中使用。?那这里的主线程又指的是啥呢?问题14不是说test.py是作为一个单独的进程来运行的?
之前提到 asyncio.run() 只能在主线程中使用时,我使用了"主线程"这个术语,它可能会引起一些混淆。让我更清楚地解释一下:
您完全正确,当您运行 test.py 脚本时,它作为一个单独的进程启动。在这个进程内部,Python 会自动创建一个主线程,也被称为主执行线程。
所以,当我说 asyncio.run() 只能在主线程中使用时, 我的意思是它只能在这个进程的主执行线程中使用, 而不能在您手动创建的其他线程中使用。
换句话说,asyncio.run() 只能在启动 Python 进程时自动创建的那个主线程中调用, 而不能在您通过 threading 模块或其他方式显式创建的线程中调用。
这个限制是由 asyncio 的设计所决定的,目的是为了避免多个事件循环同时运行而引起的竞争条件和其他潜在问题。
因此,虽然 test.py 是作为一个独立的进程运行的,但在这个进程内部,只有自动创建的主线程才能使用 asyncio.run()。如果您需要在其他线程中运行异步代码,那么就需要使用 asyncio.run_until_complete() 或其他方式。
总之,术语"主线程"是相对于进程内部的执行线程而言的,而不是指整个操作系统级别的进程本身。希望这个解释能更好地阐明我之前的表述。

浙公网安备 33010602011771号