一文理解多线程机制和多线程的优缺点

news2024/9/21 11:00:03

一文理解多线程机制

  • 前言:多线程的优缺点。
  • 一、什么是多线程
    • 1.1、多线程的概念和基本原理
    • 1.2、多线程与单线程的区别
  • 二、多线程的应用场景
  • 三、C++ 中的多线程
    • 3.1、C++11 新增加的 thread 库
    • 3.2、C++ 线程同步机制(mutex、condition_variable)
  • 四.、多线程编码中需要注意的问题
    • 4.1、资源竞争导致的死锁问题及解决方法
    • 4.2、数据共享和内存模型问题及解决方法
  • 五、总结

前言:多线程的优缺点。

多线程的优点:

  • 通过同时执行多个任务,可以利用CPU资源,提高程序运行效率。
  • 多线程可以使程序在执行繁琐操作时不会卡死或无响应,提高用户交互体验。
  • 某些功能需要同时进行多项操作才能完成,使用多线程可以更方便地实现这些复杂的功能。
  • 将任务拆分成多个子任务并分配给不同的线程后,代码结构会更清晰,并且可以降低代码的耦合度,提高可读性和可维护性。
  • 现代计算机都是多核CPU,在使用单线程的情况下无法发挥其全部性能。而使用多线程可以充分利用CPU的所有核心,从而提高计算机的处理速度。

多线程的缺点:

  • 因为涉及到并发编程、数据同步等问题,需要对程序逻辑有较深入了解,并且需要一定的经验才能避免出现竞态条件、死锁等问题;程序设计难度加大。
  • 每个线程都需要占用一定内存空间和系统资源(如CPU时间片),同时线程之间的切换也会带来一定开销,因此过多的线程会增加系统资源消耗。
  • 在多线程编程中,如果不做好数据同步和互斥访问等工作,就容易引发数据竞争问题,导致程序崩溃或结果出错。
  • 多线程编程需要注意线程安全、锁死等问题,如果没有考虑周全可能会导致程序崩溃或运行不稳定。

一、什么是多线程

多线程是指在一个程序中同时执行多个独立的任务或操作。每个任务或操作都是由一个单独的线程来执行,而这些线程共享程序的资源和内存空间。与单线程相比,多线程可以提高程序的运行效率和响应速度,因为它可以充分利用 CPU 的多核处理能力,同时也可以避免某些操作阻塞其他操作的问题。
在这里插入图片描述

1.1、多线程的概念和基本原理

多线程是一种并发编程的技术,它允许程序在同一个进程中同时执行多个独立的任务或操作。每个任务都由一个单独的线程来执行,而这些线程共享程序的资源和内存空间。
在这里插入图片描述

多线程的基本原理是通过将程序分成多个子任务,并创建对应数量的线程来同时执行这些子任务。每个线程都有自己的堆栈、寄存器和指令计数器等状态信息,可以独立地运行代码。不同线程之间可以进行通信和协调,通过锁、信号量、条件变量等机制来实现数据同步和互斥访问。

多线程在操作系统级别实现,通过操作系统提供的API(如POSIX标准中提供的pthread库)进行创建、管理和控制。在高级编程语言中也提供了相应的库或框架来支持多线程编程,如Java中的Thread类、C#中的Task类等。

1.2、多线程与单线程的区别

  1. 执行方式不同:单线程只能执行一个任务,而多线程可以同时执行多个任务。
  2. 程序性能不同:多线程可以充分利用CPU资源,提高程序运行效率,而单线程则无法充分利用CPU资源,导致程序运行速度变慢。
  3. 内存占用不同:多线程需要占用更多的内存空间和系统资源(如CPU时间片),因此对于内存有限或资源受限的应用场景,单线程更为适合。
  4. 编写难度不同:在编写过程中,多线程需要考虑到并发、数据安全等问题,需要对程序设计有一定了解和经验。而单线程相对来说比较简单易于编写。
  5. 错误处理方式不同:在单线程中如果出现异常错误会直接导致程序崩溃,在多线程中则需要使用特殊手段处理错误以保证程序稳定性。
    在这里插入图片描述

