相对于多进程,你真的知道为什么要使用多线程吗(C/C++多线程编程)

news2024/12/24 21:55:22

目录

前言

线程VS进程

POSIX线程库的使用

线程创建

线程等待

线程分离

线程状态

可结合态线程实例

 分离态线程实例

线程退出

线程的同步与互斥

同步互斥的概念

互斥锁(互斥)

互斥锁的使用步骤

总结说明

信号量

信号量的使用步骤

条件变量(协同)

条件变量的使用步骤

总结说明

多线程的问题

解决方案(引入线程池概念)


前言

1、线程与进程的区别要能在面试时侃侃而谈至少五分钟。

二者的区别详细可以参考以下博客,这是我觉得写得非常好的一篇博客:

线程与进程,你真得理解了吗

2、本文除了介绍线程与进程的区别,还包括了C语言中POSIX线程库的使用以及对线程的同步与互斥的复习,最后还引入了线程池的概念,之后将从C语言的多线程过度到C++线程池的实现。

线程VS进程

进程是资源分配的最小单位,线程是任务调度的最小单位。

进程和线程区别的本质

每个进程拥有独立的地址空间,多个线程共享同一块地址空间

区别1:进程的并发方式是比较消耗资源的,因为是独立的地址空间;而线程是共享空间,所以并发的开销比较小。

区别2:通信机制的区别,因为进程使用的是独立地址空间,所以需要提供专门的通信方式。所以我们在多进程编程时,考虑更多的是进程间的通信。

线程是共享地址空间,它们的通信使用的是全局变量(也就是所谓的数据段)。优点是通信方式简单,缺点是上锁解锁、获取释放信号量要保证线程的同步,也就是说虽然通信方式简单,但是安全性低,需要保证线程的同步

区别3:对于进程和线程来讲,进程更加安全,因为进程使用独立的地址空间,也就是一个进程的消亡不会影响到另一个进程。而线程是共享地址空间的,因此一个线程消亡了,可能会影响到其它的线程工作。


因此在实际的开发应用中,对核心的业务开发更倾向于使用进程(安全性、稳定性)。而线程的开销小,因此开销多在交互式、有响应优先级以及需要资源共享的程序中使用。

从线程和进程的运行效率上来区分

如果有一万个进程同时运行和一万个线程同时运行,假设资源都足够的情况下,进程运行比线程更快。

线程操作的API并不是操作系统提供的,进程相关的API属于系统调用,而线程相关的API属于POSIX线程库(使用POSIX线程库的好处是支持跨平台)

POSIX线程库的使用

线程创建

参数1:线程id

参数2:线程属性

参数3:线程处理函数

参数4:线程处理函数参数

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

线程等待

int pthread_join(pthread_t thread, void **retval);

该函数用于回收线程资源,如果该线程没有结束,就一直阻塞直到线程结束,相对于进程中的wait方法

为什么要实现线程等待,因为我们要回收创建的线程资源

线程分离

int pthread_detach(pthread_t thread);

功能:如果次线程的资源不希望别人调用pthread_join函数来回收,而是希望自己在结束时,自动回收资源的话,可以调用该函数,且不会导致程序阻塞(分离次线程,让次线程在结束时自动回收资源)

返回值:成功返回0,失败返回错误号

线程状态

  • 可结合态joinable
    • 这种状态下的线程是能够被其它进程回收其资源或杀死的(默认创建的线程都是可结合态,可被pthread_join回收)
  • 分离态detached
    • 这种状态下的线程是不能够被其他线程回收或杀死的;它的存储资源在它终止时由系统自动释放。这种线程也叫做异步线程。不会导致主线程阻塞,因此我们写代码一般使用分离态。

面试题:如何避免多线程退出导致的内存泄露?

1.每个可结合线程需要显示地调用pthread_join回收

2.将其变成分离态地线程。线程分离函数——pthread_detach

可结合态线程实例

#include <stdio.h>
#include <pthread.h>

void *mythread1(void *arg)
{
    for(int i=0;i<3;i++)
    {
        printf("hello world\n");
        sleep(1);
    }
}

int main(int argc, char const *argv[])
{
    pthread_t id;
    pthread_create(&id,NULL,mythread1,NULL);

    pthread_join(id,NULL);//wait
    return 0;
}

如果不回收线程,就会导致线程结束成为僵尸线程。在实际开发中,比如服务器开发,如果大量的线程没有被回收,就会导致占用大量的内存资源,导致系统运行效率下降。为了避免忘记手动回收资源,我们可以将线程设置为分离态,让系统自动回收。

