Linux学习笔记:线程

news2024/11/20 0:39:11

Linux中的线程

  • 什么是线程
  • 线程的使用
    • 原生线程库
    • 创建线程
    • 线程的id
    • 线程退出
    • 等待线程join
    • 分离线程
    • 取消一个线程
    • 线程的局部存储
    • 在c++程序中使用线程
    • 使用c++自己封装一个简易的线程库
  • 线程互斥(多线程)
    • 导致共享数据出错的原因
    • 互斥锁
    • 关键函数
      • pthread_mutex_t :创建一个锁
      • pthread_mutex_init:初始化一个互斥锁。
      • pthread_mutex_lock:加锁,如果锁已被其他线程加锁,则线程会阻塞直到锁被释放。
      • pthread_mutex_unlock:释放锁,使其他等待的线程有机会获得锁。
      • pthread_mutex_destroy:销毁一个互斥锁。
    • 加锁注意事项
    • 使用C++自己封装一个线程锁
    • 线程死锁
      • 什么是死锁
      • 死锁的四个条件
      • 死锁的代码实例
    • 线程同步
      • 什么是线程同步,作用是什么
    • 条件变量
    • 条件变量的使用
      • 创建一个条件变量 pthread_cond_t
      • 初始化条件变量 pthread_cond_init
      • 等待条件变量 pthread_cond_wait
      • 发送信号 pthread_cond_signal
    • 唤醒所有信号 pthread_cond_broadcast
      • 销毁条件变量 pthread_cond_destroy
    • 生产者消费者模型
    • 伪唤醒
    • 信号量
      • sem_init:初始化一个信号量。
      • sem_destroy:销毁一个信号量。
      • sem_wait:等待信号量。
      • sem_post:释放信号量.
      • sem_trywait:尝试等待信号量。
    • 信号量实现环形队列

什么是线程

线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。在进程的学习中,我们了解到一个可执行文件可以执行多个进程,而线程则是把进程所执行的任务可以再细分成一个或多个执行流来交给CPU执行.
在这里插入图片描述
图片来自必应搜索

Linux系统中没有真正意义上的线程,它是由进程的PCB来模拟的线程,被统一称为轻量级进程(Light weight process) ,因此在底层中,CPU调度的还是一个一个的进程,只不过是这些进程都是轻量级的进程,这样CPU调度起来也更方便,不用再进行数据的转换,调度的还是跟以前一样的进程而已,大概图解如下:
在这里插入图片描述
图片来自必应搜索

之前学习的进程实际上就是单线程的进程,在后续的Linux学习中,完全可以把进程看成包含一个或多个线程(即轻量级进程)

线程的使用

linux系统中查询线程pid:

ps -aL

原生线程库

想要操作进程,那必须使用系统提供的接口,线程也是一样的,而原生线程库定义了操作系统应该提供的一组API,以支持线程创建、同步、通信和控制等功能。
这个库一般是名为pthread的库,该库提供了创建和管理线程所需的函数。因此,我们在对线程进行操作时,一般需要包含头文件 pthread.h

并且,因为是外部库,因此在编译的时候应该找到该库然后添加相应的编译条件进行编译例如:

g++ -o mythread mythread.cc -std=c++11 -lpthread

创建线程

pthread_create()

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

参数说明:

thread:这是一个指向 pthread_t 类型变量的指针,该变量用于保存新创建线程的标识符。

attr:这是一个指向 pthread_attr_t 类型变量的指针,它允许程序员设置新线程的属性,如栈大小、线程优先级等。如果 attr 为 NULL,将使用默认属性。一般都是设置为空

start_routine:这是一个指向函数的指针,该函数将作为新线程的入口点。相当于这个线程需要执行的方法

arg:这是传递给 start_routine 函数的参数。

返回值:

如果线程创建成功,pthread_create 返回 0。
如果在创建线程时发生错误,将返回错误码。

下面是一个创建线程并让线程执行某个方法的代码示例:

#include<iostream>
#include<pthread.h>

void* threadRotine(void* arg)
{
    std::string name = static_cast<char*>(arg);
    std::cout<<"i am "<<name<<" my pthreadid: "<<pthread_self()<<std::endl;
}

int main()
{
    pthread_t tid;
    int status;
    pthread_create(&tid,nullptr,&threadRotine,(void*)"thread-1");
    status = pthread_join(tid,nullptr);
    
    return 0;
}

线程的id

pthread_self();函数返回调用线程的线程ID。

pthread_t my_thread_id = pthread_self();

线程退出

退出一个线程可以让线程执行完任务后自行返回,也可以使用线程终止函数
pthread_exit()用于线程的退出

void* thread_function(void* arg) {
    // 执行任务
    pthread_exit(NULL); // 线程退出
}

可以在线程执行方法结束后调用以结束线程,但是不能用exit()函数,因为exit()是进程的退出函数

等待线程join

线程默认是要被主线程等待的,否则会导致类似进程的僵尸问题
pthread_join()用于等待一个线程终止并获取其退出状态。
这个函数是需要确保主线程在子线程完成其工作之后才继续执行时进行调用。

int pthread_join(pthread_id pthread,void **retval);//

参数说明:

