线程与同步

news2024/11/25 12:28:07

目录

一,线程概念

1,什么是线程

2,Linux 下的线程

3,POSIX 线程库

二,线程同步

1,数据不一致问题

2,如何解决这类问题

3,死锁

三,线程同步的运用

1,生产者-消费者问题(producer-consumer)

2,线程池

3,LeetCode


一,线程概念

1,什么是线程

我们先来简单的回顾一下什么是进程:进程就是正在进行的程序,是程序执行时的一个实例。我们可以把它看作是充分描述程序已经执行到何种程度的数据结构的汇集。从内核的观点来看,进程就是担任分配系统资源(例如 CPU 时间,内存资源等)的实体。

线程(thread)就是一个进程里的一个执行序列(或者说是控制序列)。每个进程都至少有一个线程,不过经常会存在在同一个地址空间中并发的运行多个控制线程的情况。

为什么在一个进程中要有多个控制线程呢?因为这样我们就可以把进程设计成能够在同一时间段内能够做多件事情,每个线程都去处理各自独立的任务。

这样设计有很多好处,例如:

① 如果我们采用多进程,那必须通过 OS 提供的复杂机制才能实现某些资源的共享,而在一个进程内的多线程可以自动的共享一个地址空间与文件描述符等资源。

② 从 OS 的角度看,相比进程间的切换,线程之间的切换要轻松很多,创建一个线程的代价也要比创建一个进程的代价小。

③ 可以提高程序的吞吐量,改善程序的响应时间。使用多进程,相互独立的任务的处理就可以交叉的进行(比如我们可以让一个线程去与用户交互,另一个线程去做相关数据的计算工作)。

2,Linux 下的线程

Linux 实现线程的机制非常独特。从内核的角度来看,它并没有线程这个概念。Linux 把所有的线程都当做进程来实现。内核并没有去设计特别的调度算法或是定义特别的数据结构来表示线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都与进程一样,都拥有一个属于自己的 task_struct。所以在内核看来,线程就像是一个普通的进程。Linux 把线程称为轻量级进程(lightweight process,LWP)。

在 Linux 中,进程是资源分配的基本单位,而线程是 CPU 进行调度的基本单位。虽然线程共享了进程的很多资源,但是每个线程也都有自己的一些独立的数据:线程 ID,独立的栈空间,信号屏蔽字,调度优先级,errno,一组寄存器。

这里说明一下为什么线程需要有自己的独立栈空间。每个线程都有自己独立的堆栈,供各个被调用但是还没有被返回的函数使用。例如:在一个线程中函数A调用了函数B,函数B又去调用了函数C,那么当函数C执行时,供 A,B,C 使用的栈帧就会被全部储存在这个栈中。通常每个线程都会去调用自己相关的函数,从而有一个与其他线程不同的执行历史,所以每个线程都需要有自己的堆栈来维护这个执行历史。

3,POSIX 线程库

为了实现可移植的线程程序,IEEE 定义了相关的标准(POSIX 标准),它定义的线程包叫做 pthread,大部分 UNIX 系统都支持该标准。与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以 "pthread_" 打头。想要使用这些函数库,需要引入头文件 <pthread.h> 。并且要在编译选项中使用 "-lphread" 来 动态链接 pthread 线程库。前面有提到每个线程都有自己独立的栈空间,而这个栈空间就是由线程库来维护的。
下面来简单罗列一些相关的函数,具体用法就不在这里详细介绍了
① 线程标识
// 比较两个线程的 ID 是否相同, 相同返回非零值,否则返回 0
int pthread_equal(pthread_t tid1, pthread_t tid2);
// 返回当前线程的 ID
pthread_t pthread_self();
② 线程创建
// 创建一个线程, 成功返回 0, 否则返回错误码
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);

③ 线程终止

// 终止线程
void pthread_exit(void *value_ptr);
// 取消一个正在执行的线程, 成功返回 0, 失败返回错误码
int pthread_cancel(pthread_t thread);
// 等待线程结束, 成功返回 0, 失败返回错误码
int pthread_join(pthread_t thread, void **value_ptr);

进程与线程的相关函数的比较