二、多线程的应用场景

  1. CPU密集型任务:需要进行大量计算或处理数据,占用大量CPU资源,例如图像、视频处理等。这种任务适合使用多线程技术,因为可以充分利用CPU资源并提高程序运行效率。
  2. I/O密集型任务:需要进行大量的输入输出操作,例如读取文件、网络通信等。这种任务相比CPU密集型任务更适合使用单线程或少量线程,因为在进行I/O操作时会阻塞CPU,此时如果开启过多线程反而会增加上下文切换的负担,导致程序运行效率变慢。
  3. 在GUI程序中,多线程的应用主要是为了提高用户体验和避免程序卡顿的问题。后台任务:当用户进行某些操作时,例如打开文件、导入数据等,这些操作可能需要耗费一定时间。如果在主线程中执行这些操作,则会导致GUI界面卡顿或无响应。因此可以使用一个后台线程来执行这些任务,使得主界面能够保持流畅。异步更新UI:当某个操作需要对UI进行更新时,例如下载进度条、播放音乐等,在主线程中更新UI可能会造成界面卡顿。因此可以使用一个单独的线程来进行UI更新,并通过回调机制将结果返回到主线程以更新UI。
  4. 多媒体处理:在图像编辑、视频剪辑等软件中,处理大量数据需要大量计算资源。使用多个线程分别处理不同部分的数据可以提高效率并且减少卡顿现象。
  5. 高并发服务器程序中多线程的应用。多线程的应用主要是为了提高服务器的并发处理能力和吞吐量。

在GUI程序中多线程的应用可以提高程序的效率和用户体验。如果是CPU密集型任务,则可以充分利用多核CPU的优势;如果是I/O密集型任务,则可以通过异步IO等方式来减少阻塞时间,并且避免过度使用多线程造成系统负荷过重。

三、C++ 中的多线程

3.1、C++11 新增加的 thread 库

C++11新增加的thread库提供了一种方便的多线程编程方式,相比于pthread和Windows API,其使用更加简单易懂。

  1. 线程的创建和销毁:可以通过std::thread类来创建一个新线程,并在析构函数中自动销毁线程。
  2. 同步机制:提供了互斥量(mutex)、条件变量(condition_variable)等同步机制来保证线程之间的同步和协作。
  3. 线程本地存储:可以通过thread_local关键字定义线程局部存储变量,使得每个线程都有自己独立的变量副本。
  4. 原子操作:提供了atomic模板类来支持原子操作,避免了多线程并发访问共享数据时可能出现的竞争条件问题。
  5. 可执行对象:除了函数指针外,还可以将可调用对象(如lambda表达式、成员函数等)作为参数传递给std::thread构造函数。
  6. 可移植性:由于是标准C++库,因此具有跨平台性,在不同平台上都能够使用相同的接口进行多线程编程。

C++11中引入了thread库,用于支持多线程编程。在使用该库时,需要包含头文件,并使用std::thread类来创建和管理线程。

示例:

#include <iostream>
#include <thread>

void print_num(int num)
{
    std::cout << "num: " << num << std::endl;
}

int main()
{
    // 创建一个新线程
    std::thread t(print_num, 42);

    // 主线程继续执行
    std::cout << "main thread" << std::endl;

    // 等待子线程完成
    t.join();

    return 0;
}

示例中,首先定义了一个print_num函数,用于在线程中打印数字。然后在主函数中创建了一个新线程t,并传入print_num函数和参数42。主线程继续执行,在输出“main thread”后等待子线程完成并调用join()函数。

需要注意的是,在join()函数之前必须保证子线程已经完成,否则会导致主线程阻塞。另外,还可以使用detach()函数将子线程与主线程分离,使其成为独立运行的后台进程。

除了基本的创建和管理线程外,C++11的thread库还提供了一些其他功能,如互斥量、条件变量、原子操作等。

3.2、C++ 线程同步机制(mutex、condition_variable)

在多线程编程中,线程之间的同步和协作是非常重要的。C++提供了两种主要的线程同步机制:互斥量(mutex)和条件变量(condition_variable)。

(1)互斥量(mutex)。互斥量用于保护共享数据,避免多个线程同时对其进行访问而产生竞争条件问题。当一个线程获得了互斥量的锁时,其他线程就无法再次获得该锁,只有等到该锁被释放后才能继续执行。