pthread:要等待其终止的线程的线程标识符。
retval:指向 void 指针的指针,用于接收线程退出时的状态信息。

例如下面的代码中就封装了一个线程信息返回的类,并利用pthread_join函数拿到其返回值并进行信息的打印:

#include<iostream>
#include<pthread.h>

//封装线程返回信息的类
class PthreadReturn
{
public:
    PthreadReturn(pthread_t id,const std::string& info,int code)
    :_id(id),_info(info),_code(code)
    {}
public:
    pthread_t _id;
    std::string _info;
    int _code;
};

void* threadRotine(void* arg)
{
    std::string name = static_cast<char*>(arg);
    std::cout<<"i am "<<name<<" my pthreadid: "<<pthread_self()<<std::endl;
    //对返回信息进行传参
    PthreadReturn* ret = new PthreadReturn(pthread_self(),"thread 1",10);
    return ret;
}


int main()
{
    pthread_t tid;
    int status;
    pthread_create(&tid,nullptr,&threadRotine,(void*)"thread-1");
    void* ret = nullptr;
    status = pthread_join(tid,&ret); //拿到返回信息ret
    PthreadReturn* p = static_cast<PthreadReturn *>(ret); //因为返回信息的参数是void** ,因此在这里需要将类型还原成类
    
    std::cout<<p->_id<<","<<p->_info<<","<<p->_code<<std::endl;
    
    delete p;
    return 0;
}

分离线程

线程一旦分离出去,就和当前进程没有任何关系,即便退出了也会被系统回收.
但是一般建议任何程序都以主线程结束
pthread_detach()

pthread_detach(thread_id);

一个线程要么是jion的,要么是detach的,默认是jionable的

取消一个线程

pthread_cancal()

pthread_cancel(thread_id);

如果线程已经被分离了,那么这个线程就可以被取消但不能join

线程的局部存储

__thread 关键字

__thread 类型 变量名;

使用__thread关键字定义的变量就相当于给每个线程都定义了这个变量,因此每个线程在使用这个变量的时候都是单独的,并不是全局变量

在c++程序中使用线程

因为linux中的线程库也是封装的,c++也提供了对这个库的封装,使用线程的头文件thread,当然,因为这是c++封装的pthread,因此底层依然是调用了pthread_create等函数,因此在编译的时候在编译条件那里依然是要加上 -lprhead

	g++ -o $@ $^ -std=c++11 -lpthread

下面用C++提供的线程库来写一个线程相关的示例:

#include<iostream>
#include<thread>

using namespace std;

void myrun()
{
    cout<<"i am a thread"<<endl;
}

int main()
{
    thread t(myrun);
    t.join();

    return 0;
}

当然,因为C++这个线程头文件本身也是对pthread.h库的封装,因此我们也可以自己封装一个简易的线程库

使用c++自己封装一个简易的线程库

Makefile文件: //因为是对pthread.h库进行的封装,因此在编译的时候还是需要链接库

Mypthread:Mypthread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm Mypthread 

Thread.hpp

#pragma once

#include<iostream>
#include<pthread.h>
#include<functional>


using namespace std;

template<class T>
using func_t = function<void(T)>;

template<class T>
class Thread
{
public:
    Thread(func_t<T> func,const string& threadname,T data)
    :_tid(0),_ThreadName(threadname),_isruning(false),_func(func),_data(data)
    {}

    static void* threadRoutine(void* arg)
    {
        Thread* t = static_cast<Thread*>(arg);
        t->_func(t->_data);
        return nullptr;
    }

    bool Start()
    {
        pthread_t id;
        int n = pthread_create(&id,nullptr,threadRoutine,this);
        if(n == 0)
        {
            _isruning = true;
            return true;
        }
        else{
            return false;
        }
    }

    string ThreadName()
    {
        return _ThreadName;
    }

    bool join()
    {
        if(!_isruning) return false;
        void* ret = nullptr;
        int n = pthread_join(pthread_self(),&ret);
        if(n == 0)
        {
            _isruning = false;
            return true;
        }
        return false;

    }

    bool IsRun()
    {
        return _isruning;
    }
    ~Thread()
    {}

private:
    pthread_t _tid;
    string _ThreadName;
    bool _isruning;
    func_t<T> _func;
    T _data;
};

Mythread.cc

#include<iostream>
#include"Thread.hpp"
#include<vector>

using namespace std;

string getThreadname()
{
    char nums[64];
    static int num = 1;
    snprintf(nums,sizeof(nums),"Thread-%d",num++);
    return nums;
}

void myfunc(void* arg)
{
    cout<<"i am a thread"<<endl;
}

int main()
{
 
    vector<Thread<void*>> vt;
    int num = 5;
    for(int i = 0 ; i < num ; i++)
    {
        vt.push_back(Thread<void*>(myfunc,getThreadname(),nullptr));

    }

    for(auto & e:vt)
    {
        cout<<" thread_name: "<<e.ThreadName()
            <<" is  thread_run?: "<<e.IsRun()
            <<endl;
    }
    cout<<"Start:"<<endl;
    for(auto & e:vt)
    {
        e.Start();
        cout<<" thread_name: "<<e.ThreadName()
            <<" is  thread_run?: "<<e.IsRun()
            <<endl;
    }

        cout<<"join:"<<endl;
    for(auto & e:vt)
    {
        e.join();
        cout<<" thread_name: "<<e.ThreadName()
            <<" is  thread_run?: "<<e.IsRun()
            <<endl;
    }


    //Thread tid(myfunc,getThreadname());
    // cout<<"is runing?"<<tid.IsRun()<<" id: "<<tid.Pthread_id()<<endl;
    // tid.Start();
    // cout<<"is runing?"<<tid.IsRun()<<" id: "<<tid.Pthread_id()<<endl;
    // tid.join();
    // cout<<"is runing?"<<tid.IsRun()<<" id: "<<tid.Pthread_id()<<endl;

    return 0;
}


