Python 多线程学习与使用

news2024/10/21 9:16:18

Python 多线程学习与使用

目录

  1. 引言:为什么需要多线程?
  2. Python中的线程基础
    2.1 什么是线程?
    2.2 Python的threading模块
    2.3 创建和启动线程
  3. 线程同步与互斥
    3.1 竞态条件
    3.2 锁(Lock)
    3.3 可重入锁(RLock)
    3.4 信号量(Semaphore)
    3.5 事件(Event)
    3.6 条件变量(Condition)
  4. 线程池
    4.1 concurrent.futures模块
    4.2 自定义线程池
  5. 多线程编程最佳实践
    5.1 避免死锁
    5.2 线程安全的数据结构
    5.3 线程本地存储
  6. Python的全局解释器锁(GIL)
    6.1 GIL的影响
    6.2 如何绕过GIL的限制
  7. 多线程vs多进程
  8. 实战案例:多线程Web爬虫
  9. 性能优化与调试
    9.1 使用cProfile进行性能分析
    9.2 多线程调试技巧
  10. 总结与展望

引言:为什么需要多线程?

在当今的计算环境中,多线程编程已经成为一项不可或缺的技能。随着硬件性能的不断提升,多核处理器已经成为主流,而多线程编程允许我们充分利用这些硬件资源,提高程序的执行效率和响应能力。

Python作为一种versatile的编程语言,提供了强大的多线程支持。通过使用多线程,我们可以:

  1. 提高程序的并发性:同时执行多个任务,充分利用CPU资源。
  2. 改善用户体验:在执行耗时操作时保持界面响应。
  3. 优化I/O密集型任务:在等待I/O操作完成时执行其他任务。
  4. 简化复杂系统的设计:将大型任务分解为多个并发执行的小任务。

然而,多线程编程也带来了一些挑战,如竞态条件、死锁和复杂的调试过程。本文将深入探讨Python多线程编程的方方面面,帮助你掌握这一强大的技术。

Python中的线程基础

什么是线程?

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

在Python中,由于全局解释器锁(GIL)的存在,多线程主要用于I/O密集型任务,而不是CPU密集型任务。这一点我们稍后会详细讨论。

Python的threading模块

Python的threading模块提供了多线程编程的核心功能。让我们从一个简单的例子开始,了解如何使用这个模块:

import threading
import time

def print_numbers():
    for i in range(5):
        time.sleep(1)
        print(f"Thread {threading.current_thread().name}: {i}")

# 创建两个线程
thread1 = threading.Thread(target=print_numbers, name="Thread-1")
thread2 = threading.Thread(target=print_numbers, name="Thread-2")

# 启动线程
thread1.start()
thread2.start()

# 等待线程结束
thread1.join()
thread2.join()

print("All threads have finished.")

输出结果:

Thread Thread-1: 0
Thread Thread-2: 0
Thread Thread-1: 1
Thread Thread-2: 1
Thread Thread-1: 2
Thread Thread-2: 2
Thread Thread-1: 3
Thread Thread-2: 3
Thread Thread-1: 4
Thread Thread-2: 4
All threads have finished.

在这个例子中,我们创建了两个线程,每个线程都执行print_numbers函数。这个函数简单地打印0到4的数字,每次打印之间有1秒的延迟。我们可以看到两个线程几乎同时开始执行,并且交替打印数字。

创建和启动线程

在Python中,有两种主要的方式来创建线程:

  1. 通过传递一个可调用对象给Thread类(如上面的例子)
  2. 继承Thread类并重写run方法

让我们看看第二种方式的例子:

import threading
import time

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

    def run(self):
        for i in range(5):
            time.sleep(1)
            print(f"Thread {self.name}: {i}")

# 创建两个线程
thread1 = MyThread("Thread-1")
thread2 = MyThread("Thread-2")

# 启动线程
thread1.start()
thread2.start()

# 等待线程结束
thread1.join()
thread2.join()

print("All threads have finished.")

输出结果与前一个例子类似。

这两种方法各有优缺点:

  • 使用Thread(target=func)更加灵活,可以轻松地将现有函数转换为线程。
  • 继承Thread类允许你在类中封装更多的状态和方法,适合更复杂的线程行为。