进程线程描述
forkpthread_create创建新的控制流
exitpthread_exit从现有的控制流退出
getpidpthread_self获取控制流的 ID
waitpidpthread_join在当前控制流中获得其他控制流的退出状态
abortpthread_cancel请求控制流的非正常退出

④ 分离线程

// 分离线程, 成功返回 0, 失败返回错误码
int prhead_detach(pthread_t tid);
// 如果不关心线程的返回值, 不想去手动调用 join, 这个时候我们可以告诉系统,
// 当线程退出时,自动释放线程资源。

二,线程同步

线程同步指的是通过各种机制(如互斥锁、条件变量、信号量等)来确保这些进程或线程按照既定的顺序或条件来访问共享资源,以避免数据不一致等问题。

1,数据不一致问题

什么是数据不一致问题?我们来看两个例子。

第一个例子:两个线程读写相同变量

线程A读取这个变量然后为这个变量赋值,但是赋值操作需要两个CPU时钟周期。当线程B在线程A为变量赋值的两个时钟周期之间读取这个变量时,就会读取到我们不期望的结果。

第二个例子:两个线程试图在同一时间修改同一变量

我们想对一个 int 类型的变量 i 进行 ++ 操作,虽然 ++i 在我们看来只是一条语句,但是对于机器来说 ++i 其实对应着三条指令:

① 将变量 i 放入寄存器

② 将寄存器中的变量值加1

③ 把寄存器中的值写回变量 i 中

2,如何解决这类问题

pthread 库中提供了一些机制来帮助我们实现同步,接下来将介绍一下部分的机制:

① 互斥量

我们可以通过使用 pthread 的互斥接口保护数据,确保同一时间只有一个线程访问数据。互斥
量(mutex)从本质上说是一把锁
,在访问共享资源前对互斥量进行加锁,在访问完成后释放
互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直
到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程
都会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其他线程将会看到互斥
锁依然被锁住,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以
向前执行。

相关接口:

互斥变量用 pthread_mutex_t 数据类型来表示,在使用互斥变量之前,必须首先对它进
行初始化,可以把它置为常量 PTHREAD_MUTEX_INITIALIZER 也可以通过调 pthread_mutex_init 函数进行初始化。如果动态地分配互斥量,那么在释放内存前需要调用pthread_mutex_destroy。

初始化与销毁:

// 可以使用静态分配来初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