线程互斥(多线程)

当我们创建了多个线程,并且多个线程在对全局变量或者共享区数据进行访问并更改时,可能会出现一些意想不到的问题.
例如,在上面.hpp文件的基础上,写了一个简易的模仿抢票的小程序,在这个程序中有5个线程对1000张票进行抢票的一个动作,每有一个程序抢到一张票,总票数就-1 ,直到总票数为0
运行以下代码:

#include<iostream>
#include"Thread.hpp"
#include<vector>
#include<unistd.h>
#include<cstdio>

using namespace std;

string getThreadname()
{
    char nums[64];
    static int num = 1;
    snprintf(nums,sizeof(nums),"Thread-%d",num++);
    return nums;
}

int ticket = 1000; //全局共享资源

void getTicket(string name)
{
    while(true)
    {
        if(ticket > 0)
        {
            usleep(1000);
            printf("%s get a ticket :%d \n",name.c_str(),ticket);
            ticket--;
        }
        else{
            break;
        }
    }
}

int main()
{
    //pthread_mutex_t mutex;
    //pthread_mutex_init(&mutex,nullptr);

    string name1 = getThreadname();
    Thread<string> t1(getTicket,name1,name1);

    string name2 = getThreadname();
    Thread<string> t2(getTicket,name2,name2);

    string name3 = getThreadname();
    Thread<string> t3(getTicket,name3,name3);

    string name4 = getThreadname();
    Thread<string> t4(getTicket,name4,name4);

    string name5 = getThreadname();
    Thread<string> t5(getTicket,name5,name5);

    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();
    t5.Start();
// 添加延迟以确保线程有机会执行
    sleep(3);
    t1.Join();
    t2.Join();
    t3.Join();
    t4.Join();
    t5.Join();


    return 0;
}

这里我们创建了5个线程,分别代表要去看演唱会抢票的人,大家分别抢票,抢到一张票共享数据ticket就 – ,直到ticket为0,但是,运行结果如下:
在这里插入图片描述

导致共享数据出错的原因

因为 线程的时间片轮转+寄存器的逐步访问 ,这才导致本应该为0的时候就结束抢票的,但是却抢出了负数这样的bug,这是因为以下几步:

  1. 当前线程对自己的线程TCB中所保存票数进行判断
  2. 将自己线程内存中的数据放到寄存器
  3. 寄存器更改
  4. 再把寄存器的数据交给线程内存进行保存

但是一顿操作下来,到其中的某一个步骤的时候,自己的线程时间片到了,CPU直接从当前步骤中断执行下一个线程,这样的话下一个线程已经对共享数据做出更改的时候当前线程却还记录的是自己的数据,这样的话就没法做到对共享数据的同步

在这里插入图片描述
寄存器的逐步访问解释图(来自必应搜索)

因此当我们有了线程互斥锁的概念

互斥锁

互斥锁是一种基本的同步机制,用于保护共享资源,确保同一时间只有一个线程可以访问。

关键函数

因为是线程的锁,因此还是需要用到头文件 <pthread.h>

pthread_mutex_t :创建一个锁

类型通常用于声明互斥锁变量例如:

pthread_mutex_t mutex

当前就已经创建了一个锁

pthread_mutex_init:初始化一个互斥锁。

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

参数:
mutex:指向将要被初始化的互斥锁的指针。
attr:指向互斥锁属性的指针。如果传入NULL,将使用默认属性。

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

使用此函数初始化锁是多线程编程中的标准做法,以确保锁在使用前已正确设置。

pthread_mutex_lock:加锁,如果锁已被其他线程加锁,则线程会阻塞直到锁被释放。

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数:
mutex:指向需要加锁的互斥锁的指针。

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

这个函数是实现线程安全的关键,用于保护临界区(即共享代码区),确保同一时间只有一个线程可以执行临界区代码。

pthread_mutex_unlock:释放锁,使其他等待的线程有机会获得锁。

当一个线程完成其对共享资源的操作后,它应调用此函数来解锁,使其他阻塞(等待这个锁释放的)线程可以继续执行。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数:
mutex:指向需要解锁的互斥锁的指针。

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

pthread_mutex_destroy:销毁一个互斥锁。

当互斥锁不再被使用时,应该调用此函数来释放与互斥锁相关的资源。

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数:
mutex:指向需要销毁的互斥锁的指针。

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

销毁互斥锁是资源回收的重要步骤,避免内存泄漏。

加锁注意事项

