Linux多线程

1. 线程概念

什么是线程?

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
  • 一切进程至少都有一个执行线程。
  • 线程在进程内部运行,本质是在进程地址空间内运行。
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

现在说一下平常我们课本中所看到的线程的定义。

  • 线程是比进程更加轻量化的一种执行流,线程是在进程内部执行的一种执行流。

  • 线程是CPU调度基本单位,进程是承担系统资源的基本实体。

1.1 Linux下的线程

image-20240702102216077

可以看到,进程地址空间是进程的“资源窗口”。

相比于进程来说

  1. 线程创建更简单
  2. 线程在进程的地址空间中运行

操作系统如果真的支持线程,那么也必须管理线程,要管理线程就要先描述再组织,并且也要和进程一样创造一个新的线程描述符TCB,不仅如此,还要创造各种与之相关的各种算法等,非常麻烦。于是乎,Linux的开发者想到了一个非常聪明的办法,TCBPCB的功能和内容实际上是差不多的,所以在Linux下线程直接复用了进程的PCB和调度代码,这样不仅对Linux的开发者来说省了很多事,而且对于后期的维护也非常简单的,只要进程没问题线程就一定没问题,使用起来也更加可靠,增强了代码的健壮性。

那么有没有一个系统是真正意义上的实现了线程这一模块呢?答案是有的,就是我们现在经常所使用的Windows操作系统。

在Linux下:

  • 并不存在真正意义上线程,是用进程的数据结构模拟实现的。

  • 在CPU眼中,看到的PCB都要比传统的进程更加轻量化

  • 在CPU的视角下,永远看到的只是PCB,那么CPU需不要细分这个CPU到底是线程还是进程呢?

举一个生动形象的栗子,CPU就好比是快递员,然而这个快递员并不关心快递的东西是什么,只需要把快递按时交付到客户手上即可!所以CPU并不关系这个PCB到底是线程还是进程,只要执行就完事了!

在今天来看,CPU拿到的执行流是<=(小于等于)进程的。

那么如何看待之前学过的进程呢?

内部只有一个执行流的进程。

如何看待今天的进程呢?

内部有多个执行流的进程。

线程切换为什么效率更高

  1. 切换的寄存器更少;
  2. 不需要重新更新切换Cache。

文件系统IO的基本单位大小:4KB

1.2 线程的一些特性

线程的优点:

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

线程的缺点:

  • 性能损失
    • 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低
    • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。任何一个线程出现崩溃整个进程都会跟着崩溃。
  • 缺乏访问控制
    • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高
    • 编写与调试一个多线程程序比单线程程序困难得多

线程异常:

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该
    进程内的所有线程也就随即退出。

线程用途:

  • 合理的使用多线程,能提高CPU密集型程序的执行效率。
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。

1.3 Linux进程VS线程

  • 进程是资源分配的基本单位。
  • 线程是调度的基本单位。

线程共享进程数据,但也拥有自己的一部分数据:

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

总的来说(在笔试中可以这样答),线程拥有独立的硬件上下文,它是被调度被切换的;每个独立的线程要拥有自己的栈结构

进程的多个线程共享:

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

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGNSIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

进程和线程的关系如下图:

QQ_1720018686577

2. 线程控制

2.1 线程创建

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

首先来看看pthread_create函数:

QQ_1720020882807

功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void*arg);
参数:

  • thread:返回线程ID

  • attr:设置线程的属性,attrNULL表示使用默认属性,基本都设置为NULL

  • start_routine:是个函数地址,线程启动后要执行的函数

  • arg:传给线程启动函数的参数,参数类型为void*,说明对象、结构体等类型都可以传递。

返回值:成功返回0;失败返回错误码

// 新线程
void *NewThread(void *args)
{
	while (true)
	{
		cout << "I am a new thread!" << endl;
		sleep(1);
	}
}

int main()
{
	pthread_t ptid;
	pthread_create(&ptid, nullptr, NewThread, nullptr);

	// 主线程
	while (true)
	{
		cout << "I am main thread!" << endl;
		sleep(1);
	}
	return 0;
}

当我们直接编译一段多线程的代码时,会发现编译发生错误。

QQ_1720064989209

此时,我们需要在编译选项加入线程库-lpthread

QQ_1720065125318

编译成功!

