Linux线程

线程概念

执行流

程序计数器中的下一条指令地址所组成的执行轨迹称为程序的控制执行流,
执行流就是一段逻辑上独立的指令区域,是人为给处理器安排的处理单元。指令是具备“能动性”的数据,因此只有指令才有“执行”的能力,它相当于是动作的发出者,由它指导处理器产生相应的行为。

指令是由处理器来执行的,它引领处理器“前进”的方向,用“流”来表示处
理器中程序计数器的航向,借此比喻处理器依次把此区域中的指令执行完后,所形成的像河流一样曲直不一的执行轨迹、执行路径(由顺序执行指令及跳转指令导致〉。

执行流对应于代码,大到可以是整个程序文件,即进程,小到可以是一个功能独立的代码块,即函数,而线程本质上就是函数。

执行流是独立的,它的独立性体现在每个执行流都有自己的栈、一套自己的寄存器映像和内存资源,这就是执行流的上下文环境。其实任何代码块,无论大小都可以独立成为执行流,只要在它运行的时候,我们提前准备好它所依赖的上下文环境就行,这个上下文环境就是它所使用的寄存器映像、栈、内存等资源。

在任务调度器的眼里,只有执行流才是调度单元,即处理器上运行的每个任务都是调度器给分配的执行流,只要成为执行流就能够独立上处理器运行了,也就是说处理器会专门运行执行流中的指令。

线程

程序是指静态的、存储在文件系统上、尚未运行的指令代码,它是实际运行时程序的映像。
进程是指正在运行的程序,即进行中的程序,程序必须在获得运行所需要的各类资源后才能成为进程,资源包括进程所使用的枝,使用的寄存器等。

对于处理器来说,进程是一种控制流集合,集合中至少包含一条执行流,执行流之间是相互独立的,但它们共享进程的所有资源,它们是处理器的执行单位,或者称为调度单位,它们就是线程。

线程和进程比,进程拥有整个地址空间,从而拥有全部资源,线程没有自己的地址空间,因此没有任何属于自己的资源,需要借助进程的资源“生存”,所以线程被称为轻量级进程。由于各个进程都拥有自己的虚拟地址空间,正常情况下它们彼此无法访问到对方的内部,因为进程之间的安全性是由操作系统的分页机制来保证的,只要操作系统不要把相同的物理页分配给多个进程就行了。

但进程内的线程可都是共享这同一地址空间的,它们彼此能见面,也就意味着任意一个线程都可以去访问同一进程内其他线程其他的数据,这是避免不了的。

进程和线程都是执行流,它们都具备独立寄存器资源和独立的空间,因此线程也可以像进程那样调用其他函数,真正上处理器上运行的其实都叫线程,进程中的线程才是一个个的执行实体、执行流,因此,经调度器送上处理
器执行的程序都是线程。

查看线程

image-20240501145307829

LWP就是线程的id,可以看到其实只有一个线程的时候LWP和PID是相同的。LWP是内核级别的id和pthread库函数中的id是不同的,库函数创建的线程最终会被调度到内核级别的LWP中执行,所以它是一个内存地址可以在程序中区分不同的线程,而LWP表示线程在CPU中的身份,用于CPU时间片的划分。

线程控制

线程的实现由2种方式,要么系统原生支持,用户进程通过系统调用使用线程,要么操作系统不支持,由用户自己提供。因此,线程要么在 O 特权级的内核空间中实现,要么在 3 特权级的用户空间实现。

image-20240429110457852

这里介绍Linux中POSIX线程库,能够帮助我们创建和操控线程。

创建线程

#include <pthread.h>
//pthread_t类型的ID就是一个进程地址空间上的一个地址
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
//参数:
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
    
//返回值:
成功返回0;失败返回错误码  
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通
过返回值返回
    
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,
属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID,pthread_t pthread_self(void);

线程终止

void pthread_exit(void *value_ptr);
value_ptr:value_ptr不要指向一个局部变量。

int pthread_cancel(pthread_t thread); //取消一个执行中的线程
返回值:成功返回0;失败返回错误码

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

线程等待

int pthread_join(pthread_t thread, void **value_ptr);
//参数:
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

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

  1. 如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。

  2. 如果thread线程被别的线程调用pthread_cancel,value_ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。

  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。

  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

线程分离

int pthread_detach(pthread_t thread);
pthread_detach(pthread_self()); //分离自己

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

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

把上面的几个函数写成一段测试代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run( void * arg )
{
	pthread_detach(pthread_self());
	printf("%s\n", (char*)arg);
	return NULL;
}
int main( void )
{
	pthread_t tid;
	if ( pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0 ) {
    printf("create thread error\n");
    return 1;
}
    int ret = 0;
    sleep(1);//很重要,要让线程先分离,再等待
    if ( pthread_join(tid, NULL ) == 0 ) 
    {
    	printf("pthread wait success\n");
    	ret = 0;
    } 
    else 
    {
    	printf("pthread wait failed\n");
    	ret = 1;
    }
	return ret;
}

线程互斥

线程互斥是一种特殊的线程同步机制,用于保护共享资源不被多个线程同时访问,从而避免数据的不一致性和冲突。在实现线程互斥时,最常用的方式是通过互斥锁(Mutex)来实现。互斥锁是一种特殊的锁,它在同一时间只能被一个线程持有。当一个线程需要访问共享资源时,它需要先获得互斥锁,然后才能访问这个共享资源。当这个线程访问完共享资源后,它需要释放互斥锁,这样其他线程才能获得锁并访问共享资源。

互斥量接口

初始化

//1.静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
//2.动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr);
参数:
    mutex:要初始化的互斥量 attr:NULL

销毁

int pthread_mutex_destroy(pthread_mutex_t *mutex)

注意:

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

加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

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

利用上面的接口实现一个售票代码

class Ticket
{
private:
    int _tickets; //票数
    pthread_mutex_t _mtx;//原生线程库锁,系统级别   
public:
    Ticket()
    :_tickets(100)
    {
        pthread_mutex_init(&_mtx,nullptr);
    }

    bool GetTicket()
    {
      bool ret=true;
      pthread_mutex_lock(&_mtx); //加锁
      //加锁和解锁之间是临界区,线程每次进入临界区都需要先申请锁
      if(_tickets>0)
      {
        usleep(1000); //1s=1000ms 1ms=1000us
        cout<<"我是:"<<pthread_self()<<"我抢到的票是"<<_tickets<<endl;
        _tickets--;
      } 
      else{
        cout<<"票被抢空了!"<<endl; ret=false;
      } 
      pthread_mutex_unlock(&_mtx);//解锁
      return ret;
    }
    ~Ticket()
    {
        pthread_mutex_destroy(&_mtx);
    }
    
};

//线程要执行的函数
void* ThreadRoutine(void* args)
{
    Ticket* t=(Ticket*)args;
    while(true)
    {
        if(!t->GetTicket())//没票了break
        {
            break;
        }
    }

}
int main()
{
    Ticket* t=new Ticket();
    pthread_t tid[5];
    for(int i=0;i<5;i++)
    {
        int *id=new int(i);
        pthread_create(tid+i,nullptr,ThreadRoutine,(void*)t);
    }

    for(int i=0;i<5;i++)
    {
        pthread_join(tid[i],nullptr);
    }

    return 0;
}

互斥量实现原理

我们平时所写的一行代码在底层都会给翻成很多条汇编指令,那么CPU在执行的时候就有可能被切走,为了实现互斥锁的操作,有了swap和exchange指令,作用是把寄存器和内存单元的数据进行交换,只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

lock&unlock伪代码

image-20240501152210076

常见锁概念

死锁

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

死锁的4个必要条件

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

避免死锁

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

银行家算法

银行家算法的基本思想是,在进程提出资源请求时,系统先判断是否分配资源会使得系统进入不安全状态。如果会,则暂时不答应请求,让该进程等待;否则,系统就分配资源。

具体实现时,银行家算法需要知道以下几个参数:

  1. 可用资源向量Available:表示系统中每种资源的可用数量,其长度为m。如果Available[j]=K,则表示系统中可用资源Rj的数量为K。
  2. 最大需求矩阵MaxClaim:这是一个n×m的矩阵,用以表示n个进程对m类资源的最大需求。如果MaxClaim[i][j]=K,则表示进程Pi对资源Rj的最大需求量为K。
  3. 分配矩阵Allocation:这是一个n×m的矩阵,它表示了系统当前已分配给每个进程的各类资源的数量。如果Allocation[i][j]=K,则表示进程Pi当前已分得Rj资源的数量为K。
  4. 需求矩阵Need:这也是一个n×m的矩阵,用以表示每个进程尚需的各类资源数。如果Need[i][j]=K,则表示进程Pi尚需Rj资源数量为K。

在每次进程提出资源请求时,银行家算法都会按照以下步骤进行检查:

  1. 如果Requesti[j]≤Need[i][j],则转向步骤2;否则认为出错,因为它所请求的资源数已超过它所宣布的最大值。
  2. 如果Requesti[j]≤Available[j],则转向步骤3;否则表示尚无足够资源,Pi须等待。
  3. 假设系统可以暂把资源分配给进程Pi,并修改下述数据结构中的数值:
    • Available[j]=Available[j]-Requesti[j];
    • Allocation[i][j]=Allocation[i][j]+Requesti[j];
    • Need[i][j]=Need[i][j]-Requesti[j];
  4. 系统执行安全性算法,检查此次资源分配后系统是否处于安全状态。若安全,才正式分配资源,否则将标志置为“假”,让Pi等待。

安全性算法的目的是判断系统是否处于安全状态。该算法从Available出发,试探着为各个进程分配资源,如果每个进程都能得到足够的资源来完成其任务,则称系统处于安全状态。具体实现时,可以设置一个工作向量Work和一个Finish向量,并使用一个循环来遍历所有进程。在每次循环中,选择一个尚未完成(即Finish[i]=false)且Need[i]≤Work的进程Pi,并执行以下操作:

  1. Work=Work+Allocation[i];
  2. Finish[i]=true;

如果所有进程都能顺利完成(即所有Finish[i]都为true),则系统处于安全状态;否则,系统处于不安全状态。

其他锁

  1. 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。

  2. 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。

    主要采用两种方式:版本号机制和CAS操作,CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试

  3. 自旋锁:不断循环检测锁的状态

    int pthread_spin_lock(pthread_spinlock_t *lock)

  4. 阻塞等待锁:挂起等待 2种锁取决于线程访问临界资源的时间长用阻塞等待锁

线程安全/重入

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

可重入与线程安全联系

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

可重入与线程安全区别

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

常见的线程不安全的情况

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

常见的线程安全的情况

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

常见不可重入的情况

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

常见可重入的情况

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

线程同步

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

竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。

条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

条件变量接口

//初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:cond:要初始化的条件变量 attr:NULL
    
//销毁
int pthread_cond_destroy(pthread_cond_t *cond)
    
//等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:cond:要在这个条件变量上等待  mutex:互斥量
    
//唤醒等待    
int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒所有
int pthread_cond_signal(pthread_cond_t *cond);

可以看到的 pthread_cond_wait()需要给它传一个mutex为什么呢?

  1. 避免死锁:当线程调用 pthread_cond_wait() 时,它首先会释放(unlock)传入的互斥锁,然后等待条件变量。如果没有这个机制,线程可能会持有互斥锁并进入等待状态,这可能导致其他线程无法获取该互斥锁来修改与条件变量相关的条件。这可能导致死锁或其他同步问题。
  2. 确保条件的原子性检查:pthread_cond_wait() 通常与条件变量的条件检查一起使用。线程首先检查某个条件是否满足(例如,队列是否为空或是否已满)。如果条件不满足,线程将调用 pthread_cond_wait() 来等待条件的变化。重要的是,这个条件检查和等待操作必须是原子的,以防止在条件检查和等待之间发生条件的变化。通过将互斥锁传递给 pthread_cond_wait(),线程可以在条件检查和等待之间保持对互斥锁的锁定,确保这两个操作的原子性。
  3. 重新获取互斥锁:当条件变量被其他线程的信号pthread_cond_wait() 或 pthread_cond_wait() 唤醒时pthread_cond_wait() 将自动重新获取传入的互斥锁。这确保了线程在继续执行之前能够重新获得对共享资源的访问权限。
  4. 防止虚假唤醒(Spurious Wakeups):尽管在 POSIX 线程(pthreads)中不保证虚假唤醒,但在其他同步原语中(如某些操作系统的信号量),虚假唤醒是一个可能的问题。虚假唤醒是指线程在没有任何线程调用唤醒函数的情况下被唤醒。通过将互斥锁与条件变量结合使用,即使发生虚假唤醒,线程也可以在继续执行之前重新检查条件,从而确保它不会错误地继续执行。

条件变量使用规范

//等待条件
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);