1.尽可能的少给代码加锁,因为加锁会让线程在执行某段代码的时候由并行转为串行,会影响效率
2. 一般加锁都是给临界区加锁
3. 申请锁都是程序员自己保证的,因此要格外注意内存泄漏的问题
4. 谁加锁,谁解锁

下面是加了锁,更改后的代码:

#include<iostream>
#include"Thread.hpp"
#include<vector>
#include<unistd.h>
#include<cstdio>

using namespace std;

string getThreadname()
{
    char nums[64];
    static int num = 1;
    snprintf(nums,sizeof(nums),"Thread-%d",num++);
    return nums;
}

int ticket = 1000; //全局共享资源

void getTicket(pthread_mutex_t* mutex)
{
    while(true)
    {
        //加锁
        pthread_mutex_lock(mutex);
        if(ticket > 0)
        {
            usleep(1000);
            printf(" get a ticket :%d \n",ticket);
            ticket--;

        }
        else{   
            //解锁
            pthread_mutex_unlock(mutex);
            break;

        }
        //解锁
        pthread_mutex_unlock(mutex);
    }
}

int main()
{
    //创建锁
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex,nullptr);

    string name1 = getThreadname();
    Thread<pthread_mutex_t*> t1(getTicket,name1,&mutex);

    string name2 = getThreadname();
    Thread<pthread_mutex_t*> t2(getTicket,name2,&mutex);

    string name3 = getThreadname();
    Thread<pthread_mutex_t*> t3(getTicket,name3,&mutex);

    string name4 = getThreadname();
    Thread<pthread_mutex_t*> t4(getTicket,name4,&mutex);

    string name5 = getThreadname();
    Thread<pthread_mutex_t*> t5(getTicket,name5,&mutex);

    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();
    t5.Start();
// 添加延迟以确保线程有机会执行
    sleep(3);
    t1.Join();
    t2.Join();
    t3.Join();
    t4.Join();
    t5.Join();

//销毁锁
    pthread_mutex_destroy(&mutex);


    return 0;
}

在这里插入图片描述
这样的话这个简单的抢票程序就没问题了

使用C++自己封装一个线程锁

封装这个锁的目的是为了更方便的对临界区的代码进行管理,依旧是上方的抢票代码案例:
添加.hpp文件LockGuard.hpp 因为是对锁所做封装,因此还是需要用到pthread.h头文件

#pragma once 

#include<pthread.h>


class Mutex
{
public:
    Mutex(pthread_mutex_t* mutex):_mutex(mutex)
    {}
    void Lock()
    {
        pthread_mutex_lock(_mutex);
    }
    void Unlock()
    {
        pthread_mutex_unlock(_mutex);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t* _mutex;
};

class Guard
{
public:
    Guard(pthread_mutex_t* lock):_mutex(lock)
    {
        _mutex.Lock();
    }
    ~Guard()
    {
        _mutex.Unlock();
    }
public:
    Mutex _mutex;
};

这样写的目的是让我们定义一个锁之后除了对应作用域可以自行解锁和销毁而不用手动的去释放,并且可以根据需要对临界区和非临界区代码使用{}来分割

void getTicketname(string name)
{
    while(true)
    {
        //非临界区代码
        //......


        //临界区代码块  用{}分割
        {
        //加锁:
        Guard Mutex(&mutex);
        if(ticket > 0)
        {
            usleep(1000);
            printf("%s get a ticket :%d \n",name.c_str(),ticket);
            ticket--;
        }
        else{ 
            break;
        }
        }
    }
}

线程死锁

什么是死锁

在多线程环境中,当两个或多个线程相互等待对方释放资源,从而无限期地阻塞彼此的进程,就会发生死锁。这些资源可以是任何东西,如数据、文件或任何由互斥锁保护的资源。

死锁的四个条件

产生死锁的四个必要条件:

  1. 互斥条件:资源至少有一个不能被共享,只能由一个线程占用。
  2. 持有并等待条件:一个线程至少持有一个资源,并等待获取一个当前被其他线程持有的资源。
  3. 非抢占条件:资源不能被强制从一个线程中抢占,只能由持有资源的线程主动释放。
  4. 循环等待条件:涉及的线程之间形成一个环路,每个线程都在等待下一个线程持有的资源。

只要没满足上面的四条,那就都不是死锁,因此,想要不发生死锁,只需要破坏上面四条中的任意一条即可

死锁的代码实例

下面的例子中,两个线程尝试获取两把锁,从而导致死锁:

pthread_mutex_t lock1, lock2;

void* thread1(void* arg) {
    pthread_mutex_lock(&lock1);
    sleep(1); // 确保线程2能锁住lock2
    pthread_mutex_lock(&lock2);
    // 执行任务...
    pthread_mutex_unlock(&lock2);
    pthread_mutex_unlock(&lock1);
    return NULL;
}

void* thread2(void* arg) {
    pthread_mutex_lock(&lock2);
    sleep(1); // 确保线程1能锁住lock1
    pthread_mutex_lock(&lock1);
    // 执行任务...
    pthread_mutex_unlock(&lock1);
    pthread_mutex_unlock(&lock2);
    return NULL;
}

线程同步

什么是线程同步,作用是什么

多线程环境下,线程往往需要读取或修改共享数据。如果对这些共享资源的访问不加以控制,多个线程可能会同时修改同一资源,导致数据的不一致性。例如,当两个线程同时更新同一个账户余额时,如果没有适当的同步措施,最终的账户余额可能会出错.因此,在临界资源使用安全的前提下,让多线程执行具有一定的顺序性,这样做是为了让CPU资源能够更加充分的被利用,这样的情况被称为线程同步.

线程同步是一种机制,它确保两个或更多并发执行的线程在访问共享资源时不会产生冲突。无论是在多核还是单核处理器上,线程同步都是必须的,以避免由于资源竞争引起的数据不一致或应用崩溃等问题。

条件变量

条件变量是用来自动阻塞一个线程,直到某特定条件为真为止。条件变量需要与互斥锁(Mutex)一起工作,以避免竞争条件的发生。
它可以使线程在等待某个条件成立时进入阻塞状态,一旦条件成立,条件变量就会唤醒一个或多个等待的线程。

条件变量的使用

创建一个条件变量 pthread_cond_t

与线程锁一样,想要对条件变量进行操作,首先得有这么个东西才行,类型为: pthread_cond_t :

pthread_cond_t  cond;

初始化条件变量 pthread_cond_init

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

参数:
cond:指向将要被初始化的条件变量。
attr:指定条件变量属性的指针,通常设置为NULL表示默认属性。
返回值:成功返回0;失败返回错误号。

也可以使用以下代码直接 创建+初始化 全局的条件变量:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

等待条件变量 pthread_cond_wait

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

参数:
cond:指向等待的条件变量。
mutex:与条件变量一起使用的互斥锁,调用时必须已被当前线程锁定。
返回值:成功返回0;失败返回错误号。

此函数会释放互斥锁并等待条件变量被触发,触发后重新获得互斥锁继续执行。 即当一个线程没有收到能够运行的信号时,就会从这里跳转到等待队列,等待能够重新拿到互斥锁再继续运行自己的代码

发送信号 pthread_cond_signal

int pthread_cond_signal(pthread_cond_t *cond);

参数:
cond:要触发的条件变量。
返回值:成功返回0;失败返回错误号。

此函数唤醒至少一个等待(被阻塞)在指定条件变量上的线程。即告诉那个线程,你要运行的条件已经达到了,可以运行了.

唤醒所有信号 pthread_cond_broadcast

int pthread_cond_broadcast(pthread_cond_t *cond);

cond:要触发的条件变量。
返回值:成功返回0;失败返回错误号。

此函数唤醒所有等待在指定条件变量上的线程。

销毁条件变量 pthread_cond_destroy

int pthread_cond_destroy(pthread_cond_t *cond);

参数:
cond:要销毁的条件变量。
返回值:成功返回0;失败返回错误号。

生产者消费者模型

生产者和消费者模型是计算机领域中常用的一种资源控制方法,一般情况下,在一个完整的运行过程中,不可能只有单方面的生产者或者消费者的一方,基本都是一边生产资源,一边要拿资源,因此,总结下来生产者消费者模型如下:

  1. 生产者 和 消费者 间存在 竞争 和 互斥 的关系
  2. 消费者 和 消费者 间存在 竞争 和 互斥 的关系
  3. 生产 和 消费 这两个行为之间存在 互斥 和 同步 的关系

下面是用C++封装了一个生产者消费者模型:
BlockQueue.hpp

#pragma once

#include <iostream>
#include <pthread.h>
#include <queue>

int capdefault = 5;

template <class T>
class BlockQueue
{
public:
    BlockQueue(int cap = capdefault) : _capacity(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_c, nullptr);
        pthread_cond_init(&_p, nullptr);
    }

    bool IsFull()
    {
        return _q.size() == _capacity;
    }

    void Push(const T &in) // 生产者
    {
        pthread_mutex_lock(&_mutex);
        while (IsFull())    // 使用循环检查条件,防止伪唤醒
        {
            // 车位满了,等小弟送车进来
            pthread_cond_wait(&_p, &_mutex);
        }
        _q.push(in);
        std::cout << "生产者生产了一个资源 :" << in << std::endl;
        // 生产者告诉消费者该消费了
        pthread_cond_signal(&_c);

        pthread_mutex_unlock(&_mutex);
    }

    bool IsEmpty()
    {
        return _q.empty();
    }

    void Pop(T *out) // 消费者
    {
        pthread_mutex_lock(&_mutex);
        while (IsEmpty())   // 使用循环检查条件,防止伪唤醒
        {
            // 车库没车,等车送进来
            pthread_cond_wait(&_c, &_mutex);
        }
        *out = _q.front();
        _q.pop();
        std::cout << "消费者拿走了一个资源: " << *out << std::endl;
        // 消费者告诉生产者该生产了
        pthread_cond_signal(&_p);

        pthread_mutex_unlock(&_mutex);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_c);
        pthread_cond_destroy(&_p);
    }

private:
    std::queue<T> _q;
    pthread_mutex_t _mutex;
    pthread_cond_t _c;
    pthread_cond_t _p;
    int _capacity;
};

main函数所在文件 Main.cc

#include<iostream>
#include<string>
#include<ctime>
#include"BlockQueue.hpp"
#include"LockGuard.hpp"
#include<unistd.h>
#include<cstdio>

using namespace std;