// 也可以动使用态分配来初始化, 默认情况下 attr 可以设置为 nullptr
int pthread_mutex_init(pthread_mutex t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 销毁
int pthread mutex destroy(pthread_mutext *mutex);

// 成功返回 0, 否则返回错误码

对互斥量进行加锁,需要调用 pthread_mutex_lock,如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,需要调用 pthread_mutex_unlock。

加锁与解锁:

// 加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 如果线程不希望被阻塞,它可以使用 pthread_mutex_trylock 尝试对互斥量进行加锁。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

// 成功返回 0, 否则返回错误码

对于在上一部分提到的两个问题,我们只需要把 write 操作和 ++i 操作给锁住,放入临界区即可解决。

为了实现互斥锁操作,大多数体系结构都提供了 swap 或 exchange 指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,所以就能够保证原子性。即使是多处理器平台,访问内存的时钟周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待时钟周期。 现在我们可以给出一个简单实现 lock 和 unlock 的伪代码:

lock:
    movb $0, %al
    xchgb %al, mutex    // 交换操作
    if(a1寄存器的内容>0){
    	return 0;
    }
    else{
    	挂起等待;
    }
    goto lock;


unlock :
    movb $1, mutex
    唤醒等待 mutex 的线程;
    return 0;

② 条件变量

条件变量是线程可用的另一种同步机制,通常与 mutex 一起使用。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件本身是由互斥量保护的,线程在改变条件状态前必须首先锁住互斥量,其他线程在获得互斥量之前不会察觉到这种改变,因为必须锁定互斥量以后才能计算条件。

相关接口:

条件变量使用之前必须首先进行初始化,条件变量用 pthread_cond_t 数据类型来表示。在使用条件变量之前,必须首先对它进行初始化,可以把它置为常量 PTHREAD_COND_INITIALIZER 也可以通过调 pthread_cond_init 函数进行初始化。如果动态地分配条件变量,那么在释放内存前需要调用 pthread_cond_destroy。

初始化与销毁:

// 可以使用静态分配来初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER

// 也可以动使用态分配来初始化, 默认情况下 attr 可以设置为 nullptr
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
// 销毁
int pthread_cond_destroy(pthread_cond* cond);

// 成功返回 0, 否则返回错误码

等待条件满足:

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

int pthread_cond_timedwait(pthread_cond_t *restrict cond
pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);

// 成功返回 0, 否则返回错误码

我们需要传递给 pthread_cond_wait 一个互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。至于为什么我们要传入一个被锁住的 mutex 变量给 pthread_cond_wait ?我们稍后再提。

有两个函数可以用于通知线程条件已经满足。pthread_cond_signal 函数将唤醒等待该条件的某个线程,而 pthread_cond_broadcast 函数将唤醒等待该条件的所有线程。

唤醒等待:

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

// 成功返回 0, 否则返回错误码

下面来说说为什么 pthread_cond_wait 函数需要一个被锁住的 mutex 变量:

① 调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。

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

条件变量使用的规范:

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

这里再来说说为什么上述代码中用的是 while 而不是 if。因为我们要避免假唤醒。这是多线程程序中的一个现象,其中线程可能无明显原因地被唤醒。如果只使用 if 检查条件,当线程因为假唤醒而醒来,它会继续执行后面的代码,即使条件实际上并未满足。使用 while 循环可以确保在每次唤醒时重新检查条件,以保证代码的健壮性。

// 给条件发送信号
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

还有一些其他的同步机制,例如读写锁、信号量,在这里就不一一介绍了。

3,死锁

在很多情况下,进程或者是线程需要互斥性的访问(独占)某些资源,这种情况下就非常容易发生死锁。那什么是死锁呢?我们来举个例子:线程1需要申请资源A和B,线程2也需要申请资源A和资源B。线程1申请到了资源A并且对资源A上了锁,线程2申请到了资源B并且对资源B上了锁。当线程1申请资源B,线程2申请资源A,这时就会发生死锁。这两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就产生了死锁。

下面是死锁的规范定义:如果一个线程集合中的每个线程都在等待只能由该线程集合中的其他线程才能引发的事件,那么该线程集合就是死锁的。

换言之,这一死锁线程集合中的每一个线程都在等待另一个死锁的线程已经占有的资源。但是由于所有线程都不能运行,它们中的任何一个都无法释放资源,所以没有一个线程可以被唤醒。

死锁的四个必要条件

① 互斥条件:一个资源只能被一个执行流使用
② 占有和等待条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
③ 不可抢占条件:已经分配给一个执行流的资源不能强制性地被抢占,它只能被占有它的执行流显式地释放。
④ 环路等待条件:死锁发生时,系统中一定有由两个或两个以上的执行流组成的一条环路,该环路中的每个执行流都在等待着下一个执行流所占有的资源。


死锁发生时,以上四个条件一定是同时满足的。如果其中任何一个条件不成立,死锁就不会发生。

对死锁进行建模

这里简单提一下处理死锁的四个策略:

① 忽略该问题。
② 检测死锁并恢复:检测死锁是否发生,一旦发生死锁,采取行动解决问题
③ 仔细对资源进行分配,动态地避免死锁。
④ 预防死锁:通过破坏引起死锁的四个必要条件之一,防止死锁的产生。

三,线程同步的运用

1,生产者-消费者问题(producer-consumer)

作为线程同步的一个例子,这里考虑生产者-消费者问题,也称作有界缓冲区(bounded-buffer)问题。两个进程共享一个公共的固定大小的缓冲区。其中一个是生产者,将信息放入缓冲区;另一个是消费者,从缓冲区中取出信息。(也可以把这个问题一般化为 m 个生产者和 n 个消费者问题,但是为了简化解决方案这里就暂时只讨论一个生产者和一个消费者的情况)

这里的问题在于当缓冲区已满,而此时生产者还想向其中放入一个新的数据项的情况。其解决办法是让生产者睡眠,等消费者从缓冲区中取出一个或多个数据项时再唤醒它。类似地,当消费者试图从缓冲区中取数据而发现缓冲区为空时,消费者就睡眠,直到生产者向其中放入一些数据时再将其唤醒。

这里采用的是模拟阻塞队列(blocking queue)来当作缓冲区的生产消费者模型:

阻塞队列与普通队列区别在于:当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出( 以上的操作都是基于不同的线程来说的,线程在对阻塞队列进行操作时会被阻塞)
pc_problem.cc
#include <iostream>
using namespace std;

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

#include "blocking_queue.hpp"


void* producer(void* arg){
    Blocking_Queue<string>* ptr_bq = static_cast<Blocking_Queue<string>*>(arg);
    int i = 0;
    while(true){
        string data = "data-" + to_string(i);
        ++i;
        ptr_bq->put(data);
        cout << "生产者已将 " << data << " 放入队列" << endl;
        usleep(rand() % 50000);
    }
}


void* consumer(void* arg){
    Blocking_Queue<string>* ptr_bq = static_cast<Blocking_Queue<string>*>(arg);
    while(true){
        string data = ptr_bq->take();
        cout << "消费者已得到 " << data << endl;
        usleep(rand() % 100000);
    }
}


int main(){
    srand(time(nullptr));  
    Blocking_Queue<string> bq(10);

    pthread_t pro_thread, con_thread;
    pthread_create(&pro_thread, nullptr, producer, &bq);
    pthread_create(&con_thread, nullptr, consumer, &bq);

    pthread_join(pro_thread, nullptr);
    pthread_join(con_thread, nullptr);

    return 0;
}

blocking_queue.hpp

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

template<class T>
class Blocking_Queue{
    static const size_t SIZE = 10;  // 默认大小
    typedef T data_type;

private:
    std::queue<data_type> _bq;
    size_t _capacity;               // 阻塞队列的容量
    pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_cond_t _consume = PTHREAD_COND_INITIALIZER;
    pthread_cond_t _product = PTHREAD_COND_INITIALIZER;

public:
    Blocking_Queue(size_t capacity = SIZE): _capacity(capacity){}

    void put(const data_type& data){
        lock();
        while(full()){
            producer_wait();
        }
        _bq.push(data);
        if(_bq.size() == 1){
            wake_consumer();
        }
        unlock();
    }

    data_type take(){
        lock();
        while(empty()){
            consumer_wait();
        }
        data_type data = _bq.front();
        _bq.pop();
        if(_bq.size() == _capacity - 1){
            wake_producer();
        }
        unlock();
        return data;
    }

private:
    void lock(){ pthread_mutex_lock(&_mutex); }
    void unlock(){ pthread_mutex_unlock(&_mutex); }

    // 生产者 等待 消费者去消费队列中的数据
    void producer_wait(){ pthread_cond_wait(&_consume, &_mutex); }
    // 消费者 等待 生产者生产数据到队列中
    void consumer_wait(){ pthread_cond_wait(&_product, &_mutex); }

    // 唤醒消费者, 告诉消费者此时队列中已经有产品了
    void wake_consumer(){ pthread_cond_signal(&_product); }
    // 唤醒生产者, 告诉生产者此时队列已经有空位了
    void wake_producer(){ pthread_cond_signal(&_consume); }

    bool full(){ return _bq.size() == _capacity; }
    bool empty(){ return _bq.size() == 0; }

};

makefile

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

部分运行结果:

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

这样设计有以下好处:

① 将生产者与消费者解耦
② 支持并发
③ 支持生产者与消费者的忙闲不均

2,线程池

接下来是第二个线程同步的例子:线程池。线程池是一种基于池化技术的线程使用模式,用于减少在多线程程序中创建和销毁线程的开销。线程池中维护了一组已经创建但处于空闲状态的线程,当有新的任务到来时,可以直接利用这些现成的线程来执行任务,而不是每次任务到来时再创建新线程。线程池不仅可以提高程序性能,还能提高系统的稳定性。

线程池的核心组成:

① 任务队列:一个用于存放待处理任务的队列。新的任务会被加入到这个队列中等待执行。
② 工作线程:线程池中的线程,负责从任务队列中取出任务并执行。
③ 线程池管理器:负责管理线程池中的线程,包括线程的创建、销毁和任务分配等。

线程池的主要优点:

① 降低资源消耗:通过重复利用已创建的线程,减少线程创建和销毁的次数,从而降低资源消耗。
② 提高响应速度:当任务到来时,无需等待新线程的创建即可立即执行,从而提高响应速度。
③ 提高线程的可管理性:线程是稀缺资源,通过线程池可以有效进行统一分配、调优和监控,避免创建无限多的线程而耗尽系统资源。

其实线程池就是一个单生产者与多消费者的模型。

main_thread.cc

#include <iostream>
#include "task.hpp"
#include "thread_pool.hpp"
#include <unistd.h>


int main(){
    Thread_Pool<Task> tp;
    while(true){

        // 获取任务
        int task_idx = rand() % Task::tasks_size();
        Task task(task_idx);

        // 将任务交给线程池
        tp.tasks_push(task);

        //sleep(1);
    }

    return 0;
}

task.hpp

#pragma once
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>


enum
{
    SUCCEED = 0,
    ERROR = 1,
};

// 一个简易的任务类
class Task
{
private:

    static std::vector<std::function<void()>> _tasks;   // 默认任务列表
    int _task_idx;
    int _exit_code = SUCCEED;

public:
    Task(int task_idx) : _task_idx(task_idx) {}

    // 添加新的任务
    void push_task(std::function<void()> task) { _tasks.push_back(task); }

    int get_exit_code() { return _exit_code; }

    void run()
    {
        if (_task_idx < _tasks.size())
        {
            _tasks[_task_idx]();
        }
        else
        {
            _exit_code = ERROR;
        }
    }

    static size_t tasks_size() { return _tasks.size(); }
};


// 一个简易的任务列表
std::vector<std::function<void()>> Task:: _tasks = {   
        []()
        {
            std::cout << "task-0 is runing" << std::endl;
            sleep(1);
        },
        []()
        {
            std::cout << "task-1 is runing" << std::endl;
            sleep(1);
        },
        []()
        {
            std::cout << "task-2 is runing" << std::endl;
            sleep(1);
        },
        []()
        {
            std::cout << "task-3 is runing" << std::endl;
            sleep(1);
        },
        []()
        {
            std::cout << "task-4 is runing" << std::endl;
            sleep(1);
        },
        []()
        {
            std::cout << "task-5 is runing" << std::endl;
            sleep(1);
        },
    }; 

thread_pool.hpp

#pragma once
#include <iostream>
#include <pthread.h>
#include <vector>
#include <queue>
#include <string>

template <class T>
class Thread_Pool
{
private:
    // 与线程相关的数据
    struct Thread_Data
    {
        pthread_t tid;
        std::string name;
    };

    typedef T task_type;

    // 静态成员
    static const size_t T_SIZE = 10;   // 默认的线程个数
    static void *routine(void *);      // 线程创建启动后默认要执行的函数

private:
    // 成员变量
    pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_cond_t _cond = PTHREAD_COND_INITIALIZER;
    std::vector<Thread_Data> _threads; // 线程数组
    std::queue<task_type> _tasks;      // 任务队列

public:
    Thread_Pool(size_t size = T_SIZE) : _threads(size)
    {
        for (int i = 0; i < _threads.size(); ++i)
        {
            pthread_create(&_threads[i].tid, nullptr, routine, this);
            _threads[i].name = "thread-" + std::to_string(i);
        }
    }


public:
    // 锁相关的操作
    void lock() { pthread_mutex_lock(&_mutex); }
    void unlock() { pthread_mutex_unlock(&_mutex); }
    void thread_sleep() { pthread_cond_wait(&_cond, &_mutex); }
    void thread_weak() { pthread_cond_signal(&_cond); }

    // 任务队列相关的操作
    void tasks_push(const task_type& task)
    {
        lock();
        _tasks.push(task);
        thread_weak();
        unlock();
    }
    task_type tasks_pop()
    {
        task_type tmp = _tasks.front();
        _tasks.pop();
        return tmp;
    }
    bool tasks_empty() { return _tasks.empty(); }
    bool tasks_size() { return _tasks.size(); }

    // 线程相关操作
    std::string get_thread_name(pthread_t tid)
    {
        for (const auto &thread : _threads)
        {
            if (thread.tid == tid)
                return thread.name;
        }
        return "none";
    }

};



template <class T>
void *Thread_Pool<T>::routine(void *args)
{
    Thread_Pool<T> *tp = static_cast<Thread_Pool<T>*>(args);
    tp->lock();
    while (true)
    {
        while (tp->tasks_empty())
        {
            tp->thread_sleep();
        }
        std::cout << tp->get_thread_name(pthread_self()) << "已获取任务" << std::endl;
        tp->tasks_pop().run();
    }
    tp->unlock();
}

makefile

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

部分运行结果

3,LeetCode

这里再推荐三道 LeetCode 来让大家感受一下怎么去同步线程

1114. 按序打印 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/print-in-order/description/

class Foo {
    pthread_mutex_t _mutex1 = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_t _mutex2 = PTHREAD_MUTEX_INITIALIZER;

public:
    Foo() {
        pthread_mutex_lock(&_mutex1);
        pthread_mutex_lock(&_mutex2);
    }

    void first(function<void()> printFirst) {
        // printFirst() outputs "first". Do not change or remove this line.
        printFirst();
        pthread_mutex_unlock(&_mutex1);
    }

    void second(function<void()> printSecond) {
        pthread_mutex_lock(&_mutex1);
        // printSecond() outputs "second". Do not change or remove this line.
        printSecond();
        pthread_mutex_unlock(&_mutex2);
    }

    void third(function<void()> printThird) {
        pthread_mutex_lock(&_mutex2);
        // printThird() outputs "third". Do not change or remove this line.
        printThird();
    }

};

1117. H2O 生成 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/building-h2o/description/

class H2O {
    pthread_mutex_t _mutex;
    pthread_cond_t _h_cond;
    pthread_cond_t _o_cond;
    int _h_cnt = 0;
    int _o_cnt = 0;

public:
    H2O() {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_h_cond,nullptr);
        pthread_cond_init(&_o_cond,nullptr);
    }

    void hydrogen(function<void()> releaseHydrogen) {
        lock();
        while(_h_cnt == 2){
            pthread_cond_wait(&_h_cond, &_mutex);
        }

        // releaseHydrogen() outputs "H". Do not change or remove this line.
        releaseHydrogen();

        ++_h_cnt;
        if(_h_cnt == 2 and _o_cnt == 1){
            _h_cnt = 0;
            _o_cnt = 0;
        }
        pthread_cond_signal(&_o_cond);
        unlock();
    }

    void oxygen(function<void()> releaseOxygen) {
        lock();
        while(_o_cnt == 1){
            pthread_cond_wait(&_o_cond, &_mutex);
        }
        
        // releaseOxygen() outputs "O". Do not change or remove this line.
        releaseOxygen();

        ++_o_cnt;
        if(_h_cnt == 2){
            _o_cnt = 0;
            _h_cnt = 0;
        }
        pthread_cond_broadcast(&_h_cond);
        unlock();
    }

private:
    void lock(){ pthread_mutex_lock(&_mutex); }
    void unlock(){ pthread_mutex_unlock(&_mutex); }

};