条件变量测试代码

#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
using namespace std;

pthread_mutex_t mtx;
pthread_cond_t cond;

void* ctrl(void* args)
{
    string name=(char*)args;
    while(true)
    {
        cout<<"master say:begin work"<<endl;
        pthread_cond_signal(&cond);//唤醒条件变量下等待的一个线程
        sleep(3);
    }
}

void* work(void* args)
{
    int number=*(int *)args;
    delete (int*)args;
    while(true)
    {
        pthread_cond_wait(&cond,&mtx);
        cout<<"worker:"<<number<<"is working"<<endl;
    }
}
int main()
{
    pthread_mutex_init(&mtx,nullptr);
    pthread_cond_init(&cond,nullptr);

    pthread_t master;
    pthread_t worker[3];

    //主线程
    pthread_create(&master,nullptr,ctrl,(void*)"boss");

    //创建3个work线程
    for(int i=0;i<3;i++)
    {
        int *number=new int (i);
        pthread_create(worker+i,nullptr,work,(void*)number);
    }

    for(int i=0;i<3;i++){
        pthread_join(worker[i],nullptr);
    }

    pthread_join(master,nullptr);

    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cond);
}

线程总结

线程优点

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

线程缺点

  • 性能损失

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

  • 健壮性降低(一个线程出问题,其他线程也崩了)

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

  • 缺乏访问控制

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

  • 编程难度提高

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

