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