1226. 哲学家进餐 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/the-dining-philosophers/description/

class DiningPhilosophers {
private:
    static const int N = 5;
    pthread_mutex_t _mutex_arr[N];

public:
    DiningPhilosophers() {
        for(int i=0;i<N;++i){
            _mutex_arr[i] = PTHREAD_MUTEX_INITIALIZER;
        }
    }

    void wantsToEat(int philosopher,
                    function<void()> pickLeftFork,
                    function<void()> pickRightFork,
                    function<void()> eat,
                    function<void()> putLeftFork,
                    function<void()> putRightFork) {
        int left = (philosopher + 1) % N;
        int right = philosopher;
        
        if(philosopher % 2 == 0){
            lock(left);
            lock(right);
            pickLeftFork();
            pickRightFork();
        }
        else{
            lock(right);
            lock(left);
            pickRightFork();
            pickLeftFork();
        }
        
        eat();
        putLeftFork();
        putRightFork();
        unlock(left);
        unlock(right);
    }

private:
    void lock(int idx){ pthread_mutex_lock(&_mutex_arr[idx]); }
    void unlock(int idx){ pthread_mutex_unlock(&_mutex_arr[idx]); }

};

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

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

相关文章

知识图谱与大数据:区别、联系与应用

目录 前言1 知识图谱1.1 定义1.2 特点1.3 应用 2 大数据2.1 定义2.2 应用 3. 区别与联系3.1 区别3.2 联系 结语 前言 在当今信息爆炸的时代&#xff0c;数据成为了我们生活和工作中不可或缺的资源。知识图谱和大数据是两个关键概念&#xff0c;它们在人工智能、数据科学和信息…

