C++项目实战——sylar服务器框架:线程模块

96 阅读13分钟

什么是线程

线程与进程,你真得理解了吗_进程和线程的区别-CSDN博客

sylar 中线程模块的组成

(一)信号量类(class Semaphore)

// 信号量类
class Semaphore{
public:
    Semaphore(uint32_t count = 0);
    ~Semaphore();

    void wait();    // 等待信号量
    void notify();  // 发送信号量通知
private:
    Semaphore(const Semaphore&) = delete;
    Semaphore(Semaphore&&) = delete;
    Semaphore& operator=(const Semaphore&) = delete;
    Semaphore& operator=(Semaphore&&) = delete;

private:
    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()
{
    // sem_wait 用于阻塞线程,直到信号量的值大于0
    if(sem_wait(&m_semaphore))
    {
        throw std::logic_error("sem_wait error");
    }
}

void Semaphore::notify()
{
    // sem_post 调用时信号量的值增加1
    if(sem_post(&m_semaphore))
    {
        throw std::logic_error("sem_post error");
    }
}
  • Semaphore 类是对 POSIX 信号量(sem_t)的封装,用于实现线程间的同步和资源访问控制。

POSIX,全称为 “Portable Operating System Interface” ,是由 IEEE(Institute of Electrical and Electronics Engineers) 制定的一系列标准,旨在定义操作系统的 API(应用程序编程接口),以实现应用程序在不同的 Unix 系统和类 Unix 系统之间的可移植性。就是说 POSIX 标准定义了一套操作系统必须实现的 api,这样的话,当你写的代码只使用了POSIX 标准定义的接口时,那么你的代码相对于所有支持 POSIX 标准的操作系统来说,都是可移植的,最多重新编译一下就可以使用。

  • sem_t 是 POSIX 标准中定义的一个数据类型,用于表示信号量。它在多线程编程中用于同步线程访问共享资源。

    • sem_t 的实例可以表示某种资源的计数,线程可以通过等待(sem_wait)和通知(sem_post)操作来请求或释放资源。

    • 它可以被初始化为一个特定的计数值,表示资源的初始可用数量。

    • 相关函数

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

        • 初始化信号量
        • sem:指向需要初始化的 sem_t 变量。
        • pshared:如果为 0,信号量在当前进程的线程间共享;非 0 表示在多个进程间共享。
        • value:信号量的初始计数值。
        Semaphore::Semaphore(uint32_t count)
        {
            if(sem_init(&m_semaphore, 0, count))
            {
                throw std::logic_error("sem_init error");
            }
        }
        
      • int sem_destroy(sem_t *sem)

        • 销毁信号量并释放资源。
        • 只能销毁已用 sem_init 初始化的信号量,且在销毁前不能有线程在等待该信号量。
        Semaphore::~Semaphore()
        {
            sem_destroy(&m_semaphore);
        }
        
      • int sem_wait(sem_t *sem)

        • 等待信号量。 如果信号量的值大于 0,sem_wait 将减少其值并立即返回。
        • 如果值为 0,调用线程将被阻塞,直到有线程调用 sem_post 增加该值。
        void Semaphore::wait()
        {
            // sem_wait 用于阻塞线程,直到信号量的值大于0
            if(sem_wait(&m_semaphore))
            {
                throw std::logic_error("sem_wait error");
            }
        }
        
      • int sem_post(sem_t *sem)

        • 释放信号量,增加信号量的计数值。
        • 如果有其他线程在等待该信号量,sem_post 会唤醒一个等待的线程。
        void Semaphore::notify()
        {
            // sem_post 调用时信号量的值增加1
            if(sem_post(&m_semaphore))
            {
                throw std::logic_error("sem_post error");
            }
        }
        
      • int sem_trywait(sem_t *sem)

        • 尝试等待信号量。 如果当前信号量的值大于 0,则立即减少其值并返回。如果信号量为 0,不会阻塞,直接返回错误。
      • int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout)

        • 在给定时间内等待信号量。 如果在 abs_timeout 之前信号量未变为可用,返回错误。
  • 删除拷贝和移动构造函数,以及赋值运算符:

    禁止信号量对象的拷贝和移动,以确保线程同步资源的唯一性和一致性。

(二)封装互斥锁

(1)三个模板类

ScopedLockImpl:

用于封装常规锁的 RAII 实现。在构造函数中加锁,析构函数中解锁,确保锁在作用域结束时被释放。提供 lock()unlock() 方法,允许在特定情况下手动控制锁定和解锁。