上面所提到过,Linux没有真正的线程,只有轻量级的进程的概念。所以Linux操作系统只会提供轻量级进程创建的系统调用,不会直接提供线程创建的接口!

QQ_1720063629092

接口函数和实现函数实行解耦,维护性大大增加!

线程ID及进程地址空间布局:

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

QQ_1720078681737

所以pthread_t到底是什么类型呢?

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

QQ_1720149453260

2.2 线程终止

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

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

pthread_exit函数:

QQ_1720079148902

功能
线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值
无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。

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

pthread_cancel函数:

QQ_1720089422142

功能
取消一个执行中的线程。
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值
成功返回0,失败返回错误码。

2.3 线程等待

为什么需要线程等待?

  • 线程退出,如果没有等待,会导致类似于进程的僵尸问题。
  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。

pthread_join函数:

QQ_1720084343448

功能
等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数

  • thread:线程ID
  • value_ptr:它指向一个指针,后者指向线程的返回值。

返回值
成功返回0;失败返回错误码

如何得到新线程这个函数的返回值呢?

可以看到pthread_join这个函数的第二个参数类型是void**,是一个输出型参数如果我们要得到新线程的返回值,所以这个新线程的返回值也应该是void*pthread_join函数为了得到了一个void*,那么就需要一个void**作为参数,传参的时候也就需要对void*这个类型进行取地址。

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

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

2.4 线程分离

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 线程是可以被设置为分离状态的。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

pthread_detach函数:

QQ_1720088948868

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable又是分离的

如果主线程不关心新线程的结果,就可以把新线程设置为分离状态。

2.5 关于线程控制的一些周边问题

线程局部存储

线程可以fork吗?

线程是可以fork的,线程的fork就相当于这个进程的fork。

线程可以进行exec*程序替换吗?

也是可以的,但是不建议这么做。因为这么做会让整个进程都被程序替换了,有可能会影响其他线程,所以要进行线程程序替换时,建议先使用fork创建子进程来进行程序替换。

3. 线程互斥

3.1 进程线程间的互斥相关背景概念

(其实在信号量那部分已经提到过了,这里再来叙述一下!)

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

互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。

下面来段代码演示一下:

int tickets = 10000; // 有一万张票可以抢
void GrabTicket(const string& name) // 传入线程名
{
    while (true)
    {
        // 证明还有余票,继续抢
        if (tickets > 0)
        {
            usleep(1000); // 模拟抢票所需要做的工作的时间
            printf("%s grap a ticket successfully: %d\n" , name.c_str(), tickets);
            tickets--;
        }
        else break; // 没有票了,直接跳出循环
    }
}

在Ubuntu下,会有这样的情况:

QQ_1720538624840

我们发现当前的票居然到了0、-1、-2,如果真的抢票系统出现这样的情况,那么证明会有三张票重复售出,随即造成比较严重的事故,为什么会出现这样的现象呢?

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程。
  • usleep 这个模拟抢票的过程,在这个漫长的抢票过程中,可能有很多个线程会进入该代码段。
  • --ticket操作本身就不是一个原子操作。

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

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

要解决以上问题,需要做到三点:

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

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

QQ_1720447075226

3.2 互斥量(锁)的接口

使用man手册查看一下初始化互斥量的函数(man pthread_mutex_init):

QQ_1720533270519

初始化互斥量有两种方法:

方法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:第二个参数是一个指向 pthread_mutexattr_t 类型的指针,用于指定互斥锁的属性。如果将第二个参数设置为 NULL,则表示使用默认的互斥锁属性。默认情况下,创建的是普通的非递归互斥锁。

所谓的动静态分配就是一个是全局互斥量,一个是局部互斥量。

销毁互斥量:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

需要注意的是:

  • 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁。
  • 不要销毁一个已经加锁的互斥量。
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

互斥量加锁和解锁:

QQ_1720533747911

原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

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

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

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

修改一下上段抢票代码

int tickets = 10000; // 有一万张票可以抢
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void GrabTicket(const string& name)// 传入线程名字
{
    while (true)
    {
        // 证明还有余票,继续抢
        pthread_mutex_lock(&mutex); // 加锁
        if (tickets > 0)
        {
            usleep(1000); // 模拟抢票所需要做的工作的时间
            printf("%s grap a ticket successfully: %d\n" , name.c_str(), tickets);
            tickets--;
            pthread_mutex_unlock(&mutex); // 解锁
        }
        else
        {
            pthread_mutex_unlock(&mutex); // 解锁
            break;
        }
    }
}