无论使用哪种方法,都需要调用start()方法来启动线程,调用join()方法来等待线程结束。

线程同步与互斥

当多个线程同时访问共享资源时,会导致数据不一致或者其他意外的行为。这就是所谓的竞态条件(Race Condition)。为了避免这种情况,我们需要使用同步机制。

竞态条件

让我们通过一个例子来理解竞态条件:

import threading

counter = 0

def increment():
    global counter
    for _ in range(100000):
        counter += 1

# 创建两个线程
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)

# 启动线程
thread1.start()
thread2.start()

# 等待线程结束
thread1.join()
thread2.join()

print(f"Final counter value: {counter}")

你会期望最终的计数器值是200000,但实际运行这个程序多次,你会发现结果常常小于200000。这就是竞态条件的结果。

锁(Lock)

为了解决这个问题,我们可以使用锁:

import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        with lock:
            counter += 1

# 创建两个线程
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)

# 启动线程
thread1.start()
thread2.start()

# 等待线程结束
thread1.join()
thread2.join()

print(f"Final counter value: {counter}")

现在,无论运行多少次,结果都会是200000。

可重入锁(RLock)

可重入锁允许同一个线程多次获得同一个锁,而不会导致死锁。这在递归函数中特别有用:

import threading

class X:
    def __init__(self):
        self.a = 1
        self.b = 2
        self.lock = threading.RLock()

    def change(self):
        with self.lock:
            self.a = self.a + self.b

    def change2(self):
        with self.lock:
            self.b = self.a + self.b

    def change_both(self):
        with self.lock:
            self.change()
            self.change2()

x = X()
x.change_both()
print(f"a: {x.a}, b: {x.b}")

输出结果:

a: 3, b: 5

如果使用普通的Lockchange_both方法会导致死锁。

信号量(Semaphore)

信号量用于控制同时访问特定资源的线程数量:

import threading
import time

# 创建一个信号量,最多允许5个线程同时访问
semaphore = threading.Semaphore(5)

def access_resource(thread_number):
    print(f"Thread {thread_number} is trying to access the resource")
    with semaphore:
        print(f"Thread {thread_number} has accessed the resource")
        time.sleep(1)
    print(f"Thread {thread_number} has released the resource")

# 创建10个线程
threads = []
for i in range(10):
    thread = threading.Thread(target=access_resource, args=(i,))
    threads.append(thread)
    thread.start()

# 等待所有线程结束
for thread in threads:
    thread.join()

print("All threads have finished.")

这个例子中,尽管我们创建了10个线程,但每次只有5个线程能同时访问资源。

事件(Event)

事件对象用于线程之间的通信:

import threading
import time

# 创建一个事件对象
event = threading.Event()

def waiter(name):
    print(f"{name} is waiting for the event")
    event.wait()
    print(f"{name} received the event")

def setter():
    print("Setter is sleeping")
    time.sleep(3)
    print("Setter is setting the event")
    event.set()

# 创建线程
threads = []
for i in range(3):
    thread = threading.Thread(target=waiter, args=(f"Waiter-{i}",))
    threads.append(thread)
    thread.start()

setter_thread = threading.Thread(target=setter)
setter_thread.start()

# 等待所有线程结束
for thread in threads:
    thread.join()
setter_thread.join()

print("All threads have finished.")

在这个例子中,多个"waiter"线程等待事件被设置,而"setter"线程在睡眠3秒后设置事件。

条件变量(Condition)

条件变量允许线程等待某个条件成立,然后再继续执行:

import threading
import time

# 创建一个条件变量
condition = threading.Condition()
items = []

def consumer():
    with condition:
        while True:
            if items:
                item = items.pop(0)
                print(f"Consumer got {item}")
                condition.notify()
            else:
                print("Consumer is waiting")
                condition.wait()
            time.sleep(0.5)

def producer():
    for i in range(5):
        with condition:
            item = f"item-{i}"
            items.append(item)
            print(f"Producer added {item}")
            condition.notify()
        time.sleep(1)