#include <stdio.h>
#include <pthread.h>

void *mythread1(void *arg)
{
    for(int i=0;i<3;i++)
    {
        printf("hello world\n");
        sleep(1);
    }
}

int main(int argc, char const *argv[])
{
    pthread_t id;
    pthread_create(&id,NULL,mythread1,NULL);

    //pthread_join(id,NULL);//wait
    while(1)
    {
        
    }
    return 0;
}

pthread_join导致主线程阻塞

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void *mythread1(void *arg)
{
    for(int i=0;i<3;i++)
    {
        printf("hello world\n");
        sleep(1);
    }
}

int main(int argc, char const *argv[])
{
    pthread_t id;
    pthread_create(&id,NULL,mythread1,NULL);

    pthread_join(id,NULL);//wait
    //pthread_detach(id);

    while(1)
    {
        printf("main!\n");
        sleep(2);
    }
    return 0;
}

 分离态线程实例

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void *mythread1(void *arg)
{
    for(int i=0;i<3;i++)
    {
        printf("hello world\n");
        sleep(1);
    }
}

int main(int argc, char const *argv[])
{
    pthread_t id;
    pthread_create(&id,NULL,mythread1,NULL);

    //pthread_join(id,NULL);//wait
    pthread_detach(id);

    while(1)
    {
        printf("main!\n");
        sleep(2);
    }
    return 0;
}

线程退出

被动退出

int pthread_cancel(pthread_t thread);

主动退出

  • void pthread_exit(void *retval)
  • return返回

例:线程运行1s后退出

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void *mythread1(void *arg)
{
    for(int i=0;i<3;i++)
    {
        printf("hello world\n");
        sleep(1);
    }
}

int main(int argc, char const *argv[])
{
    pthread_t id;
    pthread_create(&id,NULL,mythread1,NULL);

    //pthread_join(id,NULL);//wait
    pthread_detach(id);

    while(1)
    {
        printf("main!\n");
        sleep(1);
        pthread_cancel(id);
    }
    return 0;
}

 注册线程退出处理函数,这两个函数需要成对出现使用

void pthread_cleanup_push(void(*routine)(void *),void *arg);
void pthread_cleanup_pop(int execute);

线程的同步与互斥

因为线程是共享数据段的,所以对数据段的保护一定要做到。不做线程同步的话使,就会导致共享资源和临界资源出现问题。

使用互斥锁、线程信号量或条件变量可以实现线程同步。

同步互斥的概念

互斥:同一时间,只能一个任务(进程或线程)执行,谁先运行不确定。

同步:同一时间,只能一个任务(进程或线程)执行,有顺序的运行。

同步 是特殊的 互斥。

互斥锁(互斥)

用于线程的互斥。

互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即加锁(

lock )和解锁( unlock )

互斥锁的操作流程如下:

1)在访问共享资源临界区域前,对互斥锁进行加锁

2)在访问完成后释放互斥锁上的锁。 (解锁)

3)对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁 被释放。

互斥锁的使用步骤
  • 定义一个互斥锁(变量)

pthread_mutex_t mutex;

  • 初始化互斥锁

功能:初始化定义的互斥锁

什么时初始化,解锁设置互斥锁所需要的值

返回值:总是返回0,所以这个函数不需要进行任何出错处理

参数

- mutex:互斥锁,需要我们自己定义

比如:pthread_mutex_t mutex;

pthread_mutex_t是一个结构体类型,所以mutex实际上是一个结构体变量

- attr:互斥锁的属性

设置NULL表示使用默认属性,除非我们想要实现一些互斥锁的特殊功能,否则默认属性

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                        const pthread_mutexattr_t *restrict attr);

编译时初始化锁位解锁状态

pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER

  • 加锁解锁
pthread_mutex_lock(&mutex);(阻塞加锁)访问临界区加锁操作
pthread_mutex_trylock(&mutex)(非阻塞加锁);
与lock类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待
pthread_mutex_unlock(&mutex);访问临界区解锁操作

一般都使用pthread_mutex_trylock,使用非阻塞可以提高开发效率

  • 进程退出时销毁互斥锁
#include <pthread.h>
pthread_mutex_destroy(pthread_mutex_t *mutex);

功能:销毁互斥锁

所谓销毁,说白了就是删除互斥锁相关的数据,释放互斥锁数据所占用的各种内存资源

返回值:成功返回0,失败返回非零错误号

总结说明

