Linux原生线程 与 互斥锁

pthread原生线程库 pthread原生线程库是Linux系统中用于支持多线程编程的标准库。它基于POSIX标准,提供了创建、管理和控制线程的一整套API。:pthread库是跨平台的,被广泛应用于Unix和类Unix系统中,包括Linux和macOS。:使用pthread库创建线程需要包含pthread.h头文件,并通过函数来创建新的线程。该函数接受一个线程ID指针、线程函数指针、传递给线程函数的参数以及线程属性结构体指针。:pthread库允许设置线程属性,如栈大小、调度策略等。例如,函数可以设置每个进程的栈的最低地址。 阅读详情

title: Linux原生线程 与 互斥锁
date: 2024-08-14 14:55:39
tags:

原文及更多原创文章见我的网站: www.supdriver.top

什么是线程

在一个程序(进程)里的一条执行流就叫做线程(thread),也就是说有多线程功能的进程内,可以有多个线程同时执行

所以我们可以认为:

  • 一个进程至少有一个执行进程
  • 线程在进程内部运行,本质是在进程提供的地址空间内运行

而对于Linux实现的线程,本质上是轻量化的进程,还是用的task_struct去维护的每一个线程

Linux进程结构

关于线程间内存共享
如上图所示,线程之间只有栈区是相互独立的, 像是全局变量,堆区数据都是共享的

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点

性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的
同步和调度开销,而可用的资源不变。

健壮性降低

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

缺乏访问控制

进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

编程难度提高

编写与调试一个多线程程序比单线程程序困难得多

线程异常

所有线程好比铁索连环,如果单个线程出现诸如 除零,野指针等异常问题导致线程崩溃,整个进程,包括所有线程,都会崩溃退出

线程的用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验

线程与进程辨析

进程

资源分配的基本单位,即每个进程都会分配一套独立的进程地址空间/虚拟地址,并且供内部的所有线程共享

线程

线程是调度的基本单位,线程共享进程数据,但也拥有自己的一部分数据

  • 线程ID
  • 一组寄存器
  • 栈
  • errno
  • 信号屏蔽字
  • 调度优先级