线程异常

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

STL中的容器是否是线程安全的?

STL 默认不是线程安全. 如果需要在多线程环境中安全地使用STL容器,你需要使用某种形式的同步机制,如互斥锁(mutexes)、读写锁(read-write locks)、条件变量(condition variables)或其他同步原语,来确保在任何时候只有一个线程可以修改容器。

值得注意的是,虽然STL容器本身不是线程安全的,但是C++标准库提供了一些工具来帮助处理并发,例如、<condition_variable>、、等头文件中的类和函数。你可以使用这些工具来确保STL容器在多线程环境中的安全使用。

智能指针是否是线程安全的?

智能指针(如 std::unique_ptr, std::shared_ptr, 和 std::weak_ptr)本身并不自动提供线程安全性。这意味着,如果你在多线程环境中使用智能指针,并且多个线程可能同时读写同一个智能指针对象,你仍然需要确保操作的线程安全性。

对于 std::unique_ptr,由于其所有权独占的特性,通常不建议在多线程之间共享。如果你尝试在多线程之间传递 unique_ptr,你需要非常小心,因为这可能导致未定义行为,除非你在传递时确保了独占所有权的安全转移。

std::shared_ptrstd::weak_ptr 设计用于共享所有权的场景。std::shared_ptr 内部使用引用计数来管理对象的生命周期。然而,这个引用计数本身是线程安全的——C++11标准要求 shared_ptrweak_ptr 的引用计数操作必须是线程安全的。这意味着多个线程可以同时对同一个 shared_ptrweak_ptr 实例进行拷贝、赋值或析构,而不会导致数据竞争。