在C++中,可以使用std::mutex类来创建和管理互斥量。使用方式如下:

#include <mutex>

std::mutex mtx;  // 创建一个互斥量

void func()
{
    std::lock_guard<std::mutex> lock(mtx);  // 加锁
    // 访问共享数据
}   // 解锁

其中,std::lock_guard是一个RAII封装类,用于自动加锁和解锁。需要注意的是,在访问共享数据时必须先加锁再操作,并在操作完成后及时解锁。

(2)条件变量(condition_variable)。条件变量用于在不同的线程之间传递信号或消息,以便它们能够相互通信、协调工作。通过条件变量可以实现一些高级同步机制,如生产者-消费者模型、读写锁等。

在C++中,可以使用std::condition_variable类来创建和管理条件变量。使用方式如下:

#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;

void func()
{
    std::unique_lock<std::mutex> lock(mtx);  // 加锁
    // 等待条件满足
    cv.wait(lock, [](){ return condition; });
    // 条件满足后继续执行
}   // 解锁

void notify()
{
    cv.notify_one();  // 通知一个线程
    cv.notify_all();  // 通知所有线程
}

其中,std::unique_lock也是一个RAII封装类,用于自动加锁和解锁。在等待条件时需要使用wait()函数,并传入互斥量的引用和一个可调用对象(lambda表达式或函数对象),该对象返回true表示条件已经满足,否则会一直阻塞等待。当条件满足后,wait()函数会自动解锁互斥量并返回。

在通知其他线程时,可以使用notify_one()通知任意一个线程或notify_all()通知所有线程。在发出通知之前必须先获得互斥量的锁,并且只有收到信号的线程才能继续执行。

四.、多线程编码中需要注意的问题

4.1、资源竞争导致的死锁问题及解决方法

如果两个或多个线程同时访问共享资源,可能会导致资源竞争问题。如果不加以处理,这些竞争条件可能会导致死锁问题。

死锁是指两个或多个进程或线程互相等待对方释放资源的一种情况。当一个进程被阻塞并等待另一个进程释放其占用的资源时,如果该进程同时也占用了另外一个进程需要的资源,则会形成循环依赖,导致所有相关进程都处于阻塞状态。

避免死锁的方法:

  • 加锁顺序:如果多个线程需要访问多个共享资源,则应该按照固定的顺序加锁。这样可以确保每个线程始终按照相同的顺序访问共享资源,从而避免出现循环依赖。
  • 避免嵌套锁:不要在已经获得锁的区域内再次获取其他锁。这样容易形成嵌套锁,增加死锁的风险。
  • 使用原子操作:使用原子操作可以确保对共享数据进行原子性修改,并且不需要使用显式的锁来保护共享数据。
  • 消除冗余锁:尽量减少使用不必要的锁。如果某个资源只被单个线程访问,那么就不需要对其加锁。
  • 使用条件变量:条件变量可以用来在多线程之间进行同步和通信。它可以让线程在等待某个事件发生时进入休眠状态,避免占用CPU资源。

假设有两个线程 A 和 B,它们都需要访问共享资源 X 和 Y。如果线程 A 先锁定了资源 X,然后尝试获取资源 Y,同时线程 B 先锁定了资源 Y,然后尝试获取资源 X,就会导致死锁问题。

示例代码如下:


#include <pthread.h>

pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_y = PTHREAD_MUTEX_INITIALIZER;

void* thread_a(void* arg) {
    pthread_mutex_lock(&mutex_x);
    // do something with resource X
    pthread_mutex_lock(&mutex_y);
    // do something with resource Y
    pthread_mutex_unlock(&mutex_y);
    pthread_mutex_unlock(&mutex_x);
}

void* thread_b(void* arg) {
    pthread_mutex_lock(&mutex_y);
    // do something with resource Y
    pthread_mutex_lock(&mutex_x);
    // do something with resource X
    pthread_mutex_unlock(&mutex_x);
    pthread_mutex_unlock(&mutex_y);
}

