首页 > 其他分享 >线程模块

线程模块

时间:2024-05-23 11:41:09浏览次数:30  
标签:thread 信号量 mutex 模块 pthread 线程 sem

概述

该模块基于pthread实现。sylar说,由于c++11中的thread也是由pthread封装实现的,并且没有提供读写互斥量,读写锁,自旋锁等,所以自己封装了pthread。包括以下类:

  • Thread:线程类,构造函数传入线程入口函数和线程名称,线程入口函数类型为void(),如果带参数,则需要用std::bind进行绑定。线程类构造之后线程即开始运行,构造函数在线程真正开始运行之后返回。

  • 线程同步类(这部分被拆分到mutex.h)中:

  • Semaphore: 计数信号量,基于sem_t实现

  • Mutex: 互斥锁,基于pthread_mutex_t实现

  • RWMutex: 读写锁,基于pthread_rwlock_t实现

  • Spinlock: 自旋锁,基于pthread_spinlock_t实现

  • CASLock: 原子锁,基于std::atomic_flag实现

线程模块主要由Thread类实现

  • class Thread:实现线程的封装

关于线程id的问题,在获取线程id时使用syscall获得唯一的线程id

进程pid: getpid()                 
线程tid: pthread_self()     //进程内唯一,但是在不同进程则不唯一。
线程pid: syscall(SYS_gettid)     //系统内是唯一的

锁模块介绍

信号量


信号量(Semaphore)是一种用于多线程同步的机制,能够控制多个线程对共享资源的访问。信号量的关键作用是通过计数器来管理访问权限,从而避免竞争条件。信号量有两个主要操作:

  • 等待(P 操作,wait 或 down):减少信号量的值。如果信号量的值为0,则调用线程将被阻塞,直到信号量的值大于0。
  • 释放(V 操作,signal 或 up):增加信号量的值。如果有线程因为等待操作而被阻塞,则唤醒其中一个线程。

有以下信号量函数:

  • sem_init(): 初始化一个未命名的信号量。
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
// sem: 指向信号量对象的指针。
// pshared: 指定信号量是否在进程间共享。0 表示信号量在同一进程的线程间共享,非0表示信号量在进程间共享。
// value: 信号量的初始值。
  • sem_destroy(): 销毁一个未命名的信号量。
#include <semaphore.h>
int sem_destroy(sem_t *sem);
// sem: 指向信号量对象的指针。
  • sem_wait(): 等待(减少)信号量。如果信号量的值为0,调用线程将阻塞直到信号量的值大于0。
#include <semaphore.h>
int sem_wait(sem_t *sem);
// sem: 指向信号量对象的指针。
  • sem_trywait(): 尝试等待(减少)信号量。如果信号量的值为0,该函数立即返回,并不会阻塞。
#include <semaphore.h>
int sem_trywait(sem_t *sem);
// sem: 指向信号量对象的指针。
  • sem_post(): 释放(增加)信号量。如果有其他线程因等待信号量而被阻塞,该函数会唤醒其中一个线程。
#include <semaphore.h>
int sem_post(sem_t *sem);
// sem: 指向信号量对象的指针。
  • sem_getvalue(): 获取信号量的当前值。
#include <semaphore.h>
int sem_getvalue(sem_t *sem);
// sem: 指向信号量对象的指针。
// sval: 指向整数的指针,用于存储信号量的当前值。

Sylar对其进行了封装,形成class Semaphore

// 信号量,它本质上是一个长整型的数
sem_t m_semaphore;

// 构造函数
Semaphore::Semaphore(uint32_t count) {
    if(sem_init(&m_semaphore, 0, count)) {
        throw std::logic_error("sem_init error");
    }
}

// 析构函数
Semaphore::~Semaphore() {
    sem_destroy(&m_semaphore);
}

// 获取信号量
void Semaphore::wait() {
    if(sem_wait(&m_semaphore)) {
        throw std::logic_error("sem_wait error");
    }
}

// 释放信号量
void Semaphore::notify() {
    if(sem_post(&m_semaphore)) {
        throw std::logic_error("sem_post error");
    }
}

互斥锁

pthread_mutex_t是Pthreads库中的一种互斥锁(Mutex),用于在线程间提供同步机制,确保在多线程环境中对共享资源的互斥访问。相关函数:

#include <pthread.h>