尽管如此,线程安全性仅限于智能指针自身的操作,而不包括它们所指向的对象。如果多个线程访问同一个 shared_ptr 所指向的对象,并且至少有一个线程修改了该对象,那么你需要确保对该对象的访问是线程安全的。

总之,虽然 std::shared_ptrstd::weak_ptr 的引用计数是线程安全的,但在多线程环境中使用智能指针时,仍然需要谨慎处理对智能指针所指向对象的并发访问。

实现线程同步和互斥的方法

常见的实现线程同步的方法

  1. 互斥锁(Mutex)

    使用互斥锁可以确保同一时间只有一个线程能够访问某个资源或代码段。当一个线程获取了互斥锁后,其他试图获取该锁的线程将会被阻塞,直到持有锁的线程释放该锁。

  2. 条件变量(Condition Variable)

    条件变量通常与互斥锁一起使用,允许线程等待某个特定条件成立。线程可以调用wait()(在Java中)或相应的条件变量方法(pthread_cond_wait在C语言中)来等待某个条件,并在条件成立时被唤醒。

  3. 信号量(Semaphore)

    信号量是一个计数器,用于控制同时访问某个资源的线程数量。它允许多个线程并发访问资源,但会限制同时访问的线程数。

  4. 读写锁(Read-Write Lock)

    读写锁允许对共享资源进行更细粒度的控制。多个线程可以同时读取资源,但在写操作时只允许一个线程访问资源。

  5. 阻塞队列(BlockingQueue)

    阻塞队列是一种特殊的队列,它在试图添加元素时如果队列已满,则添加操作会阻塞;在试图获取元素时如果队列为空,则获取操作会阻塞。阻塞队列常用于生产者-消费者模型中的线程同步。

    常见的实现线程互斥的方法

    1. 互斥锁(Mutex)

    2. 信号量(Semaphore)

    3. 读写锁(Read-Write Lock)

    4. 自旋锁(Spinlock)

      自旋锁是一种特殊的互斥锁,当线程尝试获取锁而失败时,它会在一个循环中持续检查锁是否可用,而不是被阻塞或进行上下文切换。这种机制适用于锁被持有的时间很短的情况,因为它避免了线程切换的开销。

      自旋锁通常不是由操作系统或编程语言库直接提供的,而是需要程序员自己实现。

    5. 原子操作(Atomic Operations)

      原子操作是不可中断的操作,即在执行过程中不会被其他线程打断。原子操作通常用于对单个数据项(如整数或指针)进行读取、修改和写入操作,以确保这些操作的原子性。

      在POSIX线程库中,没有直接提供原子操作的函数,但可以使用特定于硬件或编译器的原子操作指令来实现。