# 创建消费者和生产者线程
consumer_thread = threading.Thread(target=consumer)
producer_thread = threading.Thread(target=producer)

# 启动线程
consumer_thread.start()
producer_thread.start()

# 等待生产者线程结束
producer_thread.join()

# 设置一个标志来停止消费者线程
with condition:
    items.append(None)  # 使用None作为结束信号
    condition.notify()

# 等待消费者线程结束
consumer_thread.join()

print("All threads have finished.")

输出结果如下:

Consumer is waiting
Producer added item-0
Consumer got item-0
Consumer is waiting
Producer added item-1
Consumer got item-1
Consumer is waiting
Producer added item-2
Consumer got item-2
Consumer is waiting
Producer added item-3
Consumer got item-3
Consumer is waiting
Producer added item-4
Consumer got item-4
Consumer is waiting
All threads have finished.

这个例子展示了一个经典的生产者-消费者模型。生产者线程生产项目并添加到列表中,而消费者线程从列表中消费项目。条件变量用于协调两个线程的行为,确保消费者在列表为空时等待,而生产者在添加项目后通知消费者。

线程池

在实际应用中,我们经常需要处理大量的并发任务。如果为每个任务都创建一个新的线程,会导致系统资源的过度消耗。线程池通过重用一组固定数量的线程来执行任务,从而提高了效率。

concurrent.futures模块

Python的concurrent.futures模块提供了高级的异步执行接口,包括线程池的实现。

让我们看一个使用ThreadPoolExecutor的例子:

import concurrent.futures
import time

def task(name):
    print(f"Task {name} starting")
    time.sleep(1)  # 模拟耗时操作
    print(f"Task {name} completed")
    return f"Result of task {name}"

# 创建一个最多包含5个线程的线程池
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    # 提交10个任务到线程池
    future_to_name = {executor.submit(task, f"Task-{i}"): f"Task-{i}" for i in range(10)}
    
    # 获取任务的结果
    for future in concurrent.futures.as_completed(future_to_name):
        name = future_to_name[future]
        try:
            result = future.result()
            print(f"{name} returned {result}")
        except Exception as exc:
            print(f"{name} generated an exception: {exc}")

print("All tasks have been processed.")

输出结果如下:

Task Task-0 starting
Task Task-1 starting
Task Task-2 starting
Task Task-3 starting
Task Task-4 starting
Task Task-0 completed
Task Task-5 starting
Task Task-1 completed
Task Task-6 starting
Task Task-2 completed
Task Task-7 starting
Task Task-3 completed
Task Task-8 starting
Task Task-4 completed
Task Task-9 starting
Task Task-5 completed
Task Task-6 completed
Task Task-7 completed
Task Task-8 completed
Task Task-9 completed
Task-0 returned Result of task Task-0
Task-1 returned Result of task Task-1
Task-2 returned Result of task Task-2
Task-3 returned Result of task Task-3
Task-4 returned Result of task Task-4
Task-5 returned Result of task Task-5
Task-6 returned Result of task Task-6
Task-7 returned Result of task Task-7
Task-8 returned Result of task Task-8
Task-9 returned Result of task Task-9
All tasks have been processed.

在这个例子中,我们创建了一个最多包含5个线程的线程池,并向其提交了10个任务。尽管有10个任务,但在任何时候最多只有5个任务在并发执行。

自定义线程池

虽然concurrent.futures模块提供了强大的线程池实现,但有时我们需要更多的控制。以下是一个简单的自定义线程池实现:

import threading
import queue
import time

class ThreadPool:
    def __init__(self, num_threads):
        self.tasks = queue.Queue()
        self.results = queue.Queue()
        self.threads = []
        for _ in range(num_threads):
            thread = threading.Thread(target=self.worker)
            thread.start()
            self.threads.append(thread)

    def worker(self):
        while True:
            func, args = self.tasks.get()
            if func is None:
                break
            result = func(*args)
            self.results.put(result)
            self.tasks.task_done()

    def submit(self, func, *args):
        self.tasks.put((func, args))

    def shutdown(self):
        for _ in self.threads:
            self.tasks.put((None, None))
        for thread in self.threads:
            thread.join()

    def get_results(self):
        results = []
        while not self.results.empty():
            results.append(self.results.get())
        return results