Java基础之算数运算符的初级用法

运算符 运算符: 对字面量或者变量进行操作的符号 表达式: 用运算符把字面量或者变量连接起来,符合java语法的式子就可以称为表达式 不同运算符连接的表达式体现的是不同类型的表达式 一 .算数运算符 实践一下 加 减 乘 运行结果: 除 取模 运行结果 练习: 数值拆分 需求…

P5210A泰克P5210A高压差分探头

181/2461/8938产品概述&#xff1a; P5210A 是 Tektronix 的差分探头。差分探头是具有两个输入的有源探头。负输入和正输入连同驱动 50 欧姆电缆的地线以传输数字示波器通道的输出 泰克 P5210A 特性&#xff1a; 带宽&#xff1a;50 兆赫 差分电压&#xff1a;1000X&#xf…

LLMs之Grok-1.5:Grok-1.5的简介、安装和使用方法、案例应用之详细攻略

LLMs之Grok-1.5&#xff1a;Grok-1.5的简介、安装和使用方法、案例应用之详细攻略 导读&#xff1a;xAI公司在不久前发布了Grok-1模型以及模型结构&#xff0c;揭示了公司到去年11月为止在大语言模型研发上的进步。2024年3月28日(美国时间)&#xff0c;xAI以“迅雷不及掩耳之势…