QQ_1720539413440

问题得到了很好的解决,但是执行速度也变慢了很多。

互斥量实现原理探究:

  • 经过上面的例子,大家已经意识到单纯的i++或者++i都不是原子的,有可能会有数据一致性问题。
  • 为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下

3.3 一些常见锁的概念

死锁

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

死锁四个必要条件:

  • 互斥条件:一个资源每次只能被一个执行流使用。
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

避免死锁的方法:

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

避免死锁算法(这里不做过多赘述)

  • 死锁检测算法
  • 银行家算法

3.4 锁的封装

为了让我们的代码更加优雅点,下面我们对进行封装一下~

#pragma once 

#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t* mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }

    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }
private:
    pthread_mutex_t* _mutex;
};

还是上段抢票代码,做一下小小的修改:

int tickets = 10000; // 有一万张票可以抢
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void GrabTicket(const string &name)
{
    while (true)
    {
        // 为了更加明确表示这一块代码时临界区
        {
            // 证明还有余票,继续抢
            // pthread_mutex_lock(&mutex); // 加锁
            LockGuard lockguard(&mutex);
            if (tickets > 0)
            {
                usleep(1000); // 模拟抢票所需要做的工作的时间
                printf("%s grap a ticket successfully: %d\n", name.c_str(), tickets);
                tickets--;
                // pthread_mutex_unlock(&mutex); // 解锁
            }
            else
            {
                // pthread_mutex_unlock(&mutex); // 解锁
                break;
            }
        }
    }
}

解释一下吧:

LockGuard lockguard(&mutex);这句代码相当于在while循环内部定义的临时对象,形成该对象的时候,就会调用相应的构造函数来进行加锁,无论是if条件满足还是else跳出循环,这个临时对象都会重新进行构造和析构,也就是加锁和解锁,是不是很nice!

4. 线程同步

加锁

  1. 我们要尽可能的给少的代码块加锁。
  2. 一般加锁,都是给临界区加锁。
  3. 根据互斥的定义,任何时刻,只允许一个线程申请锁成功!如果有多个线程申请锁,申请失败的线程在mutex上进行阻塞,本质就是等待!
  4. 个别系统,抢票代码会出现很多的票被同一个线程抢完了。
  5. 多线程运行,同一份资源,有线程长时间无法拥有,就会产生饥饿问题
  6. 要解决饥饿问题,要让线程执行的时候,具备一定的顺序性,也就是同步

4.1 条件变量

同步概念与竞态条件

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。

条件变量函数

30497653f1ce2dc9850bc0507d9ff24f

原型:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

参数:
cond:要初始化的条件变量
attr:一个指向 pthread_condattr_t 类型的指针,用于指定条件变量的属性。pthread_condattr_t 结构体定义了条件变量的一些特性。如果将第二个参数设置为 NULL,则表示使用默认的条件变量属性。

销毁:

int pthread_cond_destroy(pthread_cond_t *cond);

等待条件满足

QQ_1720576243184

原型:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

参数:
cond:要在这个条件变量上等待
mutex:互斥量

唤醒等待

QQ_1720576362071

int pthread_cond_broadcast(pthread_cond_t *cond);使条件变量成立,并且唤醒所有的线程。

int pthread_cond_signal(pthread_cond_t *cond);使指定的条件变量条件成立,并且唤醒一个线程。

  1. 让线程在进行等待的时候,会自动释放锁;
  2. 线程被唤醒的时候,是在临界区内唤醒的,当线程被唤醒,线程在pthread_cond_wait返回的时候,要重新申请并持有锁;
  3. 当线程被唤醒的时候,重新申请并持有锁的本质也是要参与锁的竞争的!

5. 生产者消费者模型

生产者消费者中的“321原则”(不是标准概念,方便记忆,切忌直接拿来回答面试官的问题!)

  • 3种关系:生产者与生产者(竞争也就是互斥),消费者与消费者(竞争也就是互斥),生产者与消费者(互斥或同步)
  • 2种角色:生产者与消费者。
  • 1个交易场所:也就是内存空间。