# 使用自定义线程池
def task(name):
    print(f"Task {name} starting")
    time.sleep(1)
    print(f"Task {name} completed")
    return f"Result of task {name}"

pool = ThreadPool(5)

for i in range(10):
    pool.submit(task, f"Task-{i}")

pool.tasks.join()  # 等待所有任务完成
pool.shutdown()

results = pool.get_results()
for result in results:
    print(result)

print("All tasks have been processed.")

这个自定义线程池的实现提供了与concurrent.futures.ThreadPoolExecutor类似的功能,但给了我们更多的控制权。例如,我们可以轻松地添加优先级队列、动态调整线程数量等功能。

多线程编程最佳实践

避免死锁

死锁是多线程编程中的一个常见问题。它发生在两个或更多线程互相等待对方释放资源的情况。以下是一些避免死锁的建议:

  1. 保持一致的锁定顺序:如果多个线程需要获取多个锁,确保它们以相同的顺序获取这些锁。

  2. 使用超时机制:在尝试获取锁时使用超时,而不是无限期地等待。

  3. 避免嵌套锁:尽量减少在持有一个锁的同时获取另一个锁的情况。

  4. 使用threading.Lock()代替threading.RLock():除非确实需要可重入性,否则使用普通锁可以帮助发现潜在的死锁问题。

让我们看一个导致死锁的例子,以及如何修复它:

import threading
import time

# 导致死锁的代码
def task1(lock1, lock2):
    with lock1:
        print("Task 1 acquired lock 1")
        time.sleep(0.5)
        with lock2:
            print("Task 1 acquired lock 2")

def task2(lock1, lock2):
    with lock2:
        print("Task 2 acquired lock 2")
        time.sleep(0.5)
        with lock1:
            print("Task 2 acquired lock 1")

lock1 = threading.Lock()
lock2 = threading.Lock()

thread1 = threading.Thread(target=task1, args=(lock1, lock2))
thread2 = threading.Thread(target=task2, args=(lock1, lock2))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print("Tasks completed")

这段代码会导致死锁,因为两个线程以不同的顺序获取锁。修复这个问题的一种方法是确保所有线程以相同的顺序获取锁:

import threading
import time

# 修复后的代码
def task1(lock1, lock2):
    with lock1:
        print("Task 1 acquired lock 1")
        time.sleep(0.5)
        with lock2:
            print("Task 1 acquired lock 2")

def task2(lock1, lock2):
    with lock1:  # 改变了获取锁的顺序
        print("Task 2 acquired lock 1")
        time.sleep(0.5)
        with lock2:
            print("Task 2 acquired lock 2")

lock1 = threading.Lock()
lock2 = threading.Lock()

thread1 = threading.Thread(target=task1, args=(lock1, lock2))
thread2 = threading.Thread(target=task2, args=(lock1, lock2))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print("Tasks completed")

线程安全的数据结构

使用线程安全的数据结构可以大大简化多线程编程。Python的queue模块提供了几种线程安全的队列实现:

import queue
import threading
import time

def producer(q):
    for i in range(5):
        item = f"item-{i}"
        q.put(item)
        print(f"Produced {item}")
        time.sleep(1)

def consumer(q):
    while True:
        item = q.get()
        if item is None:
            break
        print(f"Consumed {item}")
        q.task_done()

q = queue.Queue()

producer_thread = threading.Thread(target=producer, args=(q,))
consumer_thread = threading.Thread(target=consumer, args=(q,))

producer_thread.start()
consumer_thread.start()

producer_thread.join()
q.put(None)  # 发送结束信号
consumer_thread.join()

print("All done")

这个例子使用了queue.Queue,这是一个线程安全的FIFO(先进先出)队列。它自动处理了线程同步,使得生产者和消费者可以安全地共享数据。

线程本地存储

有时,我们需要每个线程都有自己的数据副本。Python的threading.local()提供了一种简单的方式来实现线程本地存储:

import threading
import random