生产者消费者模型

同步是指多个线程相互协作,共同完成一个任务,属于线程间工作步调的相互制约。互斥是指多个线程分时访问共享资源。
生产者与消费者问题是描述多个线程协同工作的模型,Dijkstra 为演示信号量而提出的,信号量解决了协同工作中的同步和互斥。

有一个或多个生产者、 一个或多个消费者和一个固定大小的缓冲区,所有生产者和消费者共享这同一个缓冲区。生产者生产某种类型的数据,每次放一个到缓冲区中,消费者消费这种数据,每次从缓冲区中消费一个。同一时刻,缓冲区只能被生产者与消费者一个生产者或消费者使用。

当缓冲区已满时,生产者不能继续往缓冲区中添加数据,当缓冲区为空时,消费者不能在缓冲区中消费数据 。

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

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

BLOCKQUEUE.hpp

#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
using namespace std;

namespace ns_blockqueue
{
    const int default_cap = 5;
    template <class T>
    class BlockQueue
    {
    private:
        queue<T> _bq;             // 阻塞队列
        int _cap;                 // 队列元素上限
        pthread_mutex_t _mtx;     // 保护临界资源的锁
        pthread_cond_t _is_full;  //_bq满了,消费者在该条件变量下等待
        pthread_cond_t _is_empty; //_bq空的,生产者在该条件变量下等待

    private:
        bool IsFull()
        {
            return _bq.size() == _cap;
        }
        bool IsEmpty()
        {
            return _bq.size() == 0;
        }
        void LockQueue()
        {
            pthread_mutex_lock(&_mtx);
        }
        
        void UnlockQueue()
        {
            pthread_mutex_unlock(&_mtx);
        }

