线程与同步

目录

一,线程概念

1,什么是线程

2,Linux 下的线程

3,POSIX 线程库

二,线程同步

1,数据不一致问题

2,如何解决这类问题

3,死锁

三,线程同步的运用

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

2,线程池

3,LeetCode


一,线程概念

1,什么是线程

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

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

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

571630e7a6934d4a897d5fceb416f279.jpeg775b42bebf3f4458a05df160d45371d0.jpeg

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

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

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

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

2,Linux 下的线程

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

2c3d1740806640b18ac7f0235ee04ad3.jpeg

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

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

fdedea01b6274152b9722ba4e8806e1b.jpeg

3,POSIX 线程库

为了实现可移植的线程程序,IEEE 定义了相关的标准(POSIX 标准),它定义的线程包叫做 pthread,大部分 UNIX 系统都支持该标准。与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以 "pthread_" 打头。想要使用这些函数库,需要引入头文件 <pthread.h> 。并且要在编译选项中使用 "-lphread" 来 动态链接 pthread 线程库。前面有提到每个线程都有自己独立的栈空间,而这个栈空间就是由线程库来维护的。
e822687d0fb24cc0a55a92add48f75bf.jpeg
下面来简单罗列一些相关的函数,具体用法就不在这里详细介绍了
① 线程标识
// 比较两个线程的 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为变量赋值的两个时钟周期之间读取这个变量时,就会读取到我们不期望的结果。

10bdc1fbdeb743f1b78d2a08d6fe3e7b.jpeg

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

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

① 将变量 i 放入寄存器

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

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

ce477b521a7f446a913c20b520a123a0.jpeg

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, 否则返回错误码

d0773f28fb324274a97c530caf9bc756.jpeg

为了实现互斥锁操作,大多数体系结构都提供了 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,这时就会发生死锁。这两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就产生了死锁。

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

换言之,这一死锁线程集合中的每一个线程都在等待另一个死锁的线程已经占有的资源。但是由于所有线程都不能运行,它们中的任何一个都无法释放资源,所以没有一个线程可以被唤醒。我们在写代码时要小心分配资源,尽力避免死锁。

死锁的四个必要条件

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


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

对死锁进行建模

030620ddc5134873a603c4d5e1a709f7.jpeg

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

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

三,线程同步的运用

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

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

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

66ef15f8f25c443bb7059e25cb02ef8e.jpeg

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

阻塞队列与普通队列区别在于:当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进行操作时会被阻塞)
b5419454d8bc4ec7bfbf8ea06a8adfe6.jpeg
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

部分运行结果:

144d02803c564bf288499c4f26de2c19.png

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

这样设计有以下好处:

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

2,线程池

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

线程池的核心组成:

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

线程池的主要优点:

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

33cb4a6428db4456abc03ad74ce4ea58.jpeg

这里实现的线程池本质上就是一个单生产者与多消费者的模型。

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

部分运行结果

da2e7c8fad864e19aade043f693a6152.png

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]); }

};

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第十四届蓝桥杯省赛C++ B组所有题目以及题解(C++)【编程题均通过100%测试数据】

第一题《日期统计》【枚举】 【问题描述】 小蓝现在有一个长度为100的数组&#xff0c;数组中的每个元素的值都在0到9的范围之内。数组中的元素从左至右如下所示&#xff1a; 5 6 8 6 9 1 6 1 2 4 9 1 9 8 2 3 6 4 7 7 5 9 5 0 3 8 7 5 8 1 5 8 6 1 8 3 0 3 7 9 2 7 0 5 8 8 …

【Go】五、流程控制

文章目录 1、if2、switch3、for4、for range5、break6、continue7、goto8、return 1、if 条件表达式左右的()是建议省略的if后面一定要有空格&#xff0c;和条件表达式分隔开来{ }一定不能省略if后面可以并列的加入变量的定义 if count : 20;count < 30 {fmt.Println(&quo…

剑指Offer题目笔记22(快速排序)

快速排序定义&#xff1a; ​ 快速排序的基本思想是分治法&#xff0c;排序过程如下&#xff1a;在输入数组中随机选取一个元素作为中间值&#xff08;pivot&#xff09;&#xff0c;然后对数组进行分区&#xff08;partition&#xff09;&#xff0c;使所有比中间值小的数据移…

DC电源模块的分类及特点介绍

BOSHIDA DC电源模块的分类及特点介绍 DC电源模块是一种将交流电转换为直流电的设备&#xff0c;广泛应用于各种电子设备中。根据其特点和功能&#xff0c;DC电源模块可以分为线性稳压模块和开关稳压模块两种。本文将详细介绍这两种DC电源模块的分类和特点&#xff0c;以便读者…

[C++初阶] 爱上C++ : 与C++的第一次约会

&#x1f525;个人主页&#xff1a;guoguoqiang &#x1f525;专栏&#xff1a;我与C的爱恋 本篇内容带大家浅浅的了解一下C中的命名空间。 在c中&#xff0c;名称&#xff08;name&#xff09;可以是符号常量、变量、函数、结构、枚举、类和对象等等。工程越大&#xff0c;名称…