# 创建线程本地存储
thread_local = threading.local()

def worker():
    # 为每个线程设置一个随机数
    thread_local.value = random.randint(1, 100)
    print(f"Thread {threading.current_thread().name} has value {thread_local.value}")

threads = []
for i in range(5):
    thread = threading.Thread(target=worker, name=f"Thread-{i}")
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print("All threads have finished.")

输出如下:

Thread Thread-0 has value 42
Thread Thread-1 has value 23
Thread Thread-2 has value 87
Thread Thread-3 has value 11
Thread Thread-4 has value 59
All threads have finished.

每个线程都有自己的value属性,互不干扰。

Python的全局解释器锁(GIL)

Python的全局解释器锁(Global Interpreter Lock,简称GIL)是Python解释器中的一个机制,它确保同一时刻只有一个线程在执行Python字节码。这意味着在多核CPU上,Python的多线程并不能真正地并行执行。

GIL的影响

GIL的存在主要影响CPU密集型任务的性能。对于I/O密集型任务,GIL的影响较小,因为在I/O操作期间,Python会释放GIL。

让我们通过一个例子来看看GIL的影响:

import time
import threading

def cpu_bound(n):
    while n > 0:
        n -= 1

def run_serial(n):
    start = time.time()
    cpu_bound(n)
    cpu_bound(n)
    end = time.time()
    print(f"Serial execution time: {end - start:.2f} seconds")