pthread_mutex_t mutex;
// 初始化
pthread_mutex_init(&mutex, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
// 加锁互斥锁
pthread_mutex_lock(&mutex);
// 尝试加锁互斥锁
pthread_mutex_trylock(&mutex);
// 解锁互斥锁
pthread_mutex_unlock(&mutex);

为方便封装各种锁,这里定义了3个结构体,都在构造函数时自动lock,在析构时自动unlock,这样可以简化锁的操作,避免忘记解锁导致死锁。

  • ScopedLockImpl:用来分装互斥量,自旋锁,原子锁
  • ReadScopedLockImpl && WriteScopedLockImpl:用来封装读写锁

Sylar对其进行了封装,形成class Mutex

// 互斥量
pthread_mutex_t m_mutex;

// 构造函数
Mutex () {
    pthread_mutex_init(&m_mutex, nullptr);
}

// 析构函数
~Mutex () {
    pthread_mutex_destroy(&m_mutex);
}

// lock(加锁)
void lock() {
    pthread_mutex_lock(&m_mutex);
}

// unlock(解锁)
void unlock() {
        pthread_mutex_unlock(&m_mutex);
    }

自旋锁

与mutex不同,自旋锁不会使线程进入睡眠状态,而是在获取锁时进行忙等待,直到锁可用。当锁被释放时,等待获取锁的线程将立即获取锁,从而避免了线程进入和退出睡眠状态的额外开销。

Sylar中基于pthread_spinlock_t及其相关函数封装了class Spinlock

// 自旋锁定义
pthread_spinlock_t m_mutex;

// 构造函数
Spinlock() {
    pthread_spin_init(&m_mutex, 0);
}

// 析构函数
~Spinlock() {
    pthread_spin_destroy(&m_mutex);
}

// 加锁
void lock() {
    pthread_spin_lock(&m_mutex);
}

// 解锁
void unlock() {
    pthread_spin_unlock(&m_mutex);
}

读写锁

读写锁是一种同步机制,用于在多线程环境下对共享资源进行访问控制。与互斥锁不同,读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这样可以提高程序的性能和效率,但需要注意避免读写锁死锁等问题。

Sylar基于pthread_rwlock_t及其相关函数封装了class RWMutex

// 读写锁
pthread_rwlock_t m_lock;

// RWMutex(构造函数)
RWMutex() {
    pthread_rwlock_init(&m_lock, nullptr);
}

// ~RWMutex(析构函数)
~RWMutex() {
    pthread_rwlock_destroy(&m_lock);
}

// rdlock(加读锁)
void rdlock() {
    pthread_rwlock_rdlock(&m_lock);
}

// wrlock(加写锁)
void wrlock() {
    pthread_rwlock_wrlock(&m_lock);
}

// unlock(解锁)
void unlock() {
    pthread_rwlock_unlock(&m_lock);
}

原子锁

在多线程编程中,原子标志位通常用于实现简单的锁机制,以确保对共享资源的访问是互斥的。使用atomic_flag.clear()可以轻松地重置标志位,使之再次可用于控制对共享资源的访问。需要注意的是,由于该函数是一个原子操作,因此可以安全地在多个线程之间使用,而无需担心竞态条件和数据竞争等问题

class CASLock(原子锁)实现如下:

  • 成员变量
// 线程id 
pid_t m_id = -1;
// 线程结构
pthread_t m_thread = 0;
// 线程执行函数
std::function<void()> m_cb;
// 线程名称
std::string m_name;
// 信号量
Semaphore m_semaphore;
// m_mutex是一个原子布尔类型,具有特殊的原子性质,可以用于实现线程间同步和互斥。
// volatile关键字表示该变量可能会被异步修改,因此编译器不会对其进行优化,而是每次都从内存中读取该变量的值。
volatile std::atomic_flag m_mutex;

// CASLock(构造函数)
CASLock () {
    m_mutex.clear(); 
}

// lock(加锁)
void lock() {
    while (std::atomic_flag_test_and_set_explicit(&m_mutex, std::memory_order_acquire));
}


// unlock(解锁)
void unlock() {
    std::atomic_flag_clear_explicit(&m_mutex, std::memory_order_release);
}

线程模块

class Thread的实现

定义了两个线程局部变量用于指向当前线程以及线程的名称。

static thread_local是C++中的一个关键字组合,用于定义静态线程本地存储变量。具体来说,当一个变量被声明为static thread_local时,它会在每个线程中拥有自己独立的静态实例,并且对其他线程不可见。这使得变量可以跨越多个函数调用和代码块,在整个程序运行期间保持其状态和值不变。

需要注意的是,由于静态线程本地存储变量是线程特定的,因此它们的初始化和销毁时机也与普通静态变量不同。具体来说,在每个线程首次访问该变量时会进行初始化,在线程结束时才会进行销毁,而不是在程序启动或运行期间进行一次性初始化或销毁。

// 指向当前线程 
static thread_local Thread *t_thread = nullptr;
// 指向线程名称
static thread_local std::string t_thread_name = "UNKNOW";
  • Thread(构造函数):初始化线程执行函数、线程名称,创建新线程。
// thread:指向pthread_t类型的指针,用于返回新线程的ID。
// attr:指向pthread_attr_t类型的指针,该结构体包含一些有关新线程属性的信息。可以将其设置为NULL以使用默认值。
// start_routine:是指向新线程函数的指针,该函数将在新线程中运行。该函数必须采用一个void类型的指针作为参数,并返回一个void类型的指针。
// arg:是指向新线程函数的参数的指针。如果不需要传递参数,则可以将其设置为NULL。

Thread::Thread(std::function<void()> cb, const std::string &name)
    : m_cb(cb)
    , m_name(name) {
    if (name.empty()) {
        m_name = "UNKNOW";
    }
    int rt = pthread_create(&m_thread, nullptr, &Thread::run, this);
    if (rt) {
        SYLAR_LOG_ERROR(g_logger) << "pthread_create thread fail, rt=" << rt
                                  << " name=" << name;
        throw std::logic_error("pthread_create error");
    }
    m_semaphore.wait();
}

调用pthread_create函数后,将会创建一个新线程,并开始执行通过start_routine传递给它的函数。新线程的ID将存储在thread指向的变量中。请注意,新线程将在与调用pthread_create函数的线程并发执行的情况下运行。

  • ~Thread(析构函数)
Thread::~Thread() {
    if (m_thread) {
        pthread_detach(m_thread);
    }
}
  • join(等待线程执行完成)
    当调用 pthread_join() 时,当前线程会阻塞,直到指定的线程完成执行。一旦线程结束,当前线程就会恢复执行,并且可以通过 retval 参数来获取线程的返回值。如果不关心线程的返回值,也可以将 retval 参数设置为 NULL。成功:返回 0 表示线程成功退出。
// thread:要等待的线程ID。
// retval:指向指针的指针,用于存储线程返回的值。如果不需要获取返回值,则可以将其设置为NULL。
void Thread::join() {
    if (m_thread) {
        int rt = pthread_join(m_thread, nullptr);
        if (rt) {
            SYLAR_LOG_ERROR(g_logger) << "pthread_join thread fail, rt=" << rt
                                      << " name=" << m_name;
            throw std::logic_error("pthread_join error");
        }
        m_thread = 0;
    }
}
  • run(线程执行函数)

通过信号量,能够确保构造函数在创建线程之后会一直阻塞,直到run方法运行并通知信号量,构造函数才会返回。
在构造函数中完成线程的启动和初始化操作,可能会导致线程还没有完全启动就被调用,从而导致一些未知的问题。因此,在出构造函数之前,确保线程先跑起来,保证能够初始化id,可以避免这种情况的发生。同时,这也可以保证线程的安全性和稳定性。

void *Thread::run(void *arg) {
    Thread *thread = (Thread *)arg;
    t_thread       = thread;
    t_thread_name  = thread->m_name;
    thread->m_id   = sylar::GetThreadId();
    pthread_setname_np(pthread_self(), thread->m_name.substr(0, 15).c_str());

    std::function<void()> cb;
    cb.swap(thread->m_cb);

    thread->m_semaphore.notify();

    cb();
    return 0;
}

总结

  • 对日志系统的临界资源进行互斥访问时,使用自旋锁而不是互斥锁。
  1. mutex使用系统调用将线程阻塞,并等待其他线程释放锁后再唤醒它,这种方式适用于长时间持有锁的情况。而spinlock在获取锁时忙等待,即不断地检查锁状态是否可用,如果不可用则一直循环等待,因此适用于短时间持有锁的情况。
  2. 由于mutex会将线程阻塞,因此在高并发情况下可能会出现线程频繁地进入和退出睡眠状态,导致系统开销大。而spinlock虽然不会使线程进入睡眠状态,但会消耗大量的CPU时间,在高并发情况下也容易导致性能问题。
  3. 另外,当一个线程尝试获取已经被其他线程持有的锁时,mutex会将该线程阻塞,而spinlock则会在自旋等待中消耗CPU时间。如果锁的持有时间较短,则spinlock比mutex更适合使用;如果锁的持有时间较长,则mutex比spinlock
  • 在构造函数中创建子进程并等待其完成执行是一种常见的技术,可以通过信号量(Semaphore)来实现主线程等待子线程完成。
  1. 首先,在主线程中创建一个Semaphore对象并初始化为0。然后,在构造函数中创建子线程,并将Semaphore对象传递给子线程。子线程将执行所需的操作,并在最后使用Semaphore对象发出信号通知主线程它已经完成了工作。
  2. 主线程在构造函数中调用Semaphore对象的wait方法,这会使主线程阻塞直到收到信号并且Semaphore对象的计数器值大于0。当子线程发出信号时,Semaphore对象的计数器值增加1,因此主线程可以继续执行构造函数的剩余部分。

标签:thread,信号量,mutex,模块,pthread,线程,sem
From: https://www.cnblogs.com/zjq1999/p/18208080

相关文章

  • 浅谈一下C#和java的线程不同点
    C#和Java在线程处理方面有一些显著的区别,这些区别主要体现在线程的创建、管理和生命周期控制上。以下是一些主要的区别:线程的创建和管理Java:Java中线程的创建通常是通过继承Thread类或实现Runnable接口来实现的。Java提供了线程组(ThreadGroup)的概念,允许将线程组织在一起......
  • 1.什么是模块化,为什么要模块化? 2.衡量模块化独立的定性标准是什么?用自己的话表达其含
    模块化是将一个系统划分为多个独立的模块或组件,每个模块负责处理系统的一部分功能或任务。模块化能够使代码结构更清晰、易于维护和扩展,提高代码的重用性和可读性。通过模块化,开发人员可以更加高效地协同工作,降低系统复杂度。衡量模块化独立的定性标准包括内聚性和耦合性。内......
  • C++高性能服务器框架—协程模块
    协程模块概述一、概念可以简单的认为:协程就是用户态的线程,但是上下文切换的时机是靠调用方(写代码的开发人员)自身去控制的;对比首先介绍一下为什么要使用协程。从了解进程,线程,协程之间的区别开始。从定义来看进程是资源分配和拥有的基本单位。进程通过内存映射拥有独立的代......
  • socketserver模块、操作系统、操作系统的发展史
    【一】socketserver模块【1】简介socketserver中包含了两种类,一种为服务类(serverclass):前者提供了许多方法像绑定,监听,运行……(也就是建立连接的过程)。一种为请求处理类(requesthandleclass)专注于如何处理用户所发送的数据(也就是事务逻辑)。......
  • 进程间通信(管道),多线程
    Ⅰ进程间通信(管道)【一】引入借助于消息队列,进程可以将消息放入队列中,然后由另一个进程从队列中取出。这种通信方式是非阻塞的,即发送进程不需要等待接收进程的响应即可继续执行。multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的进程间通信(IPC)方式二......
  • python 自然语言处理模块
    Python中有几个流行的自然语言处理(NLP)模块,这些模块提供了广泛的工具和库,用于文本分析、处理和理解。以下是一些广泛使用的NLP模块:NLTK(NaturalLanguageToolkit)NLTK是Python中最著名的NLP库之一,它提供了文本处理的丰富工具,包括分词、词性标注、句法分析、语义推理等。网......
  • SFP光模块定义
    无论是SFP光模块还是SFP电模块,其接口定义是完全相同的,有统一的标准规范。如下图所示。引脚定义 电源:VCCT和VCCR分别是发射和接受部分电源,要求3.3V±5%,最大供电电流300mA以上。电感的直流阻抗应该小于1欧姆,确保SFP的供电电压稳定在3.3V。推荐的滤波网络,可以保证插拔......
  • 模块与包
    模块与包【一】什么是模块在Python中,一个py文件就是一个模块文件名xx.pyxx就是模块名编写模块的过程就是将零件拼装成一个完整的部件利用框架将所有部件拼接成一个完整的机器用模块开发代码,将某部分代码分别放到一个py文件中再利用主函数进行整合--->三层架构总(......
  • 三次握手和四次挥手、UDP、TCP、粘包问题、模块回顾
    【一】三次握手和四次挥手【1】TCP协议的三次握手和四次挥手TCP协议位于osi七层协议中的传输层(1)使用三次握手来建立连接一次握手:客户端发送带有SYN(SEQ=x)标志的数据包---》服务端,然后客户端进入SYN_SEND状态,等待服务器的确认。二次握手:服务端发送带有SYN+A......
  • CANoe中Logging模块使用方法及妙招⭐
    Logging是CANoe软件中的数据记录模块,主要在台架测试中使用,支持CAN/CANFD、LIN、FlexRay以及车载以太网总线的数据记录。常用的数据记录仪还有GL数据记录仪,GL有自己单独的硬件设备,应用场景主要为台架或者实车测试,进行无人看守时的数据记录,和Logging的最大区别就是Logging是CANoe软......