        void ProducterWait()
        {
            // 1.调用的时候先把锁释放了,再挂起自己
            // 2.返回的时候,先竞争锁,把锁竞争到了再返回
            pthread_cond_wait(&_is_empty, &_mtx);
        }

        void ConsumerWait()
        {
            pthread_cond_wait(&_is_full, &_mtx);
        }

        void WakeupProducter()
        {
            pthread_cond_signal(&_is_empty);
        }

        void WakeupConsumer()
        {
            pthread_cond_signal(&_is_full);
        }
       
    public:
        //默认最大给5个
        BlockQueue(int cap = default_cap)
            : _cap(cap)
        {
            pthread_mutex_init(&_mtx, nullptr);
            pthread_cond_init(&_is_full, nullptr);
            pthread_cond_init(&_is_empty, nullptr);
        }
        ~BlockQueue()
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_is_full);
            pthread_cond_destroy(&_is_empty);
        }

        // const &:输入  *:输出  &:输入输出
    public:
        //生产函数
        void Push(const T &in)
        {
            LockQueue();

            // 进行条件检测的时候,用循环的方式
            // 当退出循环的时候一定是因为条件不满足
            //进入while一定是满了,满了就等待
            //退出的时候一定是不满的,才能push
            while (IsFull())
            {
                ProducterWait();
            }
            //不满就进入队列
            _bq.push(in);
            WakeupConsumer(); // 通知消费者来消费

            UnlockQueue();
        }

        //消费函数
        void Pop(T *out)
        {
            LockQueue();

            while (IsEmpty())
            {
                ConsumerWait();
            }

            *out = _bq.front();
            _bq.pop();
            WakeupProducter(); // 通知生产者生产

            UnlockQueue();
        }
    };
}

TASK.hpp

#include <iostream>
#include <pthread.h>
using namespace std;
namespace ns_task
{
  class Task
  {
  private:
    int _x;
    int _y;
    char _op; //+-*/%
  public:
    Task() {}
    Task(int x, int y, char op)
        : _x(x), _y(y), _op(op)
    {
    }
    int Run()
    {
      int res = 0;
      switch (_op)
      {
      case '+':
        res = _x + _y;
        break;
      case '-':
        res = _x - _y;
        break;
      case '*':
        res = _x * _y;
        break;
      case '/':
        res = _x / _y;
        break;
      case '%':
        res = _x / _y;
        break;
      default:
        cout<<"bug"<<endl;
        break;
      }
      cout<<"当前任务被:"<<pthread_self()<<"处理"<<_x<<_op<<_y<<"="<<res<<endl;
    }
    int operator()()
    {
      return Run();
    }
    ~Task()
    {}
  };
}

TEST.cc

#include "BlockQueue.hpp"
#include "time.h"
#include <cstdlib>
#include <unistd.h>
#include"Task.hpp"
using namespace ns_blockqueue;
using namespace std;
using namespace ns_task;

void *consumer(void *args)
{
    BlockQueue<Task> *bq = (BlockQueue<Task> *)args;
    while (true)
    {
        // sleep(1);
        // int date = 0;
        // bq->Pop(&date);
        // cout << "消费者消费了一个数据:" << date << endl;

        //处理Task代码
        Task t;
        bq->Pop(&t);
        t();
    }
}

void *producter(void *args)
{
    BlockQueue<Task> *bq = (BlockQueue<Task> *)args;
    char oparr[]="+-*%/";
    while (true)
    {

        // int date = rand() % 10 + 1;
        // cout << "生产者生产了一个数据:" << date << endl;
        // bq->Push(date);

        //处理Task代码
        //1.产生数据
        int x=rand()%512+1;
        int y=rand()%7+1;
        char op=oparr[rand()%5];
        cout<<"生产者产生了一个数据"<<x<<op<<y<<"=?"<<endl;
        Task t(x,y,op);
        //2.发送数据过去
        bq->Push(t);
        sleep(1);
    }
}

// 生产者消费者模型 ---进程间通信的原理
int main()
{
    srand((long long)time(nullptr));
    BlockQueue<Task> *bq = new BlockQueue<Task>();

    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, (void *)bq);
    pthread_create(&p, nullptr, producter, (void *)bq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
}
  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值