C++高性能服务器框架之线程模块
作者:找人找不到北
线程模块概述
该模块基于pthread
实现。sylar说,由于c++11中的thread也是由pthread封装实现的,并且没有提供读写互斥量,读写锁,自旋锁等,所以自己封装了pthread。
锁模块实现了信号量、互斥量、读写锁、自旋锁、原子锁的封装
- class Semaphore:信号量封装
- class Mutex:互斥量封装
- class RWMutex:读写锁封装
- class Spinlock:自旋锁封装
- class CASLock:原子锁封装
线程模块主要由Thread类实现
- class Thread:实现线程的封装
关于线程id的问题,在获取线程id时使用syscall获得唯一的线程id
进程pid: getpid() 线程tid: pthread_self() //进程内唯一,但是在不同进程则不唯一。 线程pid: syscall(SYS_gettid) //系统内是唯一的
锁模块详解
class Semaphore(信号量)
mumber(成员函数)
// 信号量,它本质上是一个长整型的数 sem_t m_semaphore;
Semaphore(构造函数)
初始化信号量。函数原型:int sem_init(sem_t *sem, int pshared, unsigned int value);
其中,参数 sem
是指向要初始化的信号量的指针;参数 pshared
指定了信号量是进程内共享还是跨进程共享,如果值为 0,则表示进程内共享;参数 value
是信号量的初始值。该函数成功时返回 0,否则返回 -1,并设置适当的错误码。
Semaphore::Semaphore(uint32_t count) { if (sem_init(&m_semaphore, 0, count)) { throw std::logic_error("sem_init error"); } }
~Semaphore(析构函数)
销毁信号量。函数原型:int sem_destroy(sem_t *sem);
注意,只有在确保没有任何线程或进程正在使用该信号量时,才应该调用 sem_destroy()
函数。否则,可能会导致未定义的行为。此外,如果在调用 sem_destroy()
函数之前,没有使用 sem_post()
函数将信号量的值增加到其初始值,则可能会导致在销毁信号量时出现死锁情况。
Semaphore::~Semaphore() { sem_destroy(&m_semaphore); }
wait(获取信号量)
函数原型:int sem_wait(sem_t *sem);
其中,参数 sem
是指向要获取的信号量的指针。如果在调用此函数时信号量的值大于零,则该值将递减并立即返回。如果信号量的值为零,则当前线程将被阻塞,直到信号量的值大于零或者被信号中断。
当线程成功获取信号量时,可以执行相应的操作来使用资源。使用完资源后,可以通过调用 sem_post()
函数来增加信号量的值以释放资源,并使其他等待线程得以继续执行。
void Semaphore::wait() { if (sem_wait(&m_semaphore)) { throw std::logic_error("sem_wait error"); } }
notify(释放信号量
函数原型:int sem_post(sem_t *sem);
用于向指定的命名或未命名信号量发送信号,使其计数器加1。如果有进程或线程正在等待该信号量,那么其中一个将被唤醒以继续执行。
参数:sem
:指向要增加计数器的信号量的指针。
返回值:成功时返回0,失败时返回-1,并设置errno来指示错误原因。
void Semaphore::notify() { if (sem_post(&m_semaphore)) { throw std::logic_error("sem_post error"); } }
锁
为方便封装各种锁,这里定义了3个结构体,都在构造函数时自动lock
,在析构时自动unlock
,这样可以简化锁的操作,避免忘记解锁导致死锁。
- ScopedLockImpl:用来分装互斥量,自旋锁,原子锁
- ReadScopedLockImpl && WriteScopedLockImpl:用来封装读写锁
class Mutex(互斥量)
mumber(成员函数)
// 互斥量 pthread_mutex_t m_mutex;
Mutex(构造函数)
初始化互斥锁对象。函数原型:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
参数说明:
mutex
:指向要初始化的互斥锁对象的指针。attr
:指向互斥锁属性对象的指针,可以为NULL以使用默认属性。
返回值:
- 成功时返回0,失败时返回错误码并设置errno变量。
Mutex () { pthread_mutex_init(&m_mutex, nullptr); }
~Mutex(析构函数)
销毁已初始化的互斥锁对象。函数原型:int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数说明:
mutex
:指向要销毁的互斥锁对象的指针。
返回值:
- 成功时返回0,失败时返回错误码并设置errno变量。
~Mutex () { pthread_mutex_destroy(&m_mutex); }
lock(加锁)
函数原型:int pthread_mutex_lock(pthread_mutex_t *mutex);
参数说明:
mutex
:指向要加锁的互斥锁对象的指针。
返回值
- 成功时返回0,失败时返回错误码并设置errno变量。
当一个线程调用pthread_mutex_lock()
时,如果当前该互斥锁没有被其它线程持有,则该线程会获得该互斥锁,并将其标记为已被持有;如果该互斥锁已经被其它线程持有,则当前线程会被阻塞,直到该互斥锁被释放并重新尝试加锁。
void lock() { pthread_mutex_lock(&m_mutex); }
unlock(解锁)
函数原型:int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数说明:
mutex
:指向要解锁的互斥锁对象的指针。
返回值:
- 成功时返回0,失败时返回错误码并设置errno变量。
当一个线程调用pthread_mutex_unlock()
时,该互斥锁将被标记为未被持有,并且如果有其它线程正在等待该锁,则其中一个线程将被唤醒以继续执行。
void unlock() { pthread_mutex_unlock(&m_mutex); }
class RWMutex(读写锁)
mumber(成员变量)
// 读写锁 pthread_rwlock_t m_lock;
RWMutex(构造函数)
初始化一个读写锁对象。函数原型:int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
参数说明:
rwlock
:指向要初始化的读写锁对象的指针。attr
:指向读写锁属性对象的指针,可以为NULL以使用默认属性。
返回值:
- 成功时返回0,失败时返回错误码并设置errno变量。
读写锁是一种同步机制,用于在多线程环境下对共享资源进行访问控制。与互斥锁不同,读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这样可以提高程序的性能和效率,但需要注意避免读写锁死锁等问题。
RWMutex() { pthread_rwlock_init(&m_lock, nullptr); }
~RWMutex(析构函数)
销毁一个读写锁。
~RWMutex() { pthread_rwlock_destroy(&m_lock); }
rdlock(加读锁)
pthread_rwlock_rdlock()
用于获取读取锁(pthread_rwlock_t)上的共享读取访问权限。它允许多个线程同时读取共享资源,但不能写入它。如果有线程已经持有写入锁,则其他线程将被阻塞直到写入锁被释放。调用此函数时,如果另一个线程已经持有写入锁,则该线程将被阻塞,直到写入锁被释放。
void rdlock() { pthread_rwlock_rdlock(&m_lock); }
wrlock(加写锁)
pthread_rwlock_wrlock()
用于获取写入锁(pthread_rwlock_t)上的排他写访问权限。它阻止其他线程读取或写入共享资源,直到该线程释放写入锁。如果有其他线程已经持有读取或写入锁,则调用此函数的线程将被阻塞,直到所有的读取和写入锁都被释放。
void wrlock() { pthread_rwlock_wrlock(&m_lock); }
unlock(解锁)
pthread_rwlock_unlock()
用于释放读取或写入锁(pthread_rwlock_t)。它允许其他线程获取相应的锁来访问共享资源。如果当前线程没有持有读取或写入锁,则调用pthread_rwlock_unlock将导致未定义的行为。此外,如果已经销毁了读写锁,则再次调用pthread_rwlock_unlock也会导致未定义的行为。在使用pthread_rwlock_t时,需要注意正确地获取和释放读取或写入锁,以确保多个线程可以正确地访问共享资源。
void unlock() { pthread_rwlock_unlock(&m_lock); }
class Spinlock(自旋锁)
与mutex不同,自旋锁不会使线程进入睡眠状态,而是在获取锁时进行忙等待,直到锁可用。当锁被释放时,等待获取锁的线程将立即获取锁,从而避免了线程进入和退出睡眠状态的额外开销。
mumber(成员变量)
// 自旋锁 pthread_spinlock_t m_mutex;
Spinlock(构造函数)
函数pthread_spin_init(&m_mutex, 0)
是用于对自旋锁进行初始化的函数,其中第一个参数&m_mutex
表示要初始化的自旋锁变量,第二个参数0
表示使用默认的属性。在调用pthread_spin_init
函数之前,必须先分配内存空间来存储自旋锁变量。与pthread_rwlock_t
类似,需要在使用自旋锁前先进行初始化才能正确使用。
Spinlock() { pthread_spin_init(&m_mutex, 0); }
~Spinlock(析构函数)
pthread_spin_destroy()
用于销毁自旋锁(pthread_spinlock_t)。在不再需要自旋锁时,可以使用pthread_spin_destroy函数将其销毁。该函数确保在销毁自旋锁之前所有等待的线程都被解除阻塞并返回适当的错误码。如果自旋锁已经被销毁,则再次调用pthread_spin_destroy将导致未定义的行为。
~Spinlock() { pthread_spin_destroy(&m_mutex); }
lock(加锁)
pthread_spin_lock()
用于获取自旋锁(pthread_spinlock_t)上的排他访问权限。与mutex
不同,自旋锁在获取锁时忙等待,即不断地检查锁状态是否可用,如果不可用则一直循环等待,直到锁可用。当锁被其他线程持有时,调用pthread_spin_lock()
的线程将在自旋等待中消耗CPU时间,直到锁被释放并获取到锁。
void lock() { pthread_spin_lock(&m_mutex); }
unlock(解锁)
pthread_spin_unlock()
用于释放自旋锁(pthread_spinlock_t)。调用该函数可以使其他线程获取相应的锁来访问共享资源。与mutex
不同,自旋锁在释放锁时并不会导致线程进入睡眠状态,而是立即释放锁并允许等待获取锁的线程快速地获取锁来访问共享资源,从而避免了线程进入和退出睡眠状态的额外开销。
void unlock() { pthread_spin_unlock(&m_mutex); }
class CASLock(原子锁)
mumber(成员变量)
// m_mutex是一个原子布尔类型,具有特殊的原子性质,可以用于实现线程间同步和互斥。 // volatile关键字表示该变量可能会被异步修改,因此编译器不会对其进行优化,而是每次都从内存中读取该变量的值。 volatile std::atomic_flag m_mutex;
CASLock(构造函数)
atomic_flag.clear()是C++标准库中的一个原子操作函数,用于将给定的原子标志位(atomic flag)清除或重置为未设置状态。
在多线程编程中,原子标志位通常用于实现简单的锁机制,以确保对共享资源的访问是互斥的。使用atomic_flag.clear()可以轻松地重置标志位,使之再次可用于控制对共享资源的访问。需要注意的是,由于该函数是一个原子操作,因此可以安全地在多个线程之间使用,而无需担心竞态条件和数据竞争等问题。
CASLock () { m_mutex.clear(); }
lock(加锁)
std::atomic_flag_test_and_set_explicit()
是C++标准库中的一个原子操作函数,用于测试给定的原子标志位(atomic flag)是否被设置,并在测试后将其设置为已设置状态。该函数接受一个指向原子标志位对象的指针作为参数,并返回一个布尔值,表示在调用函数前该标志位是否已经被设置。第二个可选参数order用于指定内存序,以控制原子操作的内存顺序和同步行为。通过循环等待实现了互斥锁的效果。
std::memory_order_acquire
是C++中的一种内存序,用于指定原子操作的同步和内存顺序。具体来说,使用std::memory_order_acquire
可以确保在当前线程获取锁之前,所有该线程之前发生的写操作都被完全同步到主内存中。这样可以防止编译器或硬件对写操作进行重排序或延迟,从而确保其他线程可以正确地读取共享资源的最新值。
void lock() { while (std::atomic_flag_test_and_set_explicit(&m_mutex, std::memory_order_acquire)); }
unlock(解锁)
atomic_flag_clear_explicit()
是C++标准库中的一个原子操作函数,用于将给定的原子标志位(atomic flag)清除或重置为未设置状态。该函数接受一个指向原子标志位对象的指针作为参数,并使用可选的第二个参数order来指定内存序,以控制原子操作的同步和内存顺序。
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";
mumber(成员变量)
// 指向当前线程 static thread_local Thread *t_thread = nullptr; // 指向线程名称 static thread_local std::string t_thread_name = "UNKNOW";
Thread(构造函数)
初始化线程执行函数、线程名称,创建新线程。
函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
该函数接受四个参数:
thread
:指向pthread_t类型的指针,用于返回新线程的ID。attr
:指向pthread_attr_t类型的指针,该结构体包含一些有关新线程属性的信息。可以将其设置为NULL以使用默认值。start_routine
:是指向新线程函数的指针,该函数将在新线程中运行。该函数必须采用一个void类型的指针作为参数,并返回一个void类型的指针。arg
:是指向新线程函数的参数的指针。如果不需要传递参数,则可以将其设置为NULL。
调用pthread_create函数后,将会创建一个新线程,并开始执行通过start_routine传递给它的函数。新线程的ID将存储在thread指向的变量中。请注意,新线程将在与调用pthread_create函数的线程并发执行的情况下运行。
Thread::Thread(std::function<void()> cb, const std::string &name) :m_cb(cb) ,m_name(name) { if (m_name.empty()) { m_name = "UNKNOW"; } // 创建新线程,并将其与Thread::run方法关联,创建的新线程对象this作为参数传给run方法 int rt = pthread_create(&m_thread, nullptr, &Thread::run, this); if (rt) { SYLAR_LOG_ERROR(g_logger) << "pthread_creat thread fail, rt = " << rt << "name = " << name; throw std::logic_error("pthread_creat error"); } // 在出构造函数之前,确保线程先跑起来, 保证能够初始化id m_semaphore.wait(); }
~Thread(析构函数)
首先检查m_thread
是否存在,如果存在,则调用pthread_detach(m_thread)
函数来分离已经结束的线程。pthread_detach
函数用于释放与线程关联的资源,并确保线程可以安全地终止。通过在析构函数中分离线程,可以避免在主线程退出时出现悬挂线程,从而防止内存泄漏和其他问题。
Thread::~Thread() { if (m_thread) { pthread_detach(m_thread); } }
join(等待线程执行完成)
pthread_join
用于等待指定线程的终止,并获取该线程的返回值。它的原型为:
#include <pthread.h> int pthread_join(pthread_t thread, void **retval);
该函数接受两个参数:
thread
:要等待的线程ID。retval
:指向指针的指针,用于存储线程返回的值。如果不需要获取返回值,则可以将其设置为NULL。
当调用 pthread_join() 时,当前线程会阻塞,直到指定的线程完成执行。一旦线程结束,当前线程就会恢复执行,并且可以通过 retval
参数来获取线程的返回值。如果不关心线程的返回值,也可以将 retval
参数设置为 NULL
。成功:返回 0 表示线程成功退出。
总之,pthread_join
函数是一种阻塞机制,用于等待指定线程的终止并获取其返回值,同时负责回收线程所使用的资源。
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,可以避免这种情况的发生。同时,这也可以保证线程的安全性和稳定性。
总结
- 对日志系统的临界资源进行互斥访问时,使用自旋锁而不是互斥锁。
- mutex使用系统调用将线程阻塞,并等待其他线程释放锁后再唤醒它,这种方式适用于长时间持有锁的情况。而spinlock在获取锁时忙等待,即不断地检查锁状态是否可用,如果不可用则一直循环等待,因此适用于短时间持有锁的情况。
- 由于mutex会将线程阻塞,因此在高并发情况下可能会出现线程频繁地进入和退出睡眠状态,导致系统开销大。而spinlock虽然不会使线程进入睡眠状态,但会消耗大量的CPU时间,在高并发情况下也容易导致性能问题。
- 另外,当一个线程尝试获取已经被其他线程持有的锁时,mutex会将该线程阻塞,而spinlock则会在自旋等待中消耗CPU时间。如果锁的持有时间较短,则spinlock比mutex更适合使用;如果锁的持有时间较长,则mutex比spinlock
- 在构造函数中创建子进程并等待其完成执行是一种常见的技术,可以通过信号量(Semaphore)来实现主线程等待子线程完成。
- 首先,在主线程中创建一个Semaphore对象并初始化为0。然后,在构造函数中创建子线程,并将Semaphore对象传递给子线程。子线程将执行所需的操作,并在最后使用Semaphore对象发出信号通知主线程它已经完成了工作。
- 主线程在构造函数中调用Semaphore对象的wait方法,这会使主线程阻塞直到收到信号并且Semaphore对象的计数器值大于0。当子线程发出信号时,Semaphore对象的计数器值增加1,因此主线程可以继续执行构造函数的剩余部分。
以上就是C++高性能服务器框架之线程模块的详细内容,更多关于C++ 线程模块的资料请关注脚本之家其它相关文章!