def run_parallel(n):
    start = time.time()
    t1 = threading.Thread(target=cpu_bound, args=(n,))
    t2 = threading.Thread(target=cpu_bound, args=(n,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    end = time.time()
    print(f"Parallel execution time: {end - start:.2f} seconds")

if __name__ == "__main__":
    n = 100000000
    run_serial(n)
    run_parallel(n)

在多核CPU上运行这段代码,你会发现并行执行的时间与串行执行的时间相近,有时甚至更长。这就是GIL的影响。

如何绕过GIL的限制

虽然我们不能完全摆脱GIL,但有几种方法可以减轻其影响:

  1. 使用多进程代替多线程:对于CPU密集型任务,可以使用multiprocessing模块来实现真正的并行计算。

  2. 使用Python的替代实现:如PyPy、Jython或IronPython,它们对GIL的处理方式不同。

  3. 使用Cython或其他C扩展:将CPU密集型代码编写为C扩展可以绕过GIL。

  4. 使用异步编程:对于I/O密集型任务,使用asyncio模块可以提高并发性能。

让我们看一个使用多进程的例子:

import time
import multiprocessing
def cpu_bound(n):
    while n > 0:
        n -= 1

def run_multiprocess(n):
    start = time.time()
    p1 = multiprocessing.Process(target=cpu_bound, args=(n,))
    p2 = multiprocessing.Process(target=cpu_bound, args=(n,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    end = time.time()
    print(f"Multiprocess execution time: {end - start:.2f} seconds")

if __name__ == "__main__":
    n = 100000000
    run_serial(n)
    run_parallel(n)
    run_multiprocess(n)

在多核CPU上运行这段代码,你会发现多进程的执行时间明显短于多线程和串行执行的时间。这是因为多进程可以真正地利用多个CPU核心进行并行计算。

多线程vs多进程

让我们比较一下多线程和多进程的特点:

  1. 内存使用

    • 多线程:线程共享同一进程的内存空间,内存占用较小。
    • 多进程:每个进程有独立的内存空间,内存占用较大。
  2. CPU利用

    • 多线程:受GIL限制,不能充分利用多核CPU。
    • 多进程:可以充分利用多核CPU。
  3. 启动速度

    • 多线程:启动速度快。
    • 多进程:启动速度相对较慢。
  4. 数据共享

    • 多线程:可以直接共享数据,但需要注意线程安全。
    • 多进程:需要使用特殊的机制(如管道、队列)来共享数据。
  5. 适用场景

    • 多线程:I/O密集型任务。
    • 多进程:CPU密集型任务。

实战案例:多线程Web爬虫

让我们通过一个实际的例子来综合运用我们学到的多线程知识。我们将创建一个简单的多线程Web爬虫,它可以并发地下载多个网页。

import requests
import threading
import time
from queue import Queue
from urllib.parse import urljoin

class WebCrawler:
    def __init__(self, base_url, num_threads=5):
        self.base_url = base_url
        self.num_threads = num_threads
        self.queue = Queue()
        self.visited = set()
        self.lock = threading.Lock()

    def crawl(self):
        self.queue.put(self.base_url)
        threads = []
        for _ in range(self.num_threads):
            thread = threading.Thread(target=self.worker)
            thread.start()
            threads.append(thread)

        for thread in threads:
            thread.join()

    def worker(self):
        while True:
            try:
                url = self.queue.get(timeout=5)
                self.process_url(url)
            except Queue.Empty:
                return

    def process_url(self, url):
        with self.lock:
            if url in self.visited:
                return
            self.visited.add(url)

        try:
            response = requests.get(url, timeout=5)
            print(f"Crawled: {url} (Status: {response.status_code})")

            # 简单的链接提取
            for link in response.text.split('href="')[1:]:
                link = link.split('"')[0]
                absolute_link = urljoin(url, link)
                if absolute_link.startswith(self.base_url):
                    self.queue.put(absolute_link)
        except Exception as e:
            print(f"Error crawling {url}: {e}")

        self.queue.task_done()

if __name__ == "__main__":
    start_time = time.time()
    crawler = WebCrawler("https://python.org", num_threads=10)
    crawler.crawl()
    end_time = time.time()
    print(f"Total time: {end_time - start_time:.2f} seconds")
    print(f"Total URLs crawled: {len(crawler.visited)}")

这个爬虫使用了以下多线程技术:

  1. 线程池:创建固定数量的工作线程。
  2. 队列:用于存储待爬取的URL。
  3. 锁:保护共享的visited集合。

运行这个爬虫,你会看到它能够并发地爬取多个页面,大大提高了爬取速度。

性能优化与调试

使用cProfile进行性能分析

Python的cProfile模块是一个强大的性能分析工具。让我们用它来分析我们的爬虫:

import cProfile
import pstats

def run_crawler():
    crawler = WebCrawler("https://python.org", num_threads=10)
    crawler.crawl()

cProfile.run('run_crawler()', 'crawler_stats')

# 分析结果
p = pstats.Stats('crawler_stats')
p.sort_stats('cumulative').print_stats(10)

这将显示爬虫中最耗时的函数调用,帮助你找出性能瓶颈。

多线程调试技巧

调试多线程程序很棘手。以下是一些有用的技巧:

  1. 使用日志而不是print语句:日志是线程安全的,并且可以包含更多信息。
import logging

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(threadName)s - %(message)s')

def worker():
    logging.debug("Worker started")
    # ... 工作代码 ...
    logging.debug("Worker finished")
  1. 使用线程名称:给线程命名可以帮助你追踪每个线程的行为。
thread = threading.Thread(target=worker, name="WorkerThread-1")
  1. 使用断言:断言可以帮助你捕获意外的状态。
def process_data(data):
    assert len(data) > 0, "Data should not be empty"
    # ... 处理数据 ...
  1. 使用threading.current_thread():这个函数可以帮你识别当前正在执行的线程。
def worker():
    current_thread = threading.current_thread()
    print(f"This is thread {current_thread.name}")
  1. 使用线程安全的队列进行通信:这可以帮助你追踪线程间的数据流。
import queue

def producer(q):
    for i in range(5):
        q.put(f"item-{i}")

def consumer(q):
    while True:
        item = q.get()
        if item is None:
            break
        print(f"Processed {item}")
        q.task_done()

q = queue.Queue()
threading.Thread(target=producer, args=(q,)).start()
threading.Thread(target=consumer, args=(q,)).start()

总结与展望

在这篇深入的文章中,我们探讨了Python多线程编程的方方面面,从基础概念到高级技巧。我们学习了如何创建和管理线程,如何使用同步原语来协调线程行为,如何避免常见的陷阱如死锁,以及如何处理Python特有的GIL问题。

多线程编程是一个强大的工具,可以显著提高程序的性能和响应能力,特别是在处理I/O密集型任务时。然而,它也带来了额外的复杂性和潜在的问题。熟练掌握多线程编程需要大量的实践和经验。

随着技术的发展,Python的并发编程领域也在不断演进。异步编程(如asyncio模块)和函数式编程范式为并发提供了新的方法。此外,随着硬件的发展,如量子计算的出现,未来的并发编程会有革命性的变化。

作为一个Python开发者,持续学习和实践多线程编程技术将使你能够创建更高效、更强大的应用程序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2219929.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何将LiDAR坐标系下的3D点投影到相机2D图像上

将激光雷达点云投影到相机图像上做数据层的前融合,或者把激光雷达坐标系下标注的物体点云的3d bbox投影到相机图像上画出来,都需要做点云3D点坐标到图像像素坐标的转换计算,也就是LiDAR 3D坐标转像素坐标。 看了网上一些文章都存在有错误或者…

Android 第5种启动模式:singleInstancePerTask

Android 第5种启动模式:singleInstancePerTask 随着 Android 版本的更新,应用启动模式逐渐丰富。在 Android 12 中,新增了一种启动模式——singleInstancePerTask。它是继 standard、singleTop、singleTask 和 singleInstance 之后的第五种启…

一起搭WPF架构之LiveCharts.Wpf的简单了解与安装

一起搭WPF架构之LiveCharts.Wpf的简单了解与安装 前言LiveCharts.Wpf介绍LiveCharts.Wpf的安装总结 前言 根据项目需求,我单独留了一个界面用于进行数据分析。数据分析的内容考虑是采用图表的形式将SQLite数据库中存储的数据进行绘制成图,以便数据分析。…

HTB:Broker[WriteUP]

目录 连接至HTB服务器并启动靶机 1.Which open TCP port is running the ActiveMQ service? 使用fscan对靶机开放端口进行扫描 使用nmap对靶机开放端口进行脚本、服务信息扫描 2.What is the version of the ActiveMQ service running on the box? 3.What is the 2023 …

windows下安装VirtualBox7.1.4

记录详细的安装过程与遇到的问题; 下载地址 virtualbox官网 清华镜像源下载 下载完成后文件: 双击打开; 报错了 意思是需要pc上先安装Microsoft Visual C 2019 https://learn.microsoft.com/zh-cn/cpp/windows/latest-supported-vc-redi…

C++ 中的线程、锁、条件变量。

线程 0.Linux中有POSIX标准的线程&#xff0c;Boost库中也支持线程&#xff08;比如thread_group 和 upgrade_lock &#xff09;&#xff0c;C11<thread>头文件中也提供了相应的API支持我们使用线程。它们三个&#xff0c;你学会一个&#xff0c;自然触类旁通。 1.创建…

Java-类与对象-下篇

关于类与对象&#xff0c;内容较多&#xff0c;我们分为两篇进行讲解&#xff1a; &#x1f4da; Java-类与对象-上篇&#xff1a;————<传送门:Java-类与对象-上篇-CSDN博客> &#x1f4d5; 面向对象的概念 &#x1f4d5; 类的定义格式 &#x1f4d5; 类的使用 …

特斯拉Optimus:展望智能生活新篇章

近日&#xff0c;特斯拉举办了 "WE ROBOT" 发布会&#xff0c;发布会上描绘的未来社会愿景&#xff0c;让无数人为之向往。在这场吸引全球无数媒体的直播中&#xff0c;特斯拉 Optimus 人形机器人一出场就吸引了所有观众的关注。从多家媒体现场拍摄的视频可以看出来&…

【C++】C++11新特性——右值引用,来看看怎么个事儿

&#x1f680;个人主页&#xff1a;小羊 &#x1f680;所属专栏&#xff1a;C 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 前言一、左值引用和右值引用二、右值引用和移动语义2.1 移动构造2.2 移动赋值2.3 STL容器插入接口2.4 左值右值相互…

【C++复习】经典笔试题

文章目录 八大排序快排过程 卡特兰数反转链表链表的回文结构左叶子之和另一棵树的子树归并排序类与对象编程训练杨辉三角字符串乘积二叉树前序遍历成字符串数组的交集二叉树的非递归前序遍历连续子数组的最大乘积 八大排序 插冒归稳定 快排过程 以 [3,4,6,1,2,4,7] 为例&#…

【计网笔记】以太网

经典以太网 总线拓扑 物理层 Manchester编码 数据链路层 MAC子层 MAC帧 DIX格式与IEEE802.3格式 IEEE802.3格式兼容DIX格式 前导码&#xff08;帧开始定界符SOF&#xff09; 8字节 前7字节均为0xAA第8字节为0xAB前7字节的Manchester编码将产生稳定方波&#xff0c;用于…

steam游戏模拟人生3缺少net framework 3.5安装不成功错误弹窗0x80070422怎么修复

模拟人生3在Steam上运行时提示缺少.NET Framework 3.5并出现错误代码0x80070422&#xff0c;通常意味着.NET Framework 3.5功能没有正确启用&#xff0c;或者安装过程中出现了问题。以下是解决这个问题的步骤&#xff1a; 1.启用Windows功能 按下Win R键&#xff0c;输入opti…

【论文学习与撰写】论文里的Mathtype公式复制粘贴,跨文档复制后错码/错位问题的解决

1、描述 问题&#xff1a;论文的草稿已经写好&#xff0c;里面的公式之类的都已经一个个打上去了 但是把草稿里的正文和公式粘贴在另一个文档里的时候&#xff0c;会出些公式格式错误的情况 那该怎么操作保证复制后的公式保持原格式呢 选中复制的内容&#xff0c;在另一个文…

MySQL【知识改变命运】10

联合查询 0.前言1.联合查询在MySQL里面的原理2.练习一个完整的联合查询2.1.构造练习案例数据2.2 案例&#xff1a;⼀个完整的联合查询的过程2.2.1. 确定参与查询的表&#xff0c;学⽣表和班级表2.2.2. 确定连接条件&#xff0c;student表中的class_id与class表中id列的值相等2.…

【c++篇】:解析c++类--优化编程的关键所在(一)

文章目录 前言一.面向过程和面向对象二.c中的类1.类的引入2.类的定义3.类的封装和访问限定符4.类的作用域5.类的实例化6.类对象模型 三.this指针1.this指针的引出2.this指针的特性3.C语言和c实现栈Stack的对比 前言 在程序设计的广袤宇宙中&#xff0c;C以其强大的功能和灵活性…

[k8s理论知识]6.k8s调度器

k8s默认调度器 k8s调度器的主要职责&#xff0c;就是为一个新创建出来的pod寻找一个适合的节点Node。这包括两个步骤&#xff0c;第一&#xff0c;从所有集群的节点中&#xff0c;根据调度算法挑选出所有可以运行该pod的节点&#xff0c;第二&#xff0c;从第一步的结果中&…

Java项目-基于springboot框架的企业客户信息反馈系统项目实战(附源码+文档)

作者&#xff1a;计算机学长阿伟 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、ElementUI等&#xff0c;“文末源码”。 开发运行环境 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/…

Windows环境下Qt Creator调试模式下qDebug输出中文乱码问题

尝试修改系统的区域设置的方法&#xff1a; 可以修复问题。但会出现其它问题&#xff1a; 比如某些软件打不开&#xff0c;或者一些软件界面的中文显示乱码&#xff01; 暂时没有找到其它更好的办法。

10-Docker安装Redis

10-Docker安装Redis Docker安装Redis 以 Redis 6.0.8 为例&#xff1a; docker pull redis:6.0.8直接pull会出现以下错误 [rootdocker ~]# docker pull redis:6.0.8 Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request can…

[Python学习日记-50] Python 中的序列化模块 —— pickle 和 json

[Python学习日记-50] Python 中的序列化模块 —— pickle 和 json 简介 pickle 模块 json 模块 pickle VS json 简介 什么叫序列化&#xff1f; 序列化指的是将对象转换为可以在网络上传输或者存储到文件系统中的字节流的过程。序列化使得对象可以被保存、传输和恢复&#…