void* productor(void* args)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);
    //拿到车库钥匙,开始往车库里送车
    while(true)
    {
        //sleep(1);
        //创建数据作为车,把车送入车库
        int data = rand() % 10 + 1 ;
        bq->Push(data);
        
    }
}


void* consumer(void* args)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);
    sleep(2);
    //拿到车库钥匙,开始把车挪走
    while(true)
    {
        sleep(1);
        int data = 0 ;
        bq->Pop(&data);
    }

}


int main()
{
    srand((uint16_t)time(nullptr)^pthread_self());

    BlockQueue<int>* bq = new BlockQueue<int>(); 

    pthread_t c , p;
    pthread_create(&c,nullptr,consumer,bq);
    pthread_create(&p,nullptr,productor,bq);

    pthread_join(c,nullptr);
    pthread_join(p,nullptr);

    return 0;
}

伪唤醒

在上述代码的实现过程中,有一个小细节:
有一句判断 if (IsFull())

    void Push(const T &in) // 生产者
    {
        pthread_mutex_lock(&_mutex);
        if (IsFull())  
        {
            // 车位满了,等小弟送车进来
            pthread_cond_wait(&_p, &_mutex);
        }
        _q.push(in);
        std::cout << "生产者生产了一个资源 :" << in << std::endl;
        // 生产者告诉消费者该消费了
        pthread_cond_signal(&_c);

        pthread_mutex_unlock(&_mutex);
    }

这里的if判断可能在某些情况下造成伪唤醒

伪唤醒是指线程在等待条件变量时,即使没有其他线程显式地发出信号唤醒它,线程也会从等待状态返回。换句话说,线程可能会在没有满足预期条件的情况下被唤醒。

比如,当生产者生产了一大堆资源,然后通知所有的进程过来拿数据(pthread_cand_broadcast),这样的话就会有线程直接跳过判断语句直接唤醒,从而造成风险

处理伪唤醒的正确方法是在等待条件变量返回后,始终重新检查条件。通常的做法是在一个循环中使用条件变量,只有在条件满足时才退出循环。这种模式通常被称为“防伪唤醒模式”。
因此应该将代码中的if 更改为 while

    void Push(const T &in) // 生产者
    {
        pthread_mutex_lock(&_mutex);
        while (IsFull())  // 使用循环检查条件,防止伪唤醒
        {
            // 车位满了,等小弟送车进来
            pthread_cond_wait(&_p, &_mutex);
        }
        _q.push(in);
        std::cout << "生产者生产了一个资源 :" << in << std::endl;
        // 生产者告诉消费者该消费了
        pthread_cond_signal(&_c);

        pthread_mutex_unlock(&_mutex);
    }

这样就正确处理了伪唤醒

信号量

信号量(Semaphore)是一种用于多线程同步和互斥的机制,是一个整数变量,它可以用来控制对共享资源的访问。信号量主要分为两类:二值信号量和计数信号量。

信号量是一个具有非负整数值的计数器,它支持两种原子操作:

P操作(wait):如果信号量值大于零,则将其减一;如果信号量值为零,则阻塞直到信号量值大于零。
V操作(post):将信号量值加一,并唤醒一个等待在该信号量上的线程(如果有的话)。

要使用信号量需要加上头文件 #include<semaphore.h>

信号量类型:sem_t ,用于声明信号量变量。

sem_init:初始化一个信号量。

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

sem:指向信号量对象的指针。
pshared:如果为0,信号量用于线程间同步;如果为非零,信号量用于进程间同步。
value:信号量的初始值。
返回值:成功返回0;失败返回-1,并设置errno。

sem_destroy:销毁一个信号量。

int sem_destroy(sem_t *sem);

sem:指向要销毁的信号量对象的指针。
返回值:成功返回0;失败返回-1,并设置errno。

sem_wait:等待信号量。

如果信号量值大于0,则将其减一;如果信号量值为0,则阻塞直到信号量值大于0。 这个函数通常用于封装P()

int sem_wait(sem_t *sem);

sem:指向信号量对象的指针。
返回值:成功返回0;失败返回-1,并设置errno。

sem_post:释放信号量.

将信号量值加一,并唤醒一个等待在该信号量上的线程(如果有)。
这个函数通常用来封装V()

int sem_post(sem_t *sem);

sem:指向信号量对象的指针。
返回值:成功返回0;失败返回-1,并设置errno。

sem_trywait:尝试等待信号量。

如果信号量值大于0,则将其减一;如果信号量值为0,则立即返回并设置错误码。

int sem_trywait(sem_t *sem);

sem:指向信号量对象的指针。
返回值:成功返回0;如果信号量值为0,返回-1并设置errno为EAGAIN。

信号量实现环形队列

这是一个用vector封装的环形队列,这个环形队列是生产者和消费者的公共资源区,生产者向这个队列里产生资源,消费者从队列里拿走资源,但是这是一个竞争关系.

  1. 生产者生给队列里产了资源,消费者才能拿
  2. 若队列中资源生产满了,那生产者就不能再生产,需要消费者消费了才行
  3. 若队列中已经没有资源了,那消费者需要等待生产者生产

RingQueue.hpp

#pragma once

//环形队列
#include<iostream>
#include<vector>
#include<semaphore.h>