int main() {
    pthread_t tid_a, tid_b;
    
    pthread_create(&tid_a, NULL, thread_a, NULL);
    pthread_create(&tid_b, NULL, thread_b, NULL);

    // wait for threads to finish
    pthread_join(tid_a, NULL); 
    pthread_join(tid_b, NULL);

   return 0;
}

线程 A 先锁定了资源 X,而线程 B 先锁定了资源 Y。由于两个线程都无法释放已经持有的锁,在互相等待对方释放锁的情况下就形成了死锁。

为了避免死锁问题,可以按照一定的顺序来加锁。例如,可以要求所有线程都按照相同的顺序获取锁:

void* thread_a(void* arg) {
    pthread_mutex_lock(&mutex_x);
    // do something with resource X
    pthread_mutex_lock(&mutex_y);
    // do something with resource Y
    pthread_mutex_unlock(&mutex_y);
    pthread_mutex_unlock(&mutex_x);
}

void* thread_b(void* arg) {
    pthread_mutex_lock(&mutex_x);  // 注意这里先获取了资源 X 的锁
    pthread_mutex_lock(&mutex_y);
    // do something with resource Y
    // do something with resource X
    pthread_mutex_unlock(&mutex_x);
    pthread_mutex_unlock(&mutex_y);
}

线程 A 和线程 B 都按照相同的顺序获取锁,即先获取资源 X 的锁再获取资源 Y 的锁。这样就能够避免死锁问题。

4.2、数据共享和内存模型问题及解决方法

数据共享和内存模型问题通常指的是多线程编程中由于不同线程对共享数据的访问顺序或方式不同,导致程序出现意料之外的行为。解决这些问题需要了解多线程编程中的内存模型以及使用正确的同步机制。

(1)内存模型。内存模型描述了程序如何在计算机内存中分配、访问和更新变量。在多线程编程中,要考虑到不同线程之间的竞争条件。就是当一个线程写入某个变量时,另一个线程可能正在读取该变量或者正在修改该变量。

Java、C++11 和 C11 都定义了一套严格的内存模型规范,确保了在多个线程同时操作共享数据时能够正确地执行。例如,在 Java 中,每个 volatile 变量都有一个内存屏障(memory barrier),能够保证任何对该变量的写操作都会立即刷新到主内存,并使其他所有线程看到最新值。而在 C++11 中,则引入了原子类型(atomic type)和 memory_order 等关键字来控制并发访问。

(2)数据共享问题。在多线程编码中,要避免以下几种常见的数据共享问题:

  1. 竞态条件(Race Condition):指两个或多个进程或线程同时访问同一块数据,而且至少有一个进程或线程修改了该数据。竞态条件可能导致不可预期的结果。解决方法:使用互斥锁(mutex)、信号量(semaphore)等同步机制,确保对共享变量的访问是互斥的。
  2. 死锁(Deadlock):指两个或多个线程在等待其他线程释放资源时陷入无限等待的状态,导致程序无法继续执行。解决方法:避免循环依赖、按照相同顺序获取锁等方式来避免死锁。
  3. 饥饿(Starvation):指某些线程永远无法获得所需的资源,因为总是被其他线程占用着。解决方法:使用公平性策略,例如优先级队列、时间片轮转等方式来确保每个线程都能够得到合理的时间片和资源分配。

五、总结

多线程技术对于软件开发带来了以下几个变革:

  • 更高的并发性。多线程技术使得程序可以同时执行多个任务,从而提高了程序的并发性。在单核处理器时代,通过利用多线程技术可以实现更好的任务分配和资源利用。而在今天,随着多核处理器的普及,利用多线程技术也可以充分发挥硬件资源,提升程序的执行效率。
  • 更好的用户体验。通过使用多线程技术,我们可以将一些耗时操作(如网络请求、IO 操作等)放到后台线程中执行,并将结果返回给主线程进行 UI 更新。这样可以避免阻塞主线程导致界面卡顿,从而提供更好的用户体验。
  • 更容易编写复杂应用程序。通过使用多线程技术,我们可以将一个大型应用程序拆分成不同的模块或组件,并让每个模块或组件在独立的线程中运行。这样可以简化代码逻辑、降低系统耦合度,并且方便后期维护和扩展。
  • 更高的可伸缩性和可靠性。当系统面临大量请求时,通过使用多线程技术能够更好地满足需求并提供更高的可伸缩性。同时,多线程技术也可以帮助我们设计出更加健壮的系统,并提高系统的可靠性。
    在这里插入图片描述

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

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