// 对互斥锁的简单封装,基于RAII原则
template<class T>
struct ScopedLockImpl{
public:
    ScopedLockImpl(T& mutex) : m_mutex(mutex)
    {
        lock();
    }

    ~ScopedLockImpl()
    {
        unlock();
    }

    void lock()
    {
        if(!m_locked)
        {
            m_locked = true;
            m_mutex.lock();
        }
    }

    void unlock()
    {
        if(m_locked)
        {
            m_locked = false;
            m_mutex.unlock();
        }
    }
private:
    T& m_mutex;
    bool m_locked = false;
};

ReadScopedLockImpl

专为读锁设计,封装读操作的加锁和解锁。

template<class T>
struct ReadScopedLockImpl{
public:
    ReadScopedLockImpl(T& mutex) : m_mutex(mutex)
    {
        lock();
    }

    ~ReadScopedLockImpl()
    {
        unlock();
    }

    void lock()
    {
        if(!m_locked)
        {
            m_locked = true;
            m_mutex.rdlock();
        }
    }

    void unlock()
    {
        if(m_locked)
        {
            m_locked = false;
            m_mutex.unlock();
        }
    }
private:
    T& m_mutex;
    bool m_locked = false;
};

WriteScopedLockImpl:

专为写锁设计,确保写操作的加锁和解锁。

template<class T>
struct WriteScopedLockImpl{
public:
    WriteScopedLockImpl(T& mutex) : m_mutex(mutex)
    {
        lock();
    }

    ~WriteScopedLockImpl()
    {
        unlock();
    }

    void lock()
    {
        if(!m_locked)
        {
            // 下面两条语句不能调换顺序
            m_locked = true;    
            m_mutex.wrlock();
        }
    }

    void unlock()
    {
        if(m_locked)
        {
            m_locked = false;
            m_mutex.unlock();
        }
    }
private:
    T& m_mutex;
    bool m_locked = false;
};

(2)不同类型的锁

Mutex

基于 pthread_mutex_t 的普通互斥锁,提供了加锁和解锁功能。

class Mutex{
public:
    typedef ScopedLockImpl<Mutex> Lock;
    Mutex()
    {
        pthread_mutex_init(&m_mutex, nullptr);
    }

    ~Mutex()
    {
        pthread_mutex_destroy(&m_mutex);
    }

    void lock()
    {
        pthread_mutex_lock(&m_mutex);
    }

    void unlock()
    {
        pthread_mutex_unlock(&m_mutex);
    }


private:
    pthread_mutex_t m_mutex;
};

NullMutex:

一个 “空”互斥锁,不会执行任何操作,适用于不需要锁的地方,避免代码条件编译。

class NullMutex{
public:
    typedef ScopedLockImpl<NullMutex> Lock;
    NullMutex() {}
    ~NullMutex() {}
    void lock() {}
    void unlock() {}
};

RWMutex:

基于 pthread_rwlock_t 的读写锁。提供了读锁 (rdlock()) 和写锁 (wrlock()) 的支持,适用于读多写少的并发场景。

// 读写锁
class RWMutex{
public:
    typedef ReadScopedLockImpl<RWMutex> ReadLock;
    typedef WriteScopedLockImpl<RWMutex> WriteLock;
    RWMutex()
    {
        pthread_rwlock_init(&m_lock, nullptr);
    }

    ~RWMutex()
    {
        pthread_rwlock_destroy(&m_lock);
    }

    void rdlock()
    {
        pthread_rwlock_rdlock(&m_lock);
    }

    void wrlock()
    {
        pthread_rwlock_wrlock(&m_lock);
    }

    void unlock()
    {
        pthread_rwlock_unlock(&m_lock);
    }
private:
    pthread_rwlock_t m_lock;
};

NullRWMutex:

“空”读写锁,与 NullMutex 类似,用于无需锁的情形。

class NullRWMutex{
public:
    typedef ReadScopedLockImpl<NullMutex> ReadLock;
    typedef WriteScopedLockImpl<NullMutex> WriteLock;

    NullRWMutex() {}
    ~NullRWMutex() {}

    void rdlock() {}
    void wrlock() {}
    void unlock() {}
};

Spinlock:

基于 pthread_spinlock_t 实现的自旋锁。适合短时间的锁定,避免了线程上下文切换的开销。

// 自旋锁
class Spinlock{
public:
    typedef ScopedLockImpl<Spinlock> Lock;
    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);
    }

private:
    pthread_spinlock_t m_mutex;
};

CASLock:

// 原子锁
class CASLock {
public:

    typedef ScopedLockImpl<CASLock> Lock;

