Linux多线程——利用C++模板对pthread线程库封装

1k words

线程封装

我们之前介绍过pthread的线程库,这个线程库主要是基于C语言的void*指针来进行传参和返回

我们使用C++的模板对其封装可以让他的使用更加方便,并且经过测试可以让我们更加直观的了解到线程互斥和同步的重要性

主要框架

要对线程库进行封装,但是首先这个线程库的基本功能肯定要有

void*使用模板解决,函数指针使用包装器解决

那么基本框架就能搭建出来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#pragma once

#include<iostream>
#include<string>
#include<functional>
#include<pthread.h>

template<class T>
using Func_t = std::function<void(T)>; // 参数类型为T 返回值为空的函数

template<class T>
class Thread{
public:

Thread(const std::string& tname, Func_t<T> func, T data)
: _Tid(0)
, _ThreadName(tname)
, _IsRunning(false)
, _Func(func)
, _Data(data)
{}

~Thread()
{}

bool Start() // 线程启动
{}

bool Join() // 线程等待
{}

private:
pthread_t _Tid; // 线程id 也可以用LWP来表示,主要是为了区分不同线程
std::string _ThreadName; // 线程名称
bool _IsRunning; // 区分线程运行状态
Func_t<T> _Func; // 回调函数
T _Data;
};

线程启动

因为这个类创建之后并没有真正创建线程,没有分配线程id,而Start作为主线程需要完成的任务就是创建新线程,更改线程的运行状态,返回线程创建成功与否

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool Start()
{
bool Start() // 线程启动
{
int n = pthread_create(&_Tid, nullptr, ThreadRoutine, this);
if(n==0)
{
_IsRunning = true;
return true;
}
else
return false;
}
}

这里需要注意的点是,传入的参数直接就是this指针,相当于把整个对象传进去了

这是为什么呢

首先ThreadRoutine是需要设置在类内设置的,为了安全性考虑

那么他如果作为类内的成员函数,他的参数就是this指针和一个void*的指针

这样明显是不符合pthread_create对这个函数的要求,一个解决办法就是将其设置为静态成员函数,另一个办法就是设置在类外了

这样一来,作为静态成员函数是无法直接使用类内成员的,也就是无法使用这个回调函数,因此将this指针作为参数传递进去是一个很好的选择

1
2
3
4
5
6
static void* ThreadRoutine(void* args)
{
Thread* tp = static_cast<Thread*>(args);
tp->_Func(tp->_Data); // 调用回调函数
return nullptr;
}

线程等待

1
2
3
4
5
6
7
8
9
10
11
12
bool Join()
{
if (!_IsRunning) // 如果新线程已经结束运行了,那就没有等待的必要了
return true;
int n = pthread_join(_Tid, nullptr);
if (n == 0)
{
_IsRunning = false;
return true;
}
return false;
}

等待的代码就比较简单了

其他信息

1
2
3
4
5
6
7
8
9
std::string GetThreadName()
{
return _ThreadName;
}

bool IsRunning()
{
return _IsRunning;
}

这样我们就封装了一个最简单的线程库

测试函数

我们假设自己写了一个抢票逻辑,采用多线程的方法对其进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <unistd.h>
#include <vector>
#include <cstdio>
#include "Thread.hpp"

int ticket = 10000;

std::string ThreadName()
{
static int number = 1;
char name[64];
snprintf(name, sizeof(name), "Thread[%d]", number);
return name;
}

void BuyTicket(int mutex)
{
while (true)
{
if (ticket > 0)
{
usleep(1000); // 假设访问花费的时间
ticket--;
printf("剩余票数:%d\n", ticket);
}
else
{
break;
}
}
}

int main()
{
std::vector<Thread<int>> Ts;
for (int i = 0; i < 5; i++)
{
// Thread(const std::string &tname, Func_t<T> func, T data)
std::string name = ThreadName();
Thread<int> tmp(name, BuyTicket, 0);
Ts.push_back(tmp);
}

for (int i = 0; i < 5; i++)
{
Ts[i].Start();
}
for (int i = 0; i < 5; i++)
{
Ts[i].Join();
}
return 0;
}

这里我们模拟了五个线程,抢10000张票

运行结果是这样的

image.png

这里出现了离谱的情况,我们明明设置了,当票数检测到小于等于0时会跳出循环

但是还是依然抢到了剩下的票

如果我们直观理解的话,其实就是在判断的时候,在五个线程都只剩下了1张票时,几乎同时进了这个判断

然后再轮流执行导致了票数减少

要解决这样的问题就需要互斥锁

因为票是共享的,有限的资源,需要对其进行保护

下一篇我们会介绍Linux线程互斥和同步的实现方法

Comments