const int defaultsize = 5;

template<class T>
class RingQueue
{
private:
    void P(sem_t& sem)
    {
        sem_wait(&sem);
    }

    void V(sem_t &sem)
    {
        sem_post(&sem);
    }
public:
    RingQueue(int size = defaultsize):_ringQueue(size),_size(size),_p_step(0),_c_step(0)
    {
        sem_init(&_space,0,size); //空间资源一开始就有5个
        sem_init(&_data,0,0); //数据资源因为消费者还没生产,因此还没有
    }

    void Push(const T& in)
    {
        P(_space);
        _ringQueue[_p_step] = in;
        _p_step++;
        _p_step %= _size;
        V(_data);
    }

    void Pop(T* out)
    {
        P(_data);
        *out = _ringQueue[_c_step];
        _c_step++;
        _c_step %= _size;
        V(_space);
    }

    ~RingQueue()
    {
        sem_destroy(&_space);
        sem_destroy(&_data);
    }

private:
    std::vector<T> _ringQueue;
    int _size;  //环形队列大小

    int _p_step;  //生产者的位置
    int _c_step;  //消费者的

    sem_t _space; //生产者需要的空间
    sem_t _data;  //消费者需要的数据
};

Main.cc

#include<iostream>
#include"RingQueue.hpp"
#include<pthread.h>
#include<unistd.h>

void* productor(void* args)
{
    RingQueue<int>* rq = static_cast<RingQueue<int>*>(args); 
    while(true)
    {  
        int data = rand()%10;
        rq->Push(data);
        std::cout<<"i am productor :"<<data<<std::endl;
    }

}

void* consumer(void* args)
{
    RingQueue<int>* rq = static_cast<RingQueue<int>*>(args); 
    while(true)
    {
        sleep(1);
        int data = 0;
        rq->Pop(&data);
        std::cout<<"i am comsumder  i get a data:"<<data<<std::endl;
    }

}


int main()
{
    srand((uint64_t)time(0)^pthread_self());
    RingQueue<int>* rq = new RingQueue<int>();

    pthread_t p,c;
    pthread_create(&p,nullptr,productor,rq);
    pthread_create(&c,nullptr,consumer,rq);

    pthread_join(p,nullptr);
    pthread_join(c,nullptr);

    delete rq;

    return 0;
}

Makefile

testmain:Main.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -rf testmain

暂时完结

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

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

相关文章

[答疑]不开发系统只做领域建模,可以画冗余关联吗

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 Seven 2024-5-22 11:54 您书里面说&#xff0c;可以计算的不是关联。我有个疑问&#xff0c;要是不考虑开发系统&#xff0c;只是做领域建模&#xff0c;这个图是否可以这样画&#x…

构建高效AI代理:单代理与多代理架构的策略与挑战

AI代理架构正成为实现复杂任务自动化的关键技术。这些代理不仅需要处理信息、做出决策&#xff0c;还要能够与外部环境进行交互。随着ChatGPT等生成式AI应用的兴起&#xff0c;研究者们开始探索下一代AI应用&#xff0c;其中AI代理的角色愈发重要。单代理架构以其简洁高效的特点…

数据结构——栈(详细分析)

目录 &#x1f349;引言 &#x1f349;栈的本质和特点 &#x1f348;栈的基本操作 &#x1f348;栈的特点 &#x1f34d;后进先出 &#x1f34d;操作受限 &#x1f34d;动态调整 &#x1f348;栈的优缺点 &#x1f34d;优点 &#x1f34d;缺点 &#x1f349;栈的应用…

AI绘画ComfyUI 进阶教程 | 字节最强换脸插件PuLID 详解,还请收藏!

大家好&#xff0c;我是小强 这应当算作是小编分享的换脸工具系列中的又一力作&#xff0c;从最初的roop&#xff0c;到之后的ReActor&#xff0c;再到备受欢迎的InstantID&#xff0c;以及今日重点介绍的字节开源产品——PuLID。 提及PuLID&#xff0c;首要原因并非仅仅在于…

[xx点评完结]——白马点评完整代码+rabbitmq实现异步下单+资料,免费

项目所有功能已测&#xff0c;均可以跑通&#xff0c;Jmeter和RabbitMQ也都测了。 项目源码:dianpinghui: 仿黑马点评项目 资料: https://pan.baidu.com/s/1kTCn9PxgeIey90WgM4KRqA?pwdn66b 对佬有帮助可以给个star哈&#xff0c;感谢&#x1f339;&#x1f339;&#x1f3…

QQ个性网空间日志网站模板源码

QQ个性网空间日志网站模板源码自带后台登录设置&#xff0c;适用于博客、文章、资讯、其他类网站内容使用。模板自带eyoucms内核&#xff0c;原创设计、手工书写DIVCSS&#xff0c;完美兼容IE7、Firefox、Chrome、360浏览器等;主流浏览器;结构容易优化;多终端均可正常预览。由于…

c++|多态

c|多态 1 多态的概念2 多态的定义及其实现2.1 满足多态的条件2.2 虚函数2.3 虚函数的重写2.4 析构函数适合加virtural吗2.4 C11 override 和 final2.5 三个概念的对比 3 多态的原理4 抽象类4.1 概念4.2 纯虚函数 1 多态的概念 多态的概念&#xff1a;通俗来说&#xff0c;就是…