    CASLock() 
    {
        m_mutex.clear();
    }

    ~CASLock() 
    {
    }

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

    void unlock() 
    {
        std::atomic_flag_clear_explicit(&m_mutex, std::memory_order_release);
    }
private:

    volatile std::atomic_flag m_mutex;
};

(三)线程类

封装 POSIX 线程(pthread)的实现,并提供线程创建和管理功能。

class Thread{
public:
    typedef std::shared_ptr<Thread> ptr;    // 定义共享指针类型
    // 构造函数,接受一个可调用对象 cb 和线程名称 name。cb是线程运行时执行的回调函数
    Thread(std::function<void()> cb, const std::string& name);
    ~Thread();

    pid_t getId() const { return m_id;}                     // 返回线程 id
    const std::string& getName() const { return m_name;}    // 返回线程名称
    void join();                                            // 等待线程结束

    static Thread* GetThis();                               // 指向当前正在执行的 Thread 对象指针
    static const std::string& GetName();                    // 返回当前线程的名称
    static void SetName (const std::string& name);          // 设置当前线程的名称
private:
    Thread(const Thread&) = delete;                 // 禁止拷贝构造
    Thread(Thread&&) = delete;                      // 禁止移动构造
    Thread& operator=(const Thread&) = delete;      // 禁止拷贝赋值
    Thread& operator=(Thread&&) = delete;           // 禁止移动赋值

    static void* run(void* arg);

private:
    pid_t m_id = -1;
    pthread_t m_thread = 0;
    std:: function<void()> m_cb;
    std::string m_name;

    Semaphore m_semaphore;
};
Thread(const Thread&) = delete;                 // 禁止拷贝构造
Thread(Thread&&) = delete;                      // 禁止移动构造
Thread& operator=(const Thread&) = delete;      // 禁止拷贝赋值
Thread& operator=(Thread&&) = delete;           // 禁止移动赋值
  • 禁止这些操作的原因

    避免潜在的资源冲突

    • Thread 类管理的是一个线程资源(通过 pthread_t 或其他机制),并且线程的生命周期通常是一次性的。如果允许线程对象被拷贝或移动,那么同一个线程可能会被多个对象同时持有或管理,这会导致不确定的行为,比如:

      • 多次调用 join() :多个线程对象可能试图对同一个底层线程调用 join()detach(),这会引发未定义行为。
      • 资源释放问题:如果多个对象管理同一个线程,当其中一个对象销毁时,可能会导致线程资源的提前释放或未正确释放。

    拷贝语义不适用于线程

    • 拷贝构造或拷贝赋值意味着你希望创建一个新对象来管理同一个线程。线程对象的语义不同于普通数据,不能简单地复制,因为线程本身是一个独立的执行单元。复制一个 Thread 对象会让人误解为我们可以复制一个正在运行的线程,这在技术上和概念上都是不合理的。

    移动操作的复杂性

    • 移动构造和移动赋值操作通常会将资源从一个对象转移到另一个对象。对于像线程这样的系统资源,转移管理权并不总是安全的,尤其是当线程可能已经开始运行或可能已经被操作的情况下。这种不确定性会导致复杂的状态管理和同步问题。
    • 例如,假设线程已经开始执行,而你试图在中途移动该线程到另一个 Thread 对象中,这会引发很多复杂的同步问题。你需要明确线程的运行状态并小心管理其生命周期,避免线程资源在不同对象之间随意移动。

    简化类的设计

    • 禁止拷贝和移动操作使得 Thread 类的设计更加简单、清晰。线程对象的生命周期与所管理的底层线程严格绑定,没有多余的拷贝或转移,避免了很多潜在的错误。
    • 通过禁止这些操作,开发者可以更清晰地理解该类的设计意图——每个 Thread 对象唯一负责管理一个线程资源,且这个资源不可转移或复制。
  • pthread_t

    • pthread_t 是 POSIX 线程(pthread)库中用于标识线程的类型。

    • 相关函数:

      // 创建新线程
      int pthread_create(pthread_t *thread, const pthread_attr_t *attr,  void *(*start_routine)(void *), void *arg);
      
      • pthread_t *thread 是一个输出参数,函数会通过这个指针返回新创建的线程 ID。你可以使用这个线程 ID 进行后续的操作

      • const pthread_attr_t *attr: 这是线程的属性参数。你可以通过这个参数来指定线程的属性,比如栈大小、是否分离等。如果传递 nullptr,则使用默认的线程属性。

      • start_routine 是线程运行的函数。

      • arg 是传递给 start_routine 的参数,可以传递任意类型的指针。这个参数通常用于传递上下文数据给新线程。在实际应用中,这通常是一个结构体指针,包含多个数据。

      // 阻塞当前线程
      pthread_join(pthread_t thread, void** retval)
      
      • 阻塞当前线程,直到指定的线程 thread 结束执行。
      • 可选的 retval 参数是线程的返回值。
      // 将线程标记为分离状态
      pthread_detach(pthread_t thread)
      
      • 分离的线程在结束时会自动释放资源,不需要再调用 pthread_join