相关文章

【Openvino01】Ubuntu安装inter的openvino2022.1以及遇到的各种错误解决

交代一下今天的文章背景&#xff1a; 于最近要使用inter的一款名为Intel Movidius™ Myriad™ X 的加速卡去实现对算法模型的加速推理能力&#xff0c;由于是就得第一步安装openvino&#xff0c;然后再使用卡去验证openvino是否安装ok&#xff0c;卡是否真的存在推理加速的能力…

python pytorch教程-带你从入门到实战(代码全部可运行)

python pytorch教程-带你从入门到实战&#xff08;代码全部可运行&#xff09; 其实这个教程以前博主写过一次&#xff0c;不过&#xff0c;这回再写一次&#xff0c;打算内容写的多一点&#xff0c;由浅入深&#xff0c;然后加入一些实践案例。 下面是我们的内容目录&#x…

2022(一等奖)D1073基于Himawari-8卫星遥感的黑龙江省地表水时空格局研究

作品介绍 1 项目简介 为探究黑龙江省地表水空间格局变化&#xff0c;本项目以黑龙江省为例&#xff0c;基于高时相Himawari-8号卫星数据&#xff0c;通过影像预处理、特征指数选择、自动阈值分类、集成学习和随机森林分类等步骤&#xff0c;融合IDL二次开发与GIS空间分析&…

chatgpt赋能python:Python求绝对值的三种方法

Python 求绝对值的三种方法 Python是一门面向对象、解释型、动态类型的高级编程语言&#xff0c;它被广泛应用于各种领域&#xff0c;特别是科学计算、数据分析、机器学习等领域。在Python中&#xff0c;求绝对值是一个常见的数学操作。本文将介绍Python求绝对值的三种方法&am…

Redis原理 - Redis网络模型

原文首更地址&#xff0c;阅读效果更佳&#xff01; Redis原理 - Redis网络模型 | CoderMast编程桅杆https://www.codermast.com/database/redis/redis-netword-model.html 思考 Redis 到底是单线程还是多线程&#xff1f; 如果仅仅针对 Redis 的核心业务部分&#xff08;命…

Vicuna-13B云服务器部署

Vicuna概述 Vicuna由一群主要来自加州大学伯克利分校的研究人员推出&#xff0c;仍然是熟悉的配方、熟悉的味道。Vicuna同样是基于Meta开源的LLaMA大模型微调而来&#xff0c;它的训练数据是来自ShareGPT上的7万多条数据&#xff08;ShareGPT一个分享ChatGPT对话的谷歌插件&am…

Geocomputation (3)Spatial data operations

Geocomputation &#xff08;3&#xff09;Spatial data operations 来源&#xff1a;https://github.com/geocompx/geocompy 1.准备 #| echo: false import pandas as pd import matplotlib.pyplot as plt pd.set_option("display.max_rows", 4) pd.set_option(&…

文件搜索引擎的搭建Elasticsearch+Fscrawler+SearchUI+Git+Nginx

文章目录 前言如何搭建文档搜索引擎服务器架构环境准备一、搭建Elasticsearch二、搭建Fscrawler三、搭建SearchUI服务四、定时拉取Git文件五、搭建Nginx文件下载服务器 前言 搭建一套文档搜索引擎。有时候&#xff0c;我们有一批文档&#xff0c;需要在这批文档中查找想要的内…

【从零开始学习JAVA | 第十篇】StringBuild介绍

前言&#xff1a; 本文会对StringBuild类进行详细的介绍&#xff0c;他相比较于String&#xff0c;在进行字符串拼接的时候大大提高了效率&#xff0c;是一个 很实用的工具。 StringBulider&#xff1a; StringBuilder是Java中的一个类&#xff0c;用于在一次创建和初始化后&a…

Java中反射机制,枚举,Lambda的使用