英伟达的GPU(3)

上节内容&#xff1a;英伟达的GPU(2) (qq.com) 书接上文&#xff0c;上文我们讲到CUDA编程体系和硬件的关系&#xff0c;也留了一个小问题CUDA core以外的矩阵计算能力是咋提供的 本节介绍一下Tensor Core 上节我们介绍了CUDA core&#xff0c;或者一般NPU&#xff0c;CPU执行…

Android中华为手机三态位置权限申请理解

博主前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住也分享一下给大家&#xff0c; &#x1f449;点击跳转到教程 前言&#xff1a; 使用的华为MATE 20,Android10的系统。 <!--精准定位权限&#xff0c;如&#xff1a;…

建议收藏 | 2023年生物学类SCI期刊影响因子最新预测,Molecular Plant遥遥领先

公众号&#xff1a;生信漫谈&#xff0c;获取最新科研信息&#xff01; 建议收藏 | 2023年生物学类SCI期刊影响因子最新预测&#xff0c;Molecular Plant遥遥领先https://mp.weixin.qq.com/s/tFINUzZ1l4H9x1HWTq1kFg 2023年生物学类SCI期刊影响因子最新预测&#xff0c;Molecu…

【基于springboot+vue的房屋租赁系统】

介绍 本系统是基于springbootvue的房屋租赁系统&#xff0c;数据库为mysql&#xff0c;可用于日常学习和毕设&#xff0c;系统分为管理员、房东、用户&#xff0c;部分截图如下所示&#xff1a; 部分界面截图 用户 管理员 联系我 微信&#xff1a;Zzllh_

linux父进程fork出子进程后,子进程为何首先需要close文件描述符。

在linux c/c编程时&#xff0c;父进程fork出子进程后&#xff0c;子进程经常第一件事就是close掉所有的文件描述符&#xff1b;为何需要这样做&#xff0c;本文用一个例子进行简单说明。 考虑到一种情况&#xff0c;父进程创建了tcp服务端套接字&#xff0c;并且listen&#x…

6.中断管理

一、简介 中断是 CPU 的一种常见特性&#xff0c;中断一般由硬件产生&#xff0c;当中断发生后&#xff0c;会中断 CPU 当前正 在执行的程序而跳转到中断对应的服务程序种去执行&#xff0c;ARM Cortex-M 内核的 MCU 具有一个 用于中断管理的嵌套向量中断控制器&#xff08;NV…

Qt 在windows下显示中文

Qt在windows平台上显示中文&#xff0c;简直是一门玄学&#xff0c;经过测试&#xff0c;有如下发现&#xff1a; 1&#xff0c; 环境&#xff1a;Qt 5.15.2 vs2019 64位 win11系统 默认用Qt 创建的文件使用utf-8编码格式&#xff0c;此环境下 中文没有问题 ui->textE…

运用HTML、CSS设计Web网页——“西式甜品网”图例及代码

目录 一、效果展示图 二、设计分析 1.整体效果分析 2.头部header模块效果分析 3.导航及banner模块效果分析 4.分类classify模块效果分析 5.产品展示show模块效果分析 6.版权banquan模块效果分析 三、HTML、CSS代码分模块展示 1. 头部header模块代码 2.导航及bann…

一条命令安装Metasploit Framework

做安全渗透的人都或多或少的使用kali-Linux系统中msfconsole命令启动工具&#xff0c;然而也经常会有人遇到这样那样的问题无法启动 今天我们就用一条命令来重新安装这个工具 curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/met…

Proteus仿真小技巧(隔空连线)

用了好几天Proteus了.总结一下使用的小技巧. 目录 一.隔空连线 1.打开添加网络标号 2.输入网络标号 二.常用元件 三.运行仿真 四.总结 一.隔空连线 引出一条线,并在末尾点一下. 1.打开添加网络标号 选择添加网络标号, 也可以先点击按钮,再去选择线(注意不要点端口) 2.…

PTT票据传递攻击

一. PTT票据传递攻击原理 1.PTT介绍 PTT(Pass The Ticket)&#xff0c;中文叫票据传递攻击&#xff0c;PTT 攻击只能用于kerberos认证中,NTLM认证中没有&#xff0c; PTT是通过票据进行认证的。 进行票据传递&#xff0c;不需要提权&#xff0c;域用户或者system用户就可以 …

2024上海初中生古诗文大会倒计时4个月:单选题真题解析(持续)

现在距离2024年初中生古诗文大会还有4个多月时间&#xff0c;我们继续来看10道选择题真题和详细解析&#xff0c;以下题目截取自我独家制作的在线真题集&#xff0c;都是来自于历届真题&#xff0c;去重、合并后&#xff0c;每道题都有参考答案和解析。 为帮助孩子自测和练习&…

【JS】并发控制

需求 控制网络请求并发数控制并发按顺序返回结果 码 /** * 控制并发 * param {Function} fn 逻辑处理函数 * param {Array} arr 发送的数据 * param {Number} [max3] 并发数 默认3 * param {Number} [orderfalse] 按顺序返回执行结果 默认false * param {Number} [retry1] 重试…