在使用互斥锁的时候一定要锁信息度比较小的,也就是锁的代码段越少,锁解决问题效果越好。比如说红绿灯都是锁一个路口,没有锁一条路的。

信号量

信号量广泛用于进程线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被 用来控制对公共资源的访问。

当信号量值大于 0 时,则可以访问,否则将阻塞

信号量数据类型为:sem_t

  • 信号量用于互斥

不管多少个任务互斥 只需要一个信号量。先P 任务 再 V

  • 信号量用于同步

有多少个任务 就需要多少个信号量。最先执行的任务对应的信号量为1,其他信号量全 部为0。

每任务先P自己 任务 V下一个任务的信号量。

信号量的使用步骤
  • 定义信号量集合

sem_t *sem;

  • 初始化集合中的每个信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value)

功能:

创建一个信号量并初始化它的值。一个无名信号量在被使用前必 须先初始化。

参数:

sem:信号量的地址

pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。

value:信号量的初始值

返回值: 成功返回0,失败返回-1

  • p、v操作

PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。

        

功能:

创建一个信号量并初始化它的值。一个无名信号量在被使用前必 须先初始化。

参数:

sem:信号量的地址

pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。

value:信号量的初始值

返回值: 成功返回0,失败返回-1

  • p、v操作

PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。

        信号量减一 P操作

int sem_wait(sem_t *sem);

功能: 将信号量减一,如果信号量的值为0 则阻塞,大于0可以减一

参数:信号量的地址

返回值:成功返回0 失败返回-1

        尝试对信号量减一

int sem_trywait(sem_t *sem);

功能: 尝试将信号量减一,如果信号量的值为0 不阻塞,立即返回 ,大于0可以减一。

参数:信号量的地址

返回值:成功返回0 失败返回-1

        信号量加一 V操作

int sem_post(sem_t *sem);

功能:将信号量加一

参数:信号量的地址

返回值:成功返回0 失败返回-1

  • 进程结束时,删除线程信号量集合

销毁信号量

int sem_destroy(sem_t *sem);

功能: 销毁信号量

参数: 信号量的地址

返回值:成功返回0 失败返回-1

条件变量(协同)

协同指的是当条件满足的时候就通知线程去执行,条件不满足就不通知。

条件变量的使用步骤

条件变量的使用步骤

  • 定义一个条件变量(全局变量 )由于条件变量需要互斥锁的配合,所以还需要定义一个线程互斥锁

pthread_cond_t

  • 初始化条件变量
int pthread_cond_init(pthread_cont_t *restrict cond,const pthread_condattr_t *restrict attr);

功能

        初始化条件变量。与互斥锁的初始化类似

        pthread_cond_t cond; //定义条件变量

        pthread_cond_init(&cond,NULL); //第二个参数位NULL,表示不设置条件变量的属性

也可以直接初始化

        pthread_cond_t cond = PTHREAD_COND_INITALIZER; //与互斥锁的初始化的原理是一样的

返回值:成功返回0,失败返回非0错误号

参数:

- cond:条件变量

- attr:条件变量属性

  • 使用条件变量
    • 等待条件变量函数
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

功能:

检测条件变量cond,如果cond没有被设置,表示条件还不满足,别人还没有对cond进行设置,此时pthread_cond_wait会休眠(阻塞),直到别的线程设置cond表示条件准备好后,才会被唤醒。

返回值:成功返回0,失败返回非0错误号

参数:

- cond:条件变量

- mutex:和条件变量配合使用的互斥锁

    • pthread_cond_wait的兄弟函数
int pthread_cond_timewait(pthread_cond_t *restrict cond,\
         pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);

多了第三个参数,用于设置阻塞时间,如果条件不满足时休眠(阻塞),但不会一直休眠,当时间超时后,如果cond还没有被设置,函数不再休眠

    • 设置条件变量的函数
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);

功能:

        当线程将某个数据准备好时,就可以调用该函数去设置cond,表示条件准备好了,pthread_cond_wait检测到cond被设置后就不再休眠(被唤醒),线程继续运行,使用别的线程准备好的数据来做事。

        当调用pthread_cond_wait函数等待条件满足的线程只有一个时,就是用pthread_cond_signal来唤醒,如果说好多个线程都调用pthread_cond_wait在等待时,使用

int pthread_cond_broadcast(pthread_cond_t *cond);

它可以将所有调用pthread_cond_wait而休眠的线程都唤醒

  • 删除条件变量
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
总结说明