目录 一、反射机制 1、含义 2、作用 3、※反射相关的几个类 3.1、Class类&#xff08;Class对象是反射的基石&#xff09; 3.2、Class类中相关的方法 3.2.1 (※重要)常用获得类相关的方法 3.2.2 (※重要)常用获得类中属性、变量Field相关的方法 3.2.3 获得类中注解相…

详细设计报告

聊天系统设计与实现详细设计报告 1.编写目的 详细设计的主要任务是概要设计方案做完善和细化,本阶段主要对聊天应用系统进行过程化的描述,详细确定每一个功能模块的实现方式、执行流程,为程序员编码提供依据。设计用户界面。 2.总体方案确认 (1)系统总体结构确认 该项目管理…

Redis原理 - 通信协议RESP

原文首更地址&#xff0c;阅读效果更佳&#xff01; Redis原理 - 通信协议RESP | CoderMast编程桅杆https://www.codermast.com/database/redis/redis-communication-protocol.html RESP协议 Redis 是一个 CS 架构的软件&#xff0c;通信一般分两步&#xff08;不包括pipeli…

计算机组成与设计Patterson Hennessy 笔记_1 计算机概要与技术

Patterson & Hennessy 计算机概要与技术 计算机应用包括&#xff1a;个人计算机PC&#xff0c;服务器&#xff0c;嵌入式计算机。后PC时代出现了个人移动设备PMD&#xff08;手机&#xff09;&#xff0c;云计算&#xff08;在网络上提供服务的大服务器集群&#xff0c;供…

Unity Mac最新打苹果包流程

作者介绍&#xff1a;铸梦xy。IT公司技术合伙人&#xff0c;IT高级讲师&#xff0c;资深Unity架构师&#xff0c;铸梦之路系列课程创始人。 IOS详细打包流程1.申请APPID2.申请开发证书3.创建描述文件 IOS详细打包流程 1.申请AppID 2.创建证书 3.申请配置文件&#xff08;又名描…

NodeJS KOA⑩②

文章目录 ✨文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹支持&#x1f618;前言KOA Koa vs Express Koa更轻量 Koa~Context对象 Koa~异步流程控制 Koa~中间件模型Koa路由 1.1基本使用 2.2请求方式2.2.1规范写法2…

数据结构——带头节点的双向循环列表

带头节点的双向循环链表是一种特殊的双向链表&#xff0c;它与普通的双向链表相比&#xff0c;最大的区别是链表头结点的 next 指针不再指向第一个实际节点&#xff0c;而是指向链表中的第一个节点。同时&#xff0c;链表尾结点的 prev 指针也不再指向 NULL&#xff0c;而是指向…

轻松配置深度学习模型 ?

动动发财的小手&#xff0c;点个赞吧&#xff01; 由于所有模块都需要大量参数和设置&#xff0c;因此管理深度学习模型可能很困难。训练模块可能需要诸如 batch_size 或 num_epochs 之类的参数或学习率调度程序的参数。同样&#xff0c;数据预处理模块可能需要 train_test_spl…

Java接口幂等性,如何重试?

Java接口幂等性&#xff0c;如何重试&#xff1f; 文章目录 Java接口幂等性&#xff0c;如何重试&#xff1f;前言一、幂等性是什么&#xff1f;二、为什么要幂等性&#xff1f;三、使用什么办法实现幂等性&#xff1f;1.insert前先select2.加悲观锁3.加乐观锁4.加唯一索引5.Re…

uniapp系列-uni.getAppBaseInfo() versionCode appVersion 值不对应该怎么解决?

今天看到一个BUG 问题描述 我们使用uniapp的官方文档中uni.getAppBaseInfo()后获取的 appVersionCode appVersion &#xff0c;发现获得的结果和我们实际设置的不一致&#xff0c;不是manifest.json里面的值&#xff0c;如下图所示官方文档&#xff1a;https://uniapp.dcloud…

还原大师(MD5)

根据题目提示&#xff0c;都猜得到这应该跟MD5的加密形式有关系 我好像还没有具体了解过MD5编码的格式&#xff0c;或许本题可以通过MD5的编码格式推导出字符串 但是说实话&#xff0c;MD5的加密方式没有找到详细简介的文章 然后我就去网上百度了一下&#xff0c;经过大佬wp的洗…