文章目录
如果你在这几节视频中遇到了代码编译问题,那么下列内容会有帮助。
P21:线程01
本节内容主要包括搭建一个基础的线程类,按照视频的代码编译没问题,但是运行后可能出现报错,下列内容有解决方法。
一、方法函数
代码中注释部分仅个人理解
thread.h
#ifndef __SYLAR_THREAD_H__
#define __SYLAR_THREAD_H__
#include <thread>
#include <functional>
#include <memory>
#include <pthread.h>
namespace sylar {
class Thread {
public:
typedef std::shared_ptr<Thread> ptr;
Thread(std::function<void()> cb, const std::string& name);
~Thread();
// 获取线程ID
pid_t getId() const { return m_id; }
// 获取线程名称
const std::string& getName() const { return m_name; }
// 等待线程执行完成
void join();
// 获取当前的线程指针
static Thread* GetThis();
// 获取当前的线程名称
static const std::string& GetName();
// 设置线程名称
static void SetName(const std::string& name);
private:
Thread(const Thread&) = delete;
Thread(const Thread&&) = delete;
Thread& operator=(const Thread&) = delete;
// 线程执行函数
static void* run(void* arg);
private:
pid_t m_id = -1; // 线程ID
pthread_t m_thread = 0; // 线程结构
std::function<void()> m_cb; // 线程执行函数
std::string m_name; // 线程名称
};
}
#endif
thread.cc
#include "thread.h"
#include "log.h"
#include "util.h"
namespace sylar {
// 定义线程局部变量
static thread_local Thread* t_thread = nullptr;
// 定义线程局部变量的名称
static thread_local std::string t_thread_name = "UNKNOW";
// 定义系统日志
static sylar::Logger::ptr g_looger = SYLAR_LOG_NAME("system");
// 获取当前的线程指针
Thread* Thread::GetThis() {
return t_thread;
}
// 获取当前的线程名称
const std::string& Thread::GetName() {
return t_thread_name;
}
// 设置线程名称
void Thread::SetName(const std::string& name) {
if(t_thread) {
t_thread->m_name = name;
}
t_thread_name = name;
}
// 构造函数
Thread::Thread(std::function<void()> cb, const std::string& name)
:m_cb(cb),m_name(name){
if(name.empty()) {
m_name = "UNKNOW";
}
// 第一个参数为pthread_t类型,第二个参数一般为空,用于定制线程的不同属性,第三个参数为线程启动函数(注意类型需要是void*),第四个参数表示线程启动函数的参数值
// 没有时可以设置为null,有的话传入参数地址
// 整个函数正常时返回0
int rt = pthread_create(&m_thread, nullptr, &Thread::run, this);
if(rt) {
SYLAR_LOG_ERROR(g_looger) << "pthread_create thread fail, rt=" << rt << " name=" << name;
throw std::logic_error("pthread_create error");
}
}
// 析构函数
Thread::~Thread() {
if(m_thread){
pthread_detach(m_thread);
}
}
// 等待线程执行完成
void Thread::join() {
if(m_thread) {
int rt = pthread_join(m_thread,nullptr);
if(rt) {
SYLAR_LOG_ERROR(g_looger) << "pthread_join thread fail, rt=" << rt << " m_name=" << m_name;
throw std::logic_error("pthread_join error");
}
m_thread = 0;
}
}
// 线程执行函数
void* Thread::run(void* arg) {
Thread* thread = (Thread*)arg;
t_thread = thread;
t_thread_name = thread->m_name;
thread->m_id = sylar::GetTreadId();//获取线程ID
// 该函数的作用是设置名称的名称,第一个参数:需要设置/获取 名称的线程;第二个参数:要设置/获取 名称的buffer(16个字符长);
pthread_setname_np(pthread_self(), thread->m_name.substr(0,15).c_str());
std::function<void()> cb;
cb.swap(thread->m_cb); // 在不涉及复制或移动可调用对象的情况下,快速地交换两个function的内容
cb();
return 0;
}
}
CmakeList
cmake_minimum_required(VERSION 2.8)
project(sylar)
include (cmake/utils.cmake)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_CXX_FLAGS "$ENV{CXXFLAGS} -rdynamic -O0 -g -std=c++11 -Wall -Wno-deprecated -Werror -Wno-unused-function -Wno-builtin-macro-redefined")
include_directories(.)
include_directories(/root/Web-learning/sylar/include)
link_directories(/root/Web-learning/sylar/lib)
find_library(YAMLCPP yaml-cpp)
message("***", ${YAMLCPP})
set(LIB_SRC
sylar/log.cc
sylar/util.cc
sylar/config.cc
sylar/thread.cc
)
add_library(sylar SHARED ${LIB_SRC})
force_redefine_file_macro_for_sources(sylar) #__FILE__ 重定义该宏为相对路径
#add_library(sylar_static STATIC ${LIB_SRC})
#SET_TARGET_PROPERTIES(sylar_static PROPERTIES OUTPUT_NAME "sylar")
set(LIB_LIB
sylar
pthread
${YAMLCPP}
)
add_executable(test tests/test.cc)
add_dependencies(test sylar)
force_redefine_file_macro_for_sources(test) #__FILE__ 重定义该宏为相对路径
target_link_libraries(test ${LIB_LIB})
add_executable(test_config tests/test_config.cc)
add_dependencies(test_config sylar)
force_redefine_file_macro_for_sources(test_config) #__FILE__ 重定义该宏为相对路径
# target_link_libraries(test_config sylar -L/root/Web-learning/sylar/lib -lyaml-cpp)
target_link_libraries(test_config ${LIB_LIB})
add_executable(test_thread tests/test_thread.cc)
add_dependencies(test_thread sylar)
force_redefine_file_macro_for_sources(test_thread) #__FILE__ 重定义该宏为相对路径
target_link_libraries(test_thread ${LIB_LIB})
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
二、结果展示
按照sylar
本节的代码敲下来后可能遇到下列错误
- core dump(我没遇到,不过看大家评论也能通过下列办法解决)
出现上面两个问题就是m_cb
没有初始化,所以我们在构造函数定义时可以给它一个初始值
Thread::Thread(std::function<void()> cb, const std::string& name)
:m_cb(cb),m_name(name)
编译运行后的结果
查看线程
P22: 线程02
本节内容主要实现互斥量和信号量,因为自己对这些也不太了解,之前仅在网络编程看过系列方法,所以下列内容会有各个库的一些常用方法介绍。
一、方法函数
Semaphore
Semaphore
通常被叫做信号量,它可以在多线程环境下协调各个线程的执行。它的值只有0和1,和互斥量类似,若资源被锁住为0,资源可用为1。信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为 1 就变成互斥锁 Mutex,即同时只能有一个任务可以访问信号量保护的共享资源。
下面介绍一下sylar
在本节中使用到的一些方法函数:
-
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:创建信号量
参数:第一个参数指向一个信号对象;第二个参数控制信号量的类型,0表示信号量是当前进程的局部信号量,否则信号量就可以在多个进程间共享;第三个参数为信号对象的初始值
返回:成功返回0,否则为-1
应用:
sylar
定义的信号量构造函数使用该方法创建信号量,失败时给出错误提示。Semaphore::Semaphore(uint32_t count) { if(sem_init(&m_semaphpre, 0, count)) { throw std::logic_error("sem_init error"); } }
-
int sem_post(sem_t *sem);
功能:信号量值加1
参数:信号量对象
返回:成功0,失败-1;
应用:
Semaphore
的notify
方法void Semaphore::notify() { if(sem_post(&m_semaphpre)) { throw std::logic_error("sem_post error"); } }
-
int sem_wait(sem_t *sem);
功能:信号量的值加-1
参数:信号量对象
返回:成功0,失败-1
应用:
Semaphore
的wait
方法void Semaphore::wait() { if(sem_wait(&m_semaphpre)) { throw std::logic_error("sem_wait error"); } }
以上内容参考博客:Semaphore详解
ScopedLockImpl
该模块是局部锁,目的就是为了实现RAII机制,即调用构造函数的时候,对类型T的锁调用lock函数(T可以是互斥量,自旋锁,或者原子锁) ,超出作用域就调用析构函数,析构函数中对T类型的锁调用unlock函数。
template<class T>
struct ScopedLockImpl {
public:
ScopedLockImpl(T& mutex)
: m_mutex(mutex) {
m_mutex.lock();
m_locked = true;
}
~ScopedLockImpl() {
unlock();
}
void lock() {
if(!m_locked) {
m_mutex.lock();
m_locked = true;
}
}
void unlock() {
if(m_locked) {
m_mutex.unlock();
m_locked = false;
}
}
private:
T& m_mutex;
bool m_locked;
};
**ReadScopedLockImpl **
给读写互斥量加读锁
// 局部读锁模板
template<class T>
struct ReadScopedLockImpl {
public:
ReadScopedLockImpl(T& mutex)
: m_mutex(mutex) {
m_mutex.rdlock();
m_locked = true;
}
~ReadScopedLockImpl() {
unlock();
}
void lock() {
if(!m_locked) {
m_mutex.rdlock();
m_locked = true;
}
}
void unlock() {
if(m_locked) {
m_mutex.unlock();
m_locked = false;
}
}
private:
T& m_mutex;
bool m_locked;
};
**WriteScopedLockImpl **
给读写互斥量加写锁
template<class T>
struct WriteScopedLockImpl {
public:
WriteScopedLockImpl(T& mutex)
: m_mutex(mutex) {
m_mutex.wrlock();
m_locked = true;
}
~WriteScopedLockImpl() {
unlock();
}
void lock() {
if(!m_locked) {
m_mutex.wrlock();
m_locked = true;
}
}
void unlock() {
if(m_locked) {
m_mutex.unlock();
m_locked = false;
}
}
private:
T& m_mutex;
bool m_locked;
};
**RWMutex **
读写互斥量封装类
-
pthread_rwlock_init
功能:初始化读写锁变量
参数:读写锁对象;读写锁初始化时的属性,默认填NULL
-
pthread_rwlock_destroy
功能:释放读写锁;=此函数只是反初始化读写锁变量,并没有释放内存空间,如果读写锁变量是通过malloc等函数申请的,那么需要在free掉读写锁变量之前调用pthread_rwlock_destory函数=
参数:读写锁对象
-
pthread_rwlock_rdlock
功能:在读模式下锁定读写锁
参数:读写锁对象
-
pthread_rwlock_wrlock
功能:在写模式下锁定读写锁
参数:读写锁对象
-
pthread_rwlock_unlock
功能:不管以何种方式锁住读写锁,都可以用这个函数解锁
参数:读写锁对象
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;
};
以上内容参考博客:锁详解
二、结果展示
创建一个计时器count
和一个读写锁s_mutex
int count = 0;
sylar::RWMutex s_mutex;
首先在不加锁的情况下,循环计数100000
次,看count
的结果
void fun1() {
SYLAR_LOG_INFO(g_logger) << "name: " << sylar::Thread::GetName()
<< " this.name: " << sylar::Thread::GetThis()->getName()
<< " id: " << sylar::GetTreadId()
<< " this.id: " << sylar::Thread::GetThis()->getId();
for(int i = 0; i < 100000; ++ i) {
++ count;
}
}
从结果可以看到每一个都不同
然后给它加锁,首先在线程初始化时,必须调用信号量的wait
函数保证离开线程构造函数作用域时所以的线程都已经初始化完毕,然后在线程调用可执行函数之前使用thread->m_semaphore.notify();
下面给刚刚测试的代码加一个写锁
void fun1() {
SYLAR_LOG_INFO(g_logger) << "name: " << sylar::Thread::GetName()
<< " this.name: " << sylar::Thread::GetThis()->getName()
<< " id: " << sylar::GetTreadId()
<< " this.id: " << sylar::Thread::GetThis()->getId();
for(int i = 0; i < 100000; ++ i) {
sylar::RWMutex::WriteLock lock(s_mutex);
++ count;
}
}
从结果看,计数器能一直保持在50000
P23:线程与日志整合01
本节主要内容是将日志与互斥量进行整合,也就是在对日志的属性进行操作时,比如在读取日志格式时我们不希望过程中日志格式被改变,那么在读之前就要加上锁。本节的代码修改内容较少,就不全部列举出来,下列内容主要展示一下加锁和不加锁的区别。
一、方法函数
为了进行测试,我们创建了一个空的互斥量
// 互斥量
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;
};
// 空的Mutex,测试线程安全
class NullMutex {
public:
typedef ScopedLockImpl<NullMutex> Lock;
NullMutex() {}
~NullMutex() {}
void lock() {}
void unlock() {}
};
二、结果展示
相关配置
日志配置文件内容:
logs:
- name: root
level: INFO
appenders:
- type: FileLogAppender
file: /root/Web-learning/sylar/mutex.txt
测试代码:
注意sylar
视频中是无线循环打印,为了防止生成文件过大我这里只设置了100次。
void fun1() {
SYLAR_LOG_INFO(g_logger) << "name: " << sylar::Thread::GetName()
<< " this.name: " << sylar::Thread::GetThis()->getName()
<< " id: " << sylar::GetTreadId()
<< " this.id: " << sylar::Thread::GetThis()->getId();
for(int i = 0; i < 100000; ++ i) {
// sylar::RWMutex::WriteLock lock(s_mutex);
sylar::Mutex::Lock lock(s_mutex);
++ count;
}
}
void fun2() {
auto i = 100;
while(-- i) {
SYLAR_LOG_INFO(g_logger) << "HHHHHHHHHHHHHHHHHHHHHHHHYYYYYYYYYYYYYYYYY";
}
}
void fun3() {
auto i = 100;
while(-- i) {
SYLAR_LOG_INFO(g_logger) << "=========================================";
}
}
int main(int argc, char** argv) {
SYLAR_LOG_INFO(g_logger) << "thread test begin";
YAML::Node root = YAML::LoadFile("/root/Web-learning/sylar/bin/conf/log2.yml");
sylar::Config::LoadFromYaml(root);
std::vector<sylar::Thread::ptr> thrs; // 定义线程池
for(int i = 0; i < 2; ++ i) {
sylar::Thread::ptr thr(new sylar::Thread(&fun2, "name_" + std::to_string(i * 2)));
sylar::Thread::ptr thr2(new sylar::Thread(&fun3, "name_" + std::to_string(i * 2 + 1)));
thrs.push_back(thr);
thrs.push_back(thr2);
}
// sleep(20);
for(size_t i = 0; i < thrs.size(); ++ i) {
thrs[i]->join();
}
SYLAR_LOG_INFO(g_logger) << "thread test end";
SYLAR_LOG_INFO(g_logger) << "count = " << count;
return 0;
}
加互斥量
从生成的mutex.txt
看,一共有398条日志文件
不加互斥量
如果我们没有加锁,则会生成823条日志文件,说明有的日志被重复输出
P24:线程与日志整合02
本节主要内容有下列两点:
- 使用自旋锁
spinLock
去优化锁的性能 - 解决生成日志文件过程中,如果生成文件被删除,程序依旧占据磁盘且无法再重新打开日志文件。
一、方法函数
spinLock
sylar
进行了加锁与不加锁简单了测试,前者写速度约为8Mb/s,后者约为20Mb/s,所以要使用自旋锁进行优化。简单说一下互斥锁和自旋锁的区别(参考)
- 互斥锁:
- 互斥锁是一种独占锁,当线程A加锁成功后,此时互斥锁已经被线程A独占了,只要线程A没有释放手中的锁,线程B就会失败,就会释放掉CPU给其他线程,线程B加锁的代码就会被阻塞。互斥锁加锁失败而阻塞是由操作系统内核实现的,当加锁失败后,内核将线程置为睡眠状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程加锁成功后就可以继续执行。
- 性能开销成本:两次线程上下文切换的成本。当线程加锁失败时,内核将线程的状态从【运行】切换到睡眠状态,然后把CPU切换给其他线程运行;当锁被释放时,之前睡眠状态的线程会变成就绪状态,然后内核就会在合适的时间把CPU切换给该线程运行;
- 自旋锁:
- 自旋锁通过CPU提供的CAS,在用户态完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些开销小一些。
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;
};
日志输出优化
void FileLogAppender::log(std::shared_ptr<Logger> logger,LogLevel::Level level, LogEvent::ptr event) {
if(level >= m_level) {
uint64_t now = time(0);
if(now != m_lastTime) {
reopen();
m_lastTime = now;
}
MutexType::Lock lock(m_mutex);
if(!(m_filestream << m_formatter->format(logger,level,event))) {
std::cout << "file error!" << std::endl;
}
}
}
二、结果展示
优化文件输出前
可以看到在运行测试文件不断往mutex.txt
写入日志时,如果人为删除该文件,那么之后对文件的操作都会报错,并且程序依旧在运行。
优化后
保证日志文件在被删除后程序可以程序重新生成,成功解决该问题。但是sylar
这里是通过周期性不断使用reopen()
去重新打开文件,如果在文件删除时才通知程序重新生成文件性能会好一点。
P25:线程与配置整合
本节内容主要是给配置中一些操作加锁,详细的代码就不在下列展示,没什么需要注意的。
一、结果展示
可以把我们定义的配置类全部读取出来
总结
线程部分就结束了,几节视频看下来对如何搭建一个基本的线程类、创建和使用不同的锁有了初步的了解。对C++中各个线程的库函数仅仅有了了解,如果自己去使用可能也比较困难,后期等整个视频看完后再去找相关资料弥补,然后再会回来看这几节代码肯定会有很大提升。