Image-Adaptive YOLO for Object Detection in Adverse Weather Conditions(IA-YOLO)

1、总体概述 基于深度学习的目标检测在常规条件的数据集可以获得不错的结果&#xff0c;但是在环境、场景、天气、照度、雾霾等自然条件的综合干扰下&#xff0c;深度学习模型的适应程度变低&#xff0c;检测结果也随之下降&#xff0c;因此研究在复杂气象条件下的目标检测方法…

为什么要学Python?Python的优势在哪?

人生苦短&#xff0c;我用 Python 不知道从什么时候开始流行这句话 Python 是个什么神仙编程语言 为啥全世界都在鼓励孩子学 Python 简单容易上手 国内、国际的竞赛机会多&#xff0c;含金量足 Python 好就业、薪资高 下面且看详细分析 01 什么是Python / 科技编程老师…

变分信息瓶颈

变分信息瓶颈和互信息的定义 1 变分信息瓶颈 定义&#xff1a;变分信息瓶颈&#xff08;Variational Information Bottleneck&#xff09;是一种用于学习数据表示的方法&#xff0c;它旨在通过最小化输入和表示之间的互信息来实现数据的压缩和表示学习。这种方法通常用于无监…

安装docker 并搭建出一颗爱心树

1、docker介绍 Docker 是⼀个开源的容器运⾏时软件&#xff08;容器运⾏时是负责运⾏容器的软件&#xff09;&#xff0c;基于 Go 语 ⾔编写&#xff0c;并遵从 Apache2.0 协议开源。 Docker可以让开发者打包⾃⼰的应⽤以及依赖到⼀个轻量的容器中&#xff0c;然后发布到任何…