京东云服务器价格_云主机价格查询系统_2024年京东云优惠活动

2024年京东云服务器优惠价格表&#xff0c;轻量云主机优惠价格5.8元1个月、轻量云主机2C2G3M价格50元一年、196元三年&#xff0c;2C4G5M轻量云主机165元一年&#xff0c;4核8G5M云主机880元一年&#xff0c;游戏联机服务器4C16G配置26元1个月、4C32G价格65元1个月、8核32G费用…

爬虫学习(爬取音乐)

import re import requestsurl "http://www.yy8844.cn/ting/numes/sussoc.shtml" response requests.get(url) response.encoding "gbk" # print(r.text) #第一步&#xff0c;访问网页获取MusicID p re.compile(r"MusicId(.*?);",re.S) prin…

VBA技术资料MF135:多值匹配查找

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

Hyper-V 虚拟机设置静态 IP 和外网访问

文章目录 环境说明1 问题简介2 解决过程 环境说明 宿主机操作系统&#xff1a;Windows 11 专业版漏洞复现操作系&#xff1a;debian-live-12.5.0-amd64-standard 1 问题简介 在 Windows 上用自带的 Hyper-V 虚拟机管理应用创建了一个 Debian 12 虚拟机&#xff0c;配置静态 IP…

Linux vim用法

在命令模式下&#xff0c;点i 进入输入模式 输入模式下&#xff1a;通过箭头可以实现左右上下移动 o是从新起下一行开始写 O是新起上一行开始写 $是到此行的末尾 0是到此行的开头 gg是到第一行 yy是复制此行&#xff0c;p是粘贴 dd是删除此行 u是撤销 Ctrl r是反向撤…

启信宝商业大数据助力全国经济普查

近日&#xff0c;合合信息旗下启信宝收到中国青年创业就业基金会感谢信&#xff0c;对启信宝协同助力全国经济普查和服务青年创业就业研究表达感谢。 第五次全国经济普查是新时代新征程上一次重大国情国力调查&#xff0c;是对国民经济“全面体检”和“集中盘点”&#xff0c;…

若依菜单名称过长显示不全怎么办?

菜单名称太长的话超出宽度部分会显示...,我们可以自己调整一下菜单的宽度或者设置一个title,这样鼠标移动上去显示完整的菜单名称。 目录 1、在layout\components\Sidebar\SidebarItem.vue文件设置:title 2、在layout\components\Sidebar\Item.

简单的LAMP部署

目录 一、准备环境 二、安装apache组件 三、安装mysql组件 四、安装php组件 五、浏览器访问 一、准备环境 iptables -F #清空防火墙规则 systemctl stop firewalld #关闭防火墙 setenforce 0 …

bugku-web-eval

页面源码 <code><span style"color: #000000"> <span style"color: #0000BB"><?php <br /> </span><span style"color: #007700">include </span><span style"color: #DD0000"&…

Facebook轮播广告是什么?投放过程中有哪些需要注意的吗?

轮播广告是Facebook广告形式中的一种&#xff0c;可以把3—5个广告合并到一个可滚动的广告单元中。轮播广告会出现在新鲜事即News Feed中&#xff0c;是独立站卖家常用的一种广告形式 为什么选择轮播广告&#xff1f; 转化率更高&#xff1a;相较于单图广告&#xff0c;轮播广…

个人简历主页搭建系列-04:网站初搭建

准备工作差不多了&#xff0c;该开始搭建网站了&#xff01; 这次我们先把网站搭建部署起来&#xff0c;关于后续主题内容等更换留到后续。 创建源码文件夹 首先通过 hexo 创建本地源码文件夹。因为最终部署的 github 仓库格式为 websiteName.github.io&#xff08;websiteN…

【检索增强】Retrieval-Augmented Generation for Large Language Models:A Survey

本文简介 1、对最先进水平RAG进行了全面和系统的回顾&#xff0c;通过包括朴素RAG、高级RAG和模块化RAG在内的范式描述了它的演变。这篇综述的背景下&#xff0c;更广泛的范围内的法学硕士研究RAG的景观。 2、确定并讨论了RAG过程中不可或缺的核心技术&#xff0c;特别关注“…

A fatal error occurred: MD5 of file does not match data in flash!问题解决

采用的芯片是ESP32-S3-WROOM&#xff0c;16MB FLASH 开发环境是Arduino&#xff0c;烧录到100%后直接报错。 以为是Arduino的问题&#xff0c;用esp-idf开发的程序&#xff0c; 烧录的过程中&#xff0c;也是直接报错如下&#xff1a; esptool.py v4.7.0 Serial port /dev/…

【MySQL】DQL-条件查询语句全解(附带代码演示&案例练习)

前言 大家好吖&#xff0c;欢迎来到 YY 滴MySQL系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C Linux的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的…

The Divide-and-Conquer Paradigm分而治之范式

In general, the divide-and-conquer paradigm consists of the following steps. The divide step. The input is partitioned into 1 ≤ p ≤ n parts. The conquer step. This step consists of performing p recursive call(s) if the problem size is…