调用pthread_cond_signal区设置条件变量,相当于是给pthread_cond_wait发送了一个线程间专用的信号,通知调用pthread_cond_wait的线程,某某条件满足了,不要再睡了,赶紧做事吧。

多线程的问题

我们在实现并发服务器模型的时候,使用多线程解决问题最大的问题就是线程的数量是受限的。因为线程是存在于进程中的,线程占用的是进程的栈空间。

问题概况如下:

  • 进程所支持的线程数量问题(受限)
  • 线程的创建和销毁是有一定开销的(如果频繁地创建线程,会严重占用CPU资源)

解决方案(引入线程池概念)

使用池化技术(线程池)来解决多线程的问题:在线程池中开辟一定量的线程,如果某人需要使用线程,就从池子中来拿,如果池子为空就等待线程空余。

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

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

相关文章

数据包如何游走于 Iptables 规则之间?

在前文《Linux路由三大件》中&#xff0c;我们提到了 iptables 可以修改数据包的特征从而影响其路由。这个功能无论是传统场景下的 防火墙&#xff0c;还是云原生场景下的 服务路由&#xff08;k8s service&#xff09;、网络策略(calico network policy) 等都有依赖。 虽然业…

7.逻辑结构VS物理结构

第四章 文件管理 7.逻辑结构VS物理结构 ​   fopen这个函数做的事情就是打开了“test.txt”这个文件&#xff0c;并且“w”说明是以“写”的方式打开的&#xff0c;以“写”的方式打开才能往这个文件里写入数据&#xff0c;如果文件打开了那么fp这个指针就可以指向和这个文件…

Eclipse如何设置快捷键

在eclopse设置注释行和取消注释行 // 打开eclipse&#xff0c;依次打开&#xff1a;Window -> Preferences -> General -> Key&#xff0c;

数据结构--关键路径

数据结构–关键路径 AOE⽹ 在 带权有向图 \color{red}带权有向图 带权有向图中&#xff0c;以 顶点表示事件 \color{red}顶点表示事件 顶点表示事件&#xff0c;以 有向边表示活动 \color{red}有向边表示活动 有向边表示活动&#xff0c;以 边上的权值表示完成该活动的开销 \…

HCIE--------------------------------------第一节OSPF快速收敛(OSPF与BGP联动)

一、OSPF快速收敛概述 OSPF快速收敛是为了提高路由的收敛速度而做的扩展特性&#xff0c;包括&#xff1a;PRC&#xff08;Partial Route Calculation&#xff0c;部分路由计算&#xff09;和智能定时器。 同时&#xff0c;OSPF支持故障恢复快速收敛&#xff0c;例如通过OSPF …

Linux Server 20.04 Qt5.14.2配置Jetson Orin Nano Developer Kit 交叉编译环境

最近公司给了我一块Jetson Orin Nano的板子&#xff0c;让我搭建交叉编译环境&#xff0c;所以有了下面的文章 一 :Qt5.14.2交叉编译环境安装 1.准备 1.1设备环境 1.1.1 Server: Ubuntu20.04: Qt 源码 5.14.2 Qt 软件 5.14.2 gcc 版本 9.4.0 g 版本 9.4.0 1.1.2 Jetson …

在 React 中获取数据的6种方法

一、前言 数据获取是任何 react 应用程序的核心方面。对于 React 开发人员来说&#xff0c;了解不同的数据获取方法以及哪些用例最适合他们很重要。 但首先&#xff0c;让我们了解 JavaScript Promises。 简而言之&#xff0c;promise 是一个 JavaScript 对象&#xff0c;它将…

openGauss学习笔记-42 openGauss 高级数据管理-触发器