代码随想录训练营Day36:● 435. 无重叠区间 ● 763.划分字母区间 ● 56. 合并区间

435. 无重叠区间 题目链接 https://leetcode.cn/problems/non-overlapping-intervals/description/ 题目描述 思路 直接统计重叠区间的个数&#xff0c;就是需要删除的个数 public int eraseOverlapIntervals(int[][] intervals) {Arrays.sort(intervals,(a,b)-> Intege…

【火猫TV】NBA:快船突然崩盘众人迷茫,都是续约惹的祸!

本赛季快船队的表现就像过山车一般&#xff0c;起起伏伏让人看得惊心动魄&#xff0c;他们在赛季初期一度找不到胜利的办法&#xff0c;后来威少主动担任替补&#xff0c;球队将组织大权给了哈登&#xff0c;然后战绩一路飙升。可是到了赛季末&#xff0c;快船队的胜率出现了大…

2024软件设计师备考讲义——UML(统一建模语言)

UML的概念 用例图的概念 包含 <<include>>扩展<<exted>>泛化 用例图&#xff08;也可称用例建模&#xff09;描述的是外部执行者&#xff08;Actor&#xff09;所理解的系统功能。用例图用于需求分析阶段&#xff0c;它的建立是系统开发者和用户反复…

HarmonyOS 应用开发之UIAbility组件启动模式