进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程
中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境

  • 文教描述符表
  • 每种信号的处理方式(SIG_IGN、SIG_DFL或自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

Linux线程控制

这里介绍POSIX线程库,这是一个第三方线程库,且是个动态库,有以下特点

  • 与线程有关的函数构成了个完整的系列,绝大多数函数的名字都是以pthread_打头的
  • 要使用这些函数库,要通过引入头文<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的-lpthread选项 很容易忘

创建线程

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

使用pthread_create函数创建新线程

  • 参数
    • thread:输出型参数,返回新线程的线程id
    • attr:设置线程属性,为nullptr时使用默认属性,一般为nullptr
    • start_routine:函数地址,即新线程启动时调用的函数
    • arg:传给start_routine指向函数的参数
  • 返回值:成功返回0,失败返回错误码

关于错误码

  • 统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • threads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  • hreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小

使用示例

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

#include <string.h>
#include <pthread.h>
void *rout(void *arg)
{
    int i;
    for (;;)
    {
        printf("I'am thread 1,got arg:%d\n",*(int*)arg);
        sleep(1);
    }
}
int main(void)
{
    pthread_t tid;//储存tid的变量
    int ret;
    int arg = 114514;//准备参数
    //创建新线程
    if ((ret = pthread_create(&tid, NULL, rout, &arg)) != 0)
    {
        //创建失败
        fprintf(stderr, "pthread_create : %s\n", strerror(ret));
        exit(EXIT_FAILURE);
    }
    int i;
    for (;;)
    {
        //主线程输出
        printf("I'am main thread\n");
        sleep(1);
    }
}

线程id及进程地址空间布局

  • hread_create函数会产生一个线程id,存放在第一个参数指向的地址中。该线程id和前面说的线程ID(本文用大小写区分)不是一回事。
  • 前文的线程ID于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • hread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
  • 程库NPTL提供了pthread_self函数,可以获得线程自身的ID:

pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  • 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  • 线程可以调用pthread_exit终止自己。
  • 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

pthread_exit函数

void pthread_exit(void *retval);

由子线程本身执行

retval为输出型参数,而函数调用后,该线程退出,栈帧销毁,所以,需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

pthread_cancel函数

int pthread_cancel(pthread_t thread);

由主线程向目标线程使用

用于取消一个正在执行中的线程。

线程等待

为什么需要线程等待?这一点可以类比多进程中的进程等待

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。
  • 进程等待可以回收已退出线程的资源

pthread_join函数

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

  • thread:线程id
  • retval:输出型参数,随着线程的结束方式的不同而改变
  • 返回值: 成功返回0,失败返回错误码

调用该函数的线程将挂起等待,直到目标thread线程终止。

retval详解
thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. return返回:如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. pthread_cancel:如果thread线程被别的线程调用pthread_ cancel异常终止,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
  3. pthread_exit:如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  4. 不感兴趣:如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

分离线程

默认情况下,新创建的线程是joinable的,即线程退出后,需要对其进行pthread_join操作,否则有内存泄漏风险

但如果不关心线程的返回值,此时join是一种负担,这时我们可以通过接口告诉系统,当线程退出时,自动释放线程资源,这个调用接口的操作就是分离线程

pthread_detach函数

int pthread_detach(pthread_t thread);

该函数可以使目标线程分离,配合pthread_self()函数可以使自己分离

特别的,joinable和分离是冲突的,分离之后就不能再执行join操作了

线程互斥

背景概念补充

  • 临界资源:多线程执行流共享的资源,这些资源在某一时刻只能被一个线程访问,这样的资源就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

互斥量mutex

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量

但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

多个线程并发的操作共享变量,会带来一些问题。下面有一段代码展示

// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;//抢票程序,预留100张票
void *route(void *arg)
{
    char *id = (char *)arg;//从参数里获取线程名
    while (1)
    {
        if (ticket > 0)
        {
            usleep(1000);//降低每次抢票的速度
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
}
int main(void)
{
    pthread_t t1, t2, t3, t4;//准备四个线程id
    char name1[] = "thread 1";//准备四个线程名
    char name2[] = "thread 2";
    char name3[] = "thread 3";
    char name4[] = "thread 4";

    pthread_create(&t1, NULL, route, name1);//创建四个线程
    pthread_create(&t2, NULL, route, name2);
    pthread_create(&t3, NULL, route, name3);
    pthread_create(&t4, NULL, route, name4);
    pthread_join(t1, NULL);//进程等待
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}

某一次的执行结果

可以看到代码的执行与预期的行为发生了严重误差

  • 有的票被重复卖出
  • 有负数的票被卖出

究其原因,就是全局变量ticket被所有线程共享,对其的访问控制确得不到限制,同时对其进行非原子性的操作时,就会出现数据不一致问题,进而引发一系列问题

例如对这段代码有如下分析

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程
  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
  • --ticket 操作本身就不是一个原子操作
取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

显然--操作并不是原子操作,而是对应三条汇编指令

  • load: 将共享变量ticket从内存加载到寄存器中
  • update: 更新寄存器里面的值,执行-1操作
  • store: 将新值,从寄存器写回共享变量ticket的内存地址

将新值,从寄存器写回共享变量ticket的内存地址

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

互斥量的接口

初始化互斥量

初始化互斥量主要有两种方法

  1. 静态分配
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  2. 动态分配
    int pthread_mutex_init(pthread_mutex_t *restrict_mutex, const pthread_mutexattr_t *restrict_attr);
  • mutex为要初始化的的互斥量的变量地址
  • attr为可选参数,传nullptr即可
销毁互斥量

特别的,静态分配的,即使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要手动销毁

而对于动态分配的互斥量

  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

使用的如下函数

int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

调用 pthread_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

现学现用

我们来利用锁来改进上面的选票程序

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

int ticket = 100;//抢票程序,预留100张票
pthread_mutex_t mutex;//准备一个互斥量

void *route(void *arg)
{
    char *id = (char *)arg;//从参数里获取线程名
    while (1)
    {
        pthread_mutex_lock(&mutex);//进入if判断前就得加锁,因为判断时ticket也是共享的
        if (ticket > 0)
        {
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);//完成对共享资源的访问,可以释放互斥量
            //usleep不访问共享资源,放在锁外面,以提高多线程并行效率
            usleep(1000);//降低每次抢票的速度
        }
        else
        {
            pthread_mutex_unlock(&mutex);//原理同上
            break;
        }
    }
    return nullptr;
}
int main(void)
{
    pthread_mutex_init(&mutex,nullptr);//初始化互斥量
    pthread_t t1, t2, t3, t4;//准备四个线程id
    char name1[] = "thread 1";//准备四个线程名
    char name2[] = "thread 2";
    char name3[] = "thread 3";
    char name4[] = "thread 4";

    pthread_create(&t1, NULL, route, name1);//创建四个线程
    pthread_create(&t2, NULL, route, name2);
    pthread_create(&t3, NULL, route, name3);
    pthread_create(&t4, NULL, route, name4);
    pthread_join(t1, NULL);//进程等待
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    pthread_mutex_destroy(&mutex);//销毁互斥量

    return 0;
}

可以看到,在代码中我们用互斥量将共享资源ticket保护了起来,使其变成了临界资源,相关的代码区变成了临界区,特别的,临界区的代码越少越好,将例如usleep这样耗时长,又与共享资源无关的代码移出临界区,这样才能最大化多线程并行的效率。

可重入VS线程安全

概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

常见的可重入的情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全的联系

  • 函数是可重入的,那就是线程安全的 可重入->线程安全
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全的区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

常见锁概念

死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

死锁四个必要条件
  1. 互斥条件:一个资源每次只能被一个执行流使用
  2. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配
避免死锁的算法
  • 避免死锁算法
  • 银行家算法

Linux线程同步

条件变量

当一个线程互斥的访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如,一个线程访问队列时,发现队列为空,就只能等待,直到其它线程将一个节点添加到队列中,这种情况就要用到条件变量

同步概念与竞态条件

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步

竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

条件变量函数

初始化

int pthread_cond_init(pthread_cond_t *restrict_cond,const pthread_condattr_t *restrict_attr);

    restrict_cond:要初始化的条件变量的地址
    restrict_attr:可选参数,传nullptr即可

销毁

int pthread_cond_destroy(pthread_cond_t *cond)

等待条件满足(互斥地)

int pthread_cond_wait(pthread_cond_t *restrict_cond,pthread_mutex_t *restrict_mutex);

    restrict_cond:目标条件变量的地址
    restrict_mutex:互斥量,用于保护条件变量

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
简单示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

pthread_cond_t cond;   // 创建信号量
pthread_mutex_t mutex; // 创建互斥量

void *chicken(void *arg)
{
    while (1)
    {
        pthread_cond_wait(&cond, &mutex);
        printf("鸡哥获得了一个篮球\n");
    }
}

void *kunkun(void *arg)
{
    for (int i = 0; i < 5; ++i)
    {
        printf("坤坤扔出了一个篮球[%d]\n",i);
        pthread_cond_signal(&cond);
        sleep(1);
    }
}

int main()
{
    pthread_t t1, t2;
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&t1, NULL, chicken, NULL);
    pthread_create(&t2, NULL, kunkun, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}

在如上代码中,chicken线程负责等待条件变量(这里具象化为获取篮球),kunkun线程负责唤醒信号量(这里具象化为扔出篮球)

特别的,本段代码通过for循环控制kunkun线程只唤醒五次信号量,而chicken线程工作在死循环中,我们看一下代码的输出

可以看到,kunkun线程按照预期执行了5次循环,而chicken却也因为条件变量的控制,只执行了5次循环

为什么pthread_cond_wait需要互斥量?

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有另一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

按照上面的说法,我们设计出如下的错误代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:

// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false)
{
    pthread_mutex_unlock(&mutex);
    // 解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
    pthread_cond_wait(&cond);
    pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
  • 由于解锁和等待不是原子操作。调用解锁之后,pthread_cond_wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_cond_wait。所以解锁和等待必须是一个原子操作。
  • int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。

条件变量使用规范

等待条件代码

pthread_mutex_lock(&mutex);
while (条件为假)
    pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);

给条件发送信号代码

pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

生产者消费者模型的有点

  • 解耦
  • 支持并发
  • 支持忙闲不均

基于BlockingQueue的生产者消费者模型

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

POSIX信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。

初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值

销毁信号量

int sem_destroy(sem_t *sem);

等待信号量

功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()

发布信号量

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()

上一节生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序
(POSIX信号量)

线程池

概念

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着
监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利
用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.

代码示例

#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include <string>
#include <queue>
#include <unistd.h>
struct ThreadInfo//储存线程信息
{
    pthread_t tid;
    std::string name;
};

static const int defaultnum = 5;//默认5个线程

template<class Task>
class ThreadPool
{
public:
    void Lock()
    {
        pthread_mutex_lock(&_mutex);
    }

    void Unlock()
    {
        pthread_mutex_unlock(&_mutex);
    }

    void Wakeup()
    {
        pthread_cond_signal(&_cond);
    }

    void ThreadSleep()
    {
        pthread_cond_wait(&_cond,&_mutex);
    }

    bool IsQueueEmpty()
    {
        return _tasks.empty();//封装一下接口
    }

    std::string GetThreadName(pthread_t tid)
    {
        for(const auto &ti:_threads)
        {
            if(tid == ti.tid) return ti.name;
        }
        return "None";
    }

    Task Pop()
    {
        //确认只在加锁的情况下调用
        Task t = _tasks.front();
        _tasks.pop();
        return t;
    }

public:
    ThreadPool(int num = defaultnum):_threads(num)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_cond,nullptr);
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    static void *HandlerTask(void* args)
    {
        ThreadPool<Task> *tp = static_cast<ThreadPool<Task> *>(args);
        std::string name = tp->GetThreadName(pthread_self());

        // std::cout<<"get this success"<<std::endl;//DEBUG

        while(true)
        {
            tp->Lock();
            while(tp->IsQueueEmpty())//访问临界资源
            {
                tp->ThreadSleep();
            }
            // std::cout<<"task ready"<<std::endl; //DEBUG
            Task t = tp->Pop();
            // std::cout<<"Pop success"<<std::endl; //DEBUG
            tp->Unlock();

            t();//处理线程私有的任务,不需要加锁,可以并发
            std::cout<<name<<" run :result :" << t._result<<std::endl;
            sleep(4);
        }

    }
    void Start()
    {
        int num = _threads.size();
        for(int i = 0;i<num;++i)
        {
            _threads[i].name = "thread-"+std::to_string(i);
            pthread_create(&(_threads[i].tid),nullptr,HandlerTask,this);
            std::cout<<_threads[i].name<<" create successfully"<<std::endl;
        }
    }

    void Push(const Task& t)
    {
        Lock();
        _tasks.push(t);//推送任务
        Wakeup();
        Unlock();
    }

private:
    std::vector<ThreadInfo> _threads;//线程池
    std::queue<Task> _tasks;//任务队列

    pthread_mutex_t _mutex;//锁
    pthread_cond_t _cond;//条件队列
};

线程安全的单例模式

单例模式是一种"经典的,常用的,常考的"设计模式

某些类, 只应该具有一个对象(实例), 就称之为单例

在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据

饿汉实现方式和懒汉实现方式

单例的实现主要有两种方式。

[洗完的例子]

吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.

懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度.

饿汉方式实现

template <typename T>
class Singleton {
    static T data;
public:
    static T* GetInstance() {
        return &data;
    }
};

只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个static T 对象的实例.

懒汉方式实现

template <typename T>
class Singleton {
    static T* inst;//声明指针
public:
    static T* GetInstance() {
        if (inst == NULL) {
            inst = new T();
        }
        return inst;
    }
};

存在一个严重的问题, 线程不安全

因为inst是多个进程共享的资源,对其进行判空操作,也要用锁保护起来

线程安全的版本实现

// 懒汉模式, 线程安全
template <typename T>
class Singleton
{
    volatile static T *inst; // 需要设置 volatile 关键字, 否则可能被编译器优化->被放到寄存器中.
    static std::mutex lock;

public:
    static T *GetInstance()
    {
        if (inst == NULL)
        {                // 双重判定空指针, 降低锁冲突的概率, 提高性能.
            lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
            if (inst == NULL)
            {
                inst = new T();
            }
            lock.unlock();
        }
        return inst;
    }
};

特别注意

  1. 加锁解锁的位置
  2. 双重if判定,避免不必要的锁竞争
  3. volatile关键字防止过度优化

STL,智能指针和线程安全

STL库是不是线程安全的

原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.

智能指针是线程安全的

  • unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问

  • shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这
    个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.

Linux 下系统原生的 C++ 线程同步与异步编程详解(含示例代码) 方法所属是否推荐应用场景POSIX API✅ 推荐共享资源保护POSIX API✅ 推荐线程间通信POSIX API✅ 推荐读多写少场景POSIX API❗慎用短时间等待POSIX API✅ 推荐后台任务aio_readaio_writePOSIX AIO✅ 推荐高性能异步 I/OLinux 下使用原生的pthread接口可以实现高效的线程同步与异步操作,尤其适合对性能、资源控制有较高要求的系统级编程项目。 阅读详情

相关推荐

DMA 定制固件教程:小白跟做即得单人固件,超详细纯喂饭教程,100% 成功秘籍!FPGA仿真1:1、中断逻辑和TLP核心都在。

DMA 定制固件教程 小白跟着操作做可以做出的单人固件。DMA仿真技术io、中断逻辑,从零仿真。图文链接教程:https://docs.qq.com/doc/DQ01lVGtHelROVHNv

qq_15741349的博客 3万+

Linux-线程

文章目录操作创建线程 创建进程,我们从0到有创建了很多东西,申请了很多资源。 进程:承担分配系统资源的基本实体。 线程:调度的基本单位,线程是进程里面的执行流(线程在进程的地址空间内运行)。 进程:线程 = 1:n Linux中,没有真正意义上的线程,线程是用进程模拟的(轻量级进程)。 只有第一个进程执行时创建了地址空间等资源,其它进程都只是分享了同一个资源。显然分配资源比申请资源快。 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”” 一切进程

mmz123_的博客 1315

时序数据库避坑指南:TDengine与IoTDB在车联网场景的7个关键对比

本文对比了时序数据库TDengine与IoTDB在车联网场景下的7个关键差异,包括数据模型设计、边缘-云端协同能力、车规级函数库、存储压缩算法等。通过真实案例和性能测试,帮助企业在新能源汽车大数据处理中选择合适的时序数据库,优化存储成本和实时告警性能。

rust6ferris的博客 453

Linux下pthread_t的定义

开发环境 VMware虚拟机 Ubuntu16.04 C语言 问题描述 调用 pthread_self() 函数获取线程 ID 并用 printf打印出来,需要知道 pthread_self() 函数返回值的类型。 查询步骤 找到 pthread_self() 函数的定义,打开命令行,输入以下命令进入帮助手册,如下图所示 man pthread_self 可以看到 pthread_self 函数定义在头文件 pthread.h 中,其返回值类型为 pthread_t。 打开 pthread.h

Deng_alone的博客 1万+

海康视觉平台VisionMaster实战入门:从通讯配置到工具应用

本文为海康VisionMaster视觉平台实战入门指南,详细介绍了从软件安装准备、与PLC等外部设备的通讯配置,到匹配定位、测量、畸变矫正等核心工具的应用方法。通过实战案例拆解,帮助工程师快速掌握如何搭建完整的机器视觉检测与引导定位系统,实现工业自动化项目落地。

weixin_29224309的博客 1016

DNA pull-down技术避坑指南:从探针设计到质谱验证的完整实验方案

本文提供了一份详尽的DNA pull-down技术实战避坑指南,涵盖从探针设计、生物素标记、磁珠选择与预处理,到结合洗脱优化及下游质谱验证或Western Blot验证的全流程。重点解析了如何通过精细操作降低非特异性结合、提高实验特异性与重复性,并对比了质谱与WB两种验证路径的适用场景,旨在帮助研究者构建稳健可靠的DNA-蛋白质互作研究体系。

weixin_29274337的博客 495

linux pthread 多线程,Linux多线程之pthread_self

原型 :#include pthread_t pthread_self(void);说明 :获取本进程自身的 ID。进程 ID 类型是 pthread_t ,这个类型一般为long long 型,8个字节。测试代码 :#include #include #include void* thread_one(){printf ( "thread_one pid is %lld \n " , ...

weixin_36397104的博客 274

把 NAST 变成你的遥控器:用 ABAP 程序精确触发与处理 Output Type

摘要 SAP中的Message Control机制通过NAST表记录输出状态,支持自动触发打印、邮件等操作。关键步骤包括: 构造NAST记录:定义输出类型、介质、处理状态等参数 调用处理函数:使用RV_MESSAGE_UPDATE_SINGLE更新表,通过WFMC_MESSAGES_PROCESS执行输出 典型应用场景: 单据补发(如PO邮件重发) 外部系统集成触发输出 故障恢复处理积压消息 注意事项: 需提交事务确保更新生效 区分S/4HANA新旧输出体系 正确处理派发时间(VSZTP)和状态(VSTAT

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 257

【Linux】线程

线程相关内容

weixin_74078718的博客 3398

修改Windows hosts文件的解决方案

本文主要介绍了修改Windows hosts文件解决方案,希望能对新手有所帮助。 文章目录 1. 问题描述 2. 解决方案 2.1 安装软件 2.2 修改hosts文件

weixin_43178406的博客 1万+

Feature ‘alsa‘ was enabled, but the pre-condition ‘config.unix && !config.qnx && libs.alsa‘ failed解决

这个问题主要是Qt多媒体插件中缺少了ALSA音频库,所以我们需要自行先编译ALSA 在执行./configure的时候,配置项中ALSA为no Qt Multimedia: ALSA ................................... no GStreamer 1.0 .......................... no GStreamer 0.10 ......................... no Video for Linux .............

有问题或资源连接有缺失的可以私信公众号新潮看世界获取 2588

【Linux】线程详解

在一个程序里的一个执行路线就叫做线程。更准确的定义是:线程是“一个进程内部的控制序列”一切进程都至少有一个执行线程线程在进程内部运行,本质是在进程地址空间内运行在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更轻量化透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流进程的创建伴随着进程控制块,进程地址空间以及页表的创建,虚拟地址和物理地址就是通过页表建立映射的。

m0_72940975的博客 3686

STM32实战开发(150):基于STM32的智能电力负荷控制系统开发

本文介绍了基于STM32的智能电力负荷控制系统的设计与实现,涵盖了硬件设计、软件设计、负荷预测与控制等多个方面。该系统能够实时监控电力负荷,预测未来负荷变化,并根据预测结果自动调节电力负荷,防止电网过载或浪费。未来,我们可以进一步优化该系统,加入更多的传感器数据,提升预测算法的精度,并结合大数据与云计算技术实现更为强大的远程控制与数据分析功能。

2201_76125393的博客 715

[Linux]多线程(二)原生线程库---pthread库的使用

本文详细介绍了Linux下pthread库的使用,涵盖了多线程编程的核心函数和概念。主要内容包括: 线程创建:通过pthread_create创建线程,需注意参数传递和资源管理。 线程回收:使用pthread_join等待线程结束并回收资源,避免资源泄漏。 线程退出:通过pthread_exit或return终止线程,区分主线程与子线程的退出行为。 线程分离:使用pthread_detach将线程标记为分离状态,线程结束后自动释放资源。 互斥锁与条件变量:通过pthread_mutex和pthread_c

石墨博客 1569

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

Linux学习记录——이십오 多线程(2)

锁,互斥,线程同步

kongqizyd146的博客 893

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

Linux进阶篇:线程

线程是进程内的执行单元,是操作系统调度的基本单位,一个进程可包含多个线程,所有线程共享进程的地址空间(代码段、数据段、堆等),仅拥有独立的栈空间和寄存器上下文。轻量级:线程创建与切换的开销远小于进程资源共享:线程间可直接访问进程的全局变量、文件描述符等资源并发执行:多个线程可在同一进程内并发执行,提高程序执行效率。

2403_84469788的博客 928

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

并发之殇与互斥之盾:深入浅出 Linux 互斥锁(Mutex)底层原理与实战

本文是一篇面向零基础学员的并发编程指南。文章从多线程“同居生活”的形象比喻出发,通过一个直观的“抢票系统”Demo,深度剖析了并发环境下数据不一致的根源——非原子操作。随后,文章详细讲解了 Linux 原生互斥锁(Mutex)的 API 使用,并进阶演示了 C++ RAII 风格的优雅封装。最后,文章结合汇编级伪代码与字符画,揭秘了互斥锁底层依靠 CPU exchange 原子指令实现“偷天换日”的终极魔法,并澄清了互斥锁与并发效率之间的关系,全面助力读者攻克线程同步的难关。

kumaxi的博客 328

Linux 线程同步与互斥(一)线程的并发问题,线程的互斥锁,C++中的互斥锁

本文探讨了多线程并发中的临界资源保护问题。通过抢票案例分析了无锁保护下共享变量ticket出现负数票的并发问题根源:非原子操作被线程切换打断导致数据竞争。介绍了互斥锁(mutex)的解决方案,包括C语言的pthread_mutex_t实现和C++的std::mutex与std::lock_guard封装。深入剖析了锁的底层原理:通过CPU原子指令和内存状态交换实现线程互斥。最后展示了如何用RAII思想封装自定义锁类,确保临界区安全访问。锁的本质是通过软件机制模拟硬件原子性,解决多线程并发访问共享资源的问题。

2401_88465894的博客 459

Linux线程同步机制深度解析与性能优化实践

线程同步是多线程编程中的核心概念,主要用于解决共享资源访问时的数据竞争问题。其原理是通过互斥锁、条件变量等同步原语,协调线程执行顺序。在Linux环境下,合理的同步机制能显著提升程序性能与稳定性,尤其适用于电商秒杀、金融交易等高并发场景。以互斥锁为例,通过细粒度锁拆分和自适应锁优化,可降低30%以上的锁竞争开销。而条件变量配合屏障同步,则能有效解决并行计算中的任务调度问题。本文通过电商超卖案例和图像处理实践,展示了线程同步在保证数据一致性的同时,如何实现系统吞吐量从12k到38k QPS的跨越式提升。

weixin_28700593的博客 278

C++多线程编程:跨平台互斥锁原理与实战指南

在多线程编程中,数据竞争是导致程序行为不确定性的常见问题。互斥锁通过确保临界区资源的独占访问,提供了解决数据竞争的基本同步机制。其核心原理在于利用原子操作和操作系统调度,实现线程间的互斥访问,从而保障共享数据的一致性。这一技术对于构建高并发、高可靠的后端系统至关重要,广泛应用于计数器保护、生产者-消费者模型等并发控制场景。本文深入探讨了C++标准库、Linux pthread以及Windows原生API三种平台下的互斥锁实现,并结合锁守卫(Lock Guard)和条件变量等高级特性,提供了跨平台开发的实战解

weixin_29973493的博客 352

【Linux】多线程--互斥锁

文章目录前言基础概念互斥量mutex多线程模拟抢票(没加锁情况)为何多线程访问临界资源是不安全互斥锁相关接口多线程模拟抢票(有加锁)互斥锁实现的基本原理 前言 为什么线程需要同步和互斥的操作? 因为线程引入共享了进程的地址空间,导致了一个线程操作数据时候,极其容易影响到其他线程的情况;对其他线程造成不可控因素,或引起异常,逻辑结果不正确的情况;这也是线程不安全的原因! 如何创建一个线程安全函数?只要尽量不使用全局变量,stl,malloc,new 等操作即可;如果要使用,就要控制同步和互斥问题! 基础概念

呋喃吖的博客 1万+

[linux仓库]多线程数据竞争?一文搞定互斥锁与原子操作[线程·伍]

本文深入探讨了Linux线程互斥机制及其底层实现原理。通过抢票Demo案例,分析了共享资源访问导致的数据不一致问题,提出临界资源与临界区的概念,解释了互斥锁如何通过原子性操作解决并发问题。文章详细介绍了互斥锁的初始化、加锁和解锁操作,阐述了RAII设计模式在锁封装中的应用,并对比了C++11原生mutex与pthread库的实现差异。最后,通过汇编层面分析++/--操作的非原子性,揭示了互斥锁的底层实现原理是借助swap/exchange指令保证的原子性操作。

egoist2023的博客 1894

【Linux】多线程编程(一):线程与互斥锁详解

这篇博客讲解了Linux中多线程编程的基础

EWIAW_ilove的博客 1174

Linux环境下 解决线程共享资源冲突 —— 互斥锁(代码实现及底层原理)

为了保护这些共享资源在被使用的时候,不会受到其他线程的影响,此时要用的就是互斥锁

challenglistic的博客 3523

Linux线程(整理)

在 C 语言的多线程编程中,线程可以创建为守护线程(daemon thread)或非守护线程。守护线程在创建后不会阻塞其父线程的终止,而非守护线程则会在其终止时阻塞父线程的终止。函数允许父线程等待其子线程结束。这意味着,直到子线程完成执行,父线程才会继续执行。以下是如何使用创建一个线程函数,该函数将在新线程中执行。// 执行线程任务C++11 引入的线程库简化了多线程编程,使得创建和管理线程变得更加容易。使用用类可以轻松地创建线程,并通过互斥锁、条件变量等机制来同步线程。

ljw的博客 2131

Linux 互斥锁

互斥锁的使用、线程同步和互斥、条件变量的使用 ...

2201_75663820的博客 904
上一篇: 【数据结构】一步到胃,键值对版二叉搜索树
下一篇: 【Linux】简易进程池
supdriver
博客等级 码龄3年 192粉丝 40原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值