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

目录

前言

线程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资源)

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

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

竹烟淮雨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值