UIAbility的启动模式是指UIAbility实例在启动时的不同呈现状态。针对不同的业务场景&#xff0c;系统提供了三种启动模式&#xff1a; singleton&#xff08;单实例模式&#xff09;multiton&#xff08;多实例模式&#xff09;specified&#xff08;指定实例模式&#xff09;…

Day24:回溯法 LeedCode 77.组合

回溯法解决的问题都可以抽象为树形结构 for循环就是遍历集合区间&#xff0c;可以理解一个节点有多少个孩子&#xff0c;这个for循环就执行多少次。 从图中看出for循环可以理解是横向遍历&#xff0c;backtracking&#xff08;递归&#xff09;就是纵向遍历&#xff0c;这样就把…

3D目标检测综述笔记

3D Object Detection for Autonomous Driving: A Review and New Outlooks https://arxiv.org/pdf/2206.09474.pdf 目录 0.background​编辑 1.1表示形式 1.2感知输入 1.3数据集 1.4评估指标 1. LiDAR-based 3D Object Detection 2.数据表征 2.1 point-based​ 2.1.…

iOS - Runloop的运行逻辑

文章目录 iOS - Runloop的运行逻辑1. 苹果官方的Runloop执行图2. Mode里面的东西2.1 Source02.2 Source12.3 Timers2.4 Observers 3. 执行流程3.1 注意点 4. Runloop休眠 iOS - Runloop的运行逻辑 1. 苹果官方的Runloop执行图 2. Mode里面的东西 2.1 Source0 触摸事件处理pe…

4G/5G防爆布控球

#防爆布控球 #远程实时监控 #移动应急指挥 #高清图像采集 #防爆安全认证 4G/5G防爆布控球 M130-EX防爆布控球是针对石化装置、石油平台、燃气、化工、制药、煤炭、冶炼、船舶制造、纺织等易燃易爆环境及危险场所而开发设计的防爆智能一体化电气设备。 产品型号&#xff1a;M13…

Docker搭建FastDFS + Ngnix图片文件服务器

安装教程 一、环境与备件安装&#xff08;安装Docker&#xff09; 更新系统&#xff1a;首先&#xff0c;确保系统已更新到最新版本。 a. 更新Ubuntu系统命令&#xff1a; sudo apt update sudo apt upgradeb. 更新CentOS系统命令&#xff1a; sudo yum update安装依赖项&…

剑指Offer题目笔记20(在数组范围内二分查找)

面试题72&#xff1a; 问题&#xff1a; ​ 输入一个非负整数&#xff0c;计算它的平方根。 解决方案&#xff1a; 使用二分查找。一个数x的平方根一定小于或等于x&#xff0c;同时&#xff0c;除了0之外的所有非负整数的平方根都大于等于1&#xff0c;故该数的平方根在1到x…

AI大模型引领金融创新变革与实践【文末送书-46】

文章目录 AI大模型的定义与特点AI大模型在金融领域的应用 01 大模型在金融领域的 5 个典型应用场景02 大模型在金融领域应用所面临的风险及其防范03 AIGC 技术的科林格里奇困境04 金融机构使用 AIGC 技术的 4 条可能路径AIGC重塑金融&#xff1a;AI大模型驱动的金融变革与实践…

.NET CORE使用Redis分布式锁续命(续期)问题

结合上一期 .NET CORE 分布式事务(三) DTM实现Saga及高并发下的解决方案(.NET CORE 分布式事务(三) DTM实现Saga及高并发下的解决方案-CSDN博客)。有的小伙伴私信说如果锁内锁定的程序或者资源未在上锁时间内执行完&#xff0c;造成的使用资源冲突&#xff0c;需要如何解决。本…