Linux OS:线程封装 | RAII封装锁 | 随机数运算任务封装
一、Linux OS:线程封装
1.1 线程私有成员
现在我们手动封装一个线程,其中类成员变量包含:线程id、线程名、线程运行状态(是否允许)、线程数据、创建线程的回调方法!具体如下:
template <class T>
using func_t = std::function<void(T)>; //回调方法重命名
template <class T>
class Thread
{
private:
pthread_t _tid; // 线程id
std::string _threadname; // 线程名
bool _isrunning; // 线程运行状态(是否允许)
func_t<T> _func; // 创建出的新线程执行该回调函数
T _data; // 线程数据
};
1.2 线程成员函数
对于线程我们将实现下面几个成员方面:构造函数初始化相关数据(新创建的线程没有启动运行)、启动线程接口、线程等待接口、获取线程名!
1)构造函数初始化相关数据
对于构造函数,我们通过外部用户传入线程名、执行方法、线程数据!
Thread(const std::string& threadname, func_t<T> func, T data)
:_tid(0), _threadname(threadname), _isrunning(false)
, _func(func), _data(data)
{}
2)启动线程接口
线程默认是没有运行的,我们通过在启动线程接口
调用pthread_create
函数来创建线程,通过构造函数获取到的数据,将相关数据传递给pthread_create
函数。
static void *ThreadRoution(void *args)
{
Thread *ts = static_cast<Thread *>(args);
ts->_func(ts->_data);
}
void Start()
{
int n = pthread_create(&_tid, nullptr, ThreadRoution, this);
if(n == 0)
{
_isrunning = true;
}
}
为什么我们需要这样设计,在创建时将执行函数设计为静态的,并将this指针作为参数传递给线程执行函数?
原因在于编译器会自动传递一个this指针给类成员函数,并且是第一个参数!所以我们将线程执行函数设计为静态的,静态函数会存在this指针。但我们需要在该函数中调用线程对象的执行方法,所以我们将类对象(this)作为参数传给静态函数!之后在对参数强转即可获得线程对象,然后既可以调用相关执行方法了!
3)线程等待
只有对启动的线程进行等待才是有意义的!这也是我们为啥在类中添加表示线程状态的成员变量
的原因之一!
void Join()
{
if(_isrunning)
{
int n = pthread_join(_tid, nullptr);//等待线程,等待成功返回0
if(n == 0)
_isrunning = false;
}
}
4)获取线程名
const std::string& GetThreadName()
{
return _threadname;
}
1.3 总体代码
template <class T>
using func_t = std::function<void(T)>;
template <class T>
class Thread
{
public:
Thread(const std::string& threadname, func_t<T> func, T data)
:_tid(0), _threadname(threadname), _isrunning(false), _func(func), _data(data)
{}
static void *ThreadRoution(void *args)
{
Thread *ts = static_cast<Thread *>(args);
ts->_func(ts->_data);
}
void Start()
{
int n = pthread_create(&_tid, nullptr, ThreadRoution, this);
if(n == 0)
{
_isrunning = true;
}
}
void Join()
{
if(_isrunning)
{
int n = pthread_join(_tid, nullptr);
if(n == 0)
_isrunning = false;
}
}
const std::string& GetThreadName()
{
return _threadname;
}
~Thread()
{}
private:
pthread_t _tid;
std::string _threadname;
bool _isrunning;
func_t<T> _func;
T _data;
};
二、RAII封装锁
在实际项目中,一般不建议用户自己持有锁,释放锁。这样做的后果就是:一旦加锁和解锁之间的代码抛异常,将会导致死锁,进而导致内存泄漏!
所以我们一般采用RAII思想。对于临界区加锁时,我们将锁交给对象(LockGuard
)来管理。其中LockGuard
只存在一个锁成员函数。用户将锁传递给LockGuard
对象后,构造函数初始化列表时将锁的对象创建出来,然后在函数体中进行加锁,在析构函数中封装解锁方法!
class Mutex
{
public:
Mutex(pthread_mutex_t *lock):_lock(lock)
{}
void Lock()
{
pthread_mutex_lock(_lock);
}
void Unlock()
{
pthread_mutex_unlock(_lock);
}
~Mutex()
{}
private:
pthread_mutex_t *_lock;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *lock): _mutex(lock)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex _mutex;
};
三、随机数运算任务疯转
下面我们疯转一个类:我们传入两个值和运算符后对数据进行处理。但运算过程中,用户传入运算符可能未知,可能出现除0,模0错误…所以除了运算结果外,我们添加一个_code
的成员变量,用于判断结构是否可信!
然后我们用一个枚举集合表示结果集合(0表示结果可信,非0对应相应错误)。然后我们不仅可以通过_code
成员变量判断结果是否可行,一旦结果不可信还可以获取到出错原因。
这里我们提供了打印任务和打印运算结果接口、仿函数等接口。比较简单,就不多说了!
const int defaultResult = 0;
enum
{
ok = 0,
div_zero,
mod_zero,
unknow
};
class Task
{
public:
Task()
{}
Task(int data_x, int data_y, char op)
: _data_x(data_x), _data_y(data_y), _op(op), _result(ok), _code(defaultResult)
{
}
void Run()
{
switch (_op)
{
case '+':
_result = _data_x + _data_y;
break;
case '-':
_result = _data_x - _data_y;
break;
case '*':
_result = _data_x * _data_y;
break;
case '/':
{
if (_data_y == 0)
_code = div_zero;
else
_result = _data_x / _data_y;
break;
}
case '%':
{
if (_data_y == 0)
_code = mod_zero;
else
_result = _data_x % _data_y;
break;
}
default:
_code = unknow;
}
}
void operator()()
{
Run();
}
std::string PrintTask()
{
std::string s;
s += std::to_string(_data_x);
s += _op;
s += std::to_string(_data_y);
s += "=?";
return s;
}
std::string PrintResult()
{
std::string s;
s += std::to_string(_data_x);
s += _op;
s += std::to_string(_data_y);
s += "=";
s += std::to_string(_result);
s += ", [";
s += std::to_string(_code);
s += "]";
return s;
}
~Task()
{
}
private:
int _data_x;
int _data_y;
char _op;
int _result;
int _code; // 错误码为0,结果可行,否则不可信
};