(四)线程类的函数解析

(1)构造函数

Thread::Thread(std::function<void()> cb, const std::string &name)
    :m_cb(std::move(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();
}

  • m_cb(std::move(cb)):将传入的回调函数 cb 移动到成员变量 m_cb。使用 std::move 是为了避免不必要的拷贝,提高性能。如果 cb 是一个大对象,移动可以节省内存分配和拷贝。

  • m_semaphore.wait(); : 通过一个信号量 m_semaphore 来同步主线程和子线程。m_semaphore.wait()阻塞主线程,直到子线程发出信号量通知,表示子线程已完成初始化。这样可以确保主线程在使用子线程之前,子线程已经正确初始化

(2)析构函数

Thread::~Thread()
{  
    if(m_thread)
    {
        pthread_detach(m_thread);
    }
}

(3)线程执行入口函数 run

static thread_local Thread* t_thread = nullptr;             // 指向当前执行的 Thread 对象
static thread_local std::string t_thread_name = "UNKNOW";   // 存储当前线程名称

...

void *Thread::run(void *arg)
{
    Thread* thread = (Thread*)arg;  // 将传入的参数 arg 转换为 Thread 指针
    t_thread = thread;              // 将当前线程对象指针赋值给线程局部变量 t_thread
    t_thread_name = thread->m_name; // 设置线程局部变量 t_thread_name 为线程的名称
    thread->m_id = sylar::GetThreadId(); // 获取当前线程 ID,并赋值给线程对象的 m_id
    pthread_setname_np(pthread_self(), thread->m_name.substr(0, 15).c_str()); // 设置系统线程名称

    std::function<void()> cb;       // 声明一个函数对象 cb
    cb.swap(thread->m_cb);          // 获取并交换回调函数,保证回调函数的安全执行
    
    // run()是静态成员函数,无法直接访问类的非静态成员函数
    thread->m_semaphore.notify();   // 子线程完成初始化,通知主线程
    cb();                           // 调用回调函数
    return 0;                       // 返回 0,表示线程正常结束
}
  • thread_local

    • 线程安全
      thread_local 变量是每个线程独有的,因此它们不需要进行额外的同步或锁机制。多个线程可以安全地访问和修改自己的局部变量,而不会相互干扰。这确保了你的 Thread*std::string 可以在多线程环境下正确工作。
    • 每个线程有独立的状态
      对于 Thread* 和线程名,你希望每个线程都有自己独立的对象和名字,而不与其他线程共享。所以你使用 thread_local 来实现这种线程本地存储。
  • cb.swap(thread->m_cb)

    • 回调函数是一个 std::function<void()> 类型,它通常会包含一个可调用对象(例如,函数指针、函数对象、或 Lambda 表达式)。在多线程环境中,直接赋值可能会涉及到拷贝操作,这样可能会在多线程中产生竞态条件,导致不安全的状态。

    • 使用 swap 操作可以避免这种风险。swap 是原子操作,确保了在交换 std::function 对象时的线程安全。因为 swap 不需要任何资源的拷贝,而是交换两个对象的内部指针,确保了线程间资源的安全切换

    • cb 被传递给子线程时,使用 swapm_cb 拥有 cb 中的资源,并且交换操作后,cb 本身就被置为空或无效。这样可以确保 Thread 类在运行过程中安全地拥有该回调函数的所有权,而不需要担心回调函数的生命周期问题。

  • run() 函数中,当子线程开始执行时,首先会进行一些初始化操作。完成初始化后,子线程调用 thread->m_semaphore.notify(),它通过 notify() 操作将信号量的值增加1,此时如果有线程(主线程)在 wait() 上阻塞,它会被唤醒。即当信号量被通知(值变为大于0),主线程从 m_semaphore.wait() 处返回,继续执行后续的代码。

  • thread->m_semaphore.notify();cb(); 顺序不能变cb() 执行回调,实际上执行了子线程的核心任务。只有在通知主线程子线程初始化完成之后,才能确保回调执行时子线程的状态是安全和一致的