文章目录 openGauss学习笔记-42 openGauss 高级数据管理-触发器42.1 语法格式42.2 参数说明42.3 示例 openGauss学习笔记-42 openGauss 高级数据管理-触发器 触发器会在指定的数据库事件发生时自动执行函数。 42.1 语法格式 创建触发器 CREATE TRIGGER trigger_name { BEFORE…

Java8实战-总结16

Java8实战-总结16 引入流流与集合只能遍历一次外部迭代与内部迭代 引入流 流与集合 只能遍历一次 和迭代器类似&#xff0c;流只能遍历一次。遍历完之后&#xff0c;这个流就已经被消费掉了。可以从原始数据源那里再获得一个新的流来重新遍历一遍&#xff0c;就像迭代器一样…

使用qsqlmysql操作mysql提示Driver not loaded

环境: win10 IDE: qt creator 编译器: mingw32 这里简单的记录下。我遇到的情况是在IDE使用debug和release程序都是运行正常&#xff0c;但是当我编译成发布版本之后。老是提示Driver not load。 这就很奇诡了。 回顾了下编译的时候是需要在使用qt先编译下libqsqlmysql.dll的…

从入门到精通Python隧道代理的使用与优化

哈喽&#xff0c;Python爬虫小伙伴们&#xff01;今天我们来聊聊如何从入门到精通地使用和优化Python隧道代理&#xff0c;让我们的爬虫程序更加稳定、高效&#xff01;今天我们将对使用和优化进行一个简单的梳理&#xff0c;并且会提供相应的代码示例。 1. 什么是隧道代理&…

V2board缓存投毒漏洞复现

1.什么是缓存投毒 缓存投毒&#xff08;Cache poisoning&#xff09;&#xff0c;通常也称为域名系统投毒&#xff08;domain name system poisoning&#xff09;&#xff0c;或DNS缓存投毒&#xff08;DNS cache poisoning&#xff09;。它是利用虚假Internet地址替换掉域名系…

数据结构—排序

8.排序 8.1排序的概念 什么是排序&#xff1f; 排序&#xff1a;将一组杂乱无章的数据按一定规律顺序排列起来。即&#xff0c;将无序序列排成一个有序序列&#xff08;由小到大或由大到小&#xff09;的运算。 如果参加排序的数据结点包含多个数据域&#xff0c;那么排序往…

Arduino 入门学习笔记10 使用I2C的OLED屏幕

Arduino 入门学习笔记10 使用I2C的OLED屏幕 一、准备工具二、JMD0.96C-1介绍1. 显示屏参数2. SSD1306驱动芯片介绍&#xff1a; 三、使用Arduino开发步骤1. 安装库&#xff08;1&#xff09;Adafruit_GFX_Library 库&#xff08;2&#xff09;Adafruit_SSD1306 驱动库&#xff…

HCIP——STP配置案例

STP配置案例 一、简介二、实现说明1、华为实现说明2、其他厂商实现 三、STP原理1、协商原则2、角色和状态3、报文格式4、BPDU报文处理流程4.1 BPDU报文的分类4.2 BPDU报文的处理流程4.3 BPDU报文格式 四、使用注意事项五、配置举例1、组网需求2、配置思路3、操作步骤4、配置文件…

多维时序 | MATLAB实现WOA-CNN鲸鱼算法优化卷积神经网络的数据多变量时间序列预测

多维时序 | MATLAB实现WOA-CNN鲸鱼算法优化卷积神经网络的数据多变量时间序列预测 目录 多维时序 | MATLAB实现WOA-CNN鲸鱼算法优化卷积神经网络的数据多变量时间序列预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 多维时序 | MATLAB实现WOA-CNN鲸鱼算法优化卷积神经…

大模型技术实践(一)|ChatGLM2-6B基于UCloud UK8S的创新应用

近半年来&#xff0c;通过对多款主流大语言模型进行了调研&#xff0c;我们针对其训练方法和模型特点进行逐一分析&#xff0c;方便大家更加深入了解和使用大模型。本文将重点分享ChatGLM2-6B基于UCloud云平台的UK8S实践应用。 01各模型结构及特点 自从2017年6月谷歌推出Transf…

【OpenCV学习笔记】我的OpenCV学习之路

刚开始接触OpenCV是因为需要进行图像的处理&#xff0c;由于之前没有接触过&#xff0c;所以只能自己进行学习&#xff0c;下面将学习的过程做简单记录分享。 OpenCV专栏链接 OpenCV学习笔记 一、引言 OpenCV&#xff08;Open Source Computer Vision Library&#xff09;是…

【C# 基础精讲】文件读取和写入

文件读取和写入是计算机程序中常见的操作&#xff0c;用于从文件中读取数据或将数据写入文件。在C#中&#xff0c;使用System.IO命名空间中的类来进行文件读写操作。本文将详细介绍如何在C#中进行文件读取和写入&#xff0c;包括读取文本文件、写入文本文件、读取二进制文件和写…

MyBatis动态SQL:打造灵活可变的数据库操作

目录 if标签trim标签where标签set标签foreach标签 动态SQL就是根据不同的条件或需求动态地生成查询语句&#xff0c;比如动态搜索条件、动态表或列名、动态排序等。 if标签 在我们填写一些信息时&#xff0c;有些信息是必填字段&#xff0c;有的则是非必填的&#xff0c;这些…