为什么要使用生产者消费者模型?

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

生产者消费者模型优点

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

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

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

6. POSIX信号量

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

初始化信号量:

QQ_1720599946766

头文件
#include <semaphore.h>

原型
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数

  • sem:表示传递的信号量。
  • pshared:0表示线程间共享,非零表示进程间共享。
  • value:信号量初始值。

返回值
成功时返回0;出错时,返回 -1,并将errno设置为指示错误。

销毁信号量:

QQ_1720601519808

原型
int sem_destroy(sem_t *sem);

返回值
成功时返回0;出错时,返回-1,并将errno设置为指示错误。

等待信号量:

QQ_1720601649681

功能
等待信号量,会将信号量的值减1。

原型
int sem_wait(sem_t *sem); //P()也就是P操作

返回值
所有这些函数在成功时返回0;出错时,信号量的值保持不变,返回 -1,并将errno设置为指示错误。

发布信号量:

QQ_1720602133963

功能
发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。

原型
int sem_post(sem_t *sem);//V() 也就是V操作

返回值
所有这些函数在成功时返回0;出错时,信号量的值保持不变,返回 -1,并将errno设置为指示错误。

7. 基于环形队列的生产消费模型

  • 环形队列采用数组模拟,用模运算来模拟环状特性。
  • 环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态。

QQ_1720754353084

  • 但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程。

来段伪代码:

p->sem_space = N;
p->sem_data = 0;

// 生产者
P(sem_space)
...
// 生产行为
...
V(sem_data)

// 消费者
P(sem_data)
...
// 消费行为
...
V(sem_space)

8. 读者写者问题

8.1 读写锁

在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁

QQ_1720754814114

注意:写独占,读共享,读锁优先级高。

8.2 读写锁接口

设置读写优先

原型
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
int pthread_rwlockattr_getkind_np(const pthread_rwlockattr_t *attr, int *pref);

说明
pthread_rwlockattr_setkind_np()函数将attr引用的读写锁定属性对象的"锁定种类"属性设置为pref中指定的值。参数pref可以设置为以下之一:

  • PTHREAD_RWLOCK_PREFER_READER_NP
    这是默认值。一个线程可以持有多个读锁;也就是说,读取锁是递归的。根据《单一Unix规范》,当读取器尝试放置锁并且没有写入锁但写入器正在等待时,行为未指定。由PTHREAD_RWLOCK_PREFER_READER_NP设置的优先级授予读取器,意味着即使写入器正在等待,读取器也将收到请求的锁。只要有读者,作家就会饿死。
  • PTHREAD_RWLOCK_PREFER_WRITER_NP
    这旨在用作PTHREAD_RWLOCK_PREFER_READER_NP的写锁定模拟。 glibc忽略了这一点,因为支持递归读锁的POSIX要求将导致该选项创建琐碎的死锁;而是使用PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP,以确保应用程序开发人员不会采用递归读取锁,从而避免了死锁。
  • PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP
    只要不以递归方式进行任何读取锁定,就将锁定类型设置为此可以避免编写者饥饿。
  • pthread_rwlockattr_getkind_np()函数返回指针pref中attr引用的读写锁定属性对象的锁定>种类属性的值。

返回值
成功时,这些函数将返回0。给定有效的指针参数,pthread_rwlockattr_getkind_np()总是成功。发生错误时pthread_rwlockattr_setkind_np() 返回一个非零错误码 返回一个非零错误码 返回一个非零错误码

😊参考自——OniTRoad之路教程

初始化:

QQ_1720755053508

原型:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);

返回值:
如果成功,pthread_rwlock_destroy()pthread_rwlock_init() 函数将返回零;否则,应返回错误码以指示错误。

销毁:

QQ_1720755089212

原型:
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

返回值:
如果成功,pthread_rwlock_destroy()pthread_rwlock_init() 函数将返回零;否则,应返回错误码以指示错误。

加锁和解锁:

QQ_1720755127917

原型
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

返回值
如果成功,pthread_rwlock_rdlock() 函数将返回零;否则,应返回错误码以指示错误。如果获取了 rwlock 引用的读写锁对象上的读取锁,则pthread_rwlock_tryrdlock() 函数应返回零,否则为错误码应返回以指示错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值