操作系统:ubuntu20.04LTS
使用头文件:pthread.h
在之前的文章中已经实现了C++不可复制基类和线程同步类的封装,本文章内容实现对线程的封装。
需求
首先考虑要封装的线程类包含哪些内容,也就是明确类的属性和 方法。需要一个能够在操作系统唯一标识这个线程的id以及线程结构;为了方便追踪线程,可以给线程一个名称;为了控制线程同步,给一个信号量用来阻塞和唤醒线程;还有最重要的线程执行函数。
线程属性
- 唯一标识线程的id和线程结构
- 线程的名称
- 线程的回调函数
- 信号量
现在考虑需要哪些方法,首先需要构造函数和析构函数,在构造函数中需要制定线程的名称和回调函数,析构函数中需要释放线程所持有的资源;对于线程名称,提供访问和设置的方法;线程常用函数join比必不可少;为了更方便的管理线程,还提供了下面几个函数:1-返回当前正在执行的线程指针;2-设置当前正在执行的线程名词;3-返回当前正在执行的线程名称;4-返回线程的id值。这四个函数为类服务,而不是服务于某个实例,使用static关键字修饰。
那么如何追踪正在执行的线程和现存名词呢?答案是利用C++11提供的关键字thread_local。
thread_local关键字
在C++11中,thread_local
是一个关键字,用于声明线程局部存储(TLS)变量。TLS变量是一种特殊类型的变量,每个线程都有其自己的独立实例,每个线程对该变量的访问都是独立的,不会影响其他线程的该变量的值。thread_local
关键字可以用于以下三个地方:
1. 全局变量:如果你在全局范围内声明一个变量,并使用thread_local
关键字,那么每个线程都会有该变量的一个独立实例。
2. 静态变量:如果你在一个函数内部或类的成员函数内部声明一个静态变量,并使用thread_local
关键字,那么每个线程都会有该静态变量的一个独立实例。
3. 类的非静态成员变量:如果你在类的定义中声明一个非静态成员变量,并使用thread_local
关键字,那么每个对象的该成员变量都会有一个独立的实例,而且每个线程都会有属于自己的该成员变量的实例。
使用thread_local
关键字可以帮助你在多线程程序中管理数据的共享和隔离,每个线程都有自己的数据副本,避免了数据竞争和线程安全性问题。
举一个例子:下面的类中提供了两个函数,如果创建两个线程分别执行function1或function2函数,那么function1中执行两次后的结果count依然是5,第一个线程执行5次加一后count的值为5,第二个线程依然执行5次加一操作,但是两个线程之间是隔离的,可以看作线程1有一个count,线程2有一个count,这两个分别被执行了5次加一。而function2中static初始化一次,被整个类共享。只有一个count,线程1执行5次加一后线程2对同样的count执行了5次加一,最终得到10.
#include <iostream>
#include <thread>
class Test {
public:
Test() = default;
~Test() = default;
void function1() {
thread_local int count = 0;
for(int i = 0; i < 5; ++i) {
++count;
}
}
void function2() {
static int count = 0;
for(int i = 0; i < 5; ++i) {
++count;
}
}
};
线程方法
- 构造函数
- 析构函数
- join函数
- 设置线程名称函数
- 返回线程名称函数
- 返回现存id
- 设置正在执行线程的名称
- 返回正在执行线程的名称
- 将当前线程设置为正在执行的线程
- 线程内部回调函数
现在我们已经明确了需求,开始实现代码:
#pragma once
#include <pthread.h>
#include <string.h>
#include <functional>
#include <unistd.h>
#include <thread>
#include <sys/syscall.h>
#include "lock.h"
#include "uncopyable.h"
class Thread : Uncopyable {
public:
// 构造函数
Thread(const std::string& name, std::function<void()> callback_func);
// 析构函数
~Thread();
// 设置线程名称
void setName(const std::string& name);
// 返回线程pid
const pid_t getPID() const;
// 返回线程名称
const std::string& getName() const;
// 等待资源回收
void join();
// 设置正在执行线程名称
static void SetCurrentThreadName(const std::string& name);
// 获得正在执行的线程名称
static const std::string& GetCurrentThreadName();
// 获取正在执行的线程的指针
static Thread* GetCurrentThreadPtr();
// 返回线程pid
static const pid_t GetPID();
// 返回线程id
static const std::thread::id GetThreadID();
private:
// 线程绑定的执行函数
static void* callbackFunc(void* arg);
private:
// 进程ID,系统唯一标识
pid_t pid_;
// 线程结构,进程中唯一标识线程
pthread_t pthread_;
// 线程名
std::string name_;
// 线程内部执行函数
std::function<void()> callback_func_;
// 信号量,用于阻塞和唤醒线程
Semaphore semaphore_;
};
#include "thread.h"
static thread_local Thread* current_thread = nullptr;
static thread_local std::string current_thread_name = "UNKNOWN";
// 构造函数
Thread::Thread(const std::string& name, std::function<void()> callback_func):
name_(name),
callback_func_(callback_func) {
if(name.empty()) {
name_ = "UNKNOWN";
}
if(pthread_create(&pthread_, nullptr, &callbackFunc, this)) {
throw std::runtime_error("pthread_create error");
}
semaphore_.wait();
}
// 析构函数
Thread::~Thread() {
if(pthread_) {
pthread_detach(pthread_);
}
}
// 设置线程名称
void Thread::setName(const std::string& name) {
if(name.empty()) {
return;
}
name_ = name;
}
// 返回线程pid
const pid_t Thread::getPID() const {
return pid_;
}
// 返回线程名称
const std::string& Thread::getName() const {
return name_;
}
// 等待资源回收
void Thread::join() {
if(pthread_join(pthread_, nullptr)) {
throw std::runtime_error("pthread_join error");
}
pthread_ = 0;
}
// 设置正在执行线程名称
void Thread::SetCurrentThreadName(const std::string& name) {
if(name.empty()) {
return;
}
if(current_thread) {
current_thread->name_ = name;
}
current_thread_name = name;
}
// 设置正在执行的线程名称
const std::string& Thread::GetCurrentThreadName() {
return current_thread_name;
}
// 获取正在执行的线程的指针
Thread* Thread::GetCurrentThreadPtr() {
return current_thread;
}
// 线程绑定的执行函数
void* Thread::callbackFunc(void* arg) {
Thread* thread = (Thread*)(arg);
current_thread = thread;
current_thread_name = thread->name_;
thread->pid_ = GetPID();
pthread_setname_np(pthread_self(), thread->name_.substr(0, 15).c_str());
std::function<void()> func;
func.swap(thread->callback_func_);
thread->semaphore_.post();
func();
return 0;
}
// 返回线程id
const pid_t Thread::GetPID() {
return syscall(SYS_gettid);
}
// 返回线程id
const std::thread::id Thread::GetThreadID() {
return std::this_thread::get_id();
}
这里简单说明下detach和join,detach并不会阻塞主线程而join主线程需要等的线程执行完毕。
detach | join | |
作用 | 将线程与其资源关联解除,使得线程执行结束后自动释放资源 | 等待制定线程执行结束,并可以获取线程的返回值 |
阻塞行为 | 不会阻塞当前线程,立即返回,无论线程是否执行完毕 | 阻塞当前线程,指导指定的线程执行完毕或超时/阻塞/取消/出错 |
返回值 | 0表示成功;非0表示失败 | 线程返回值,出错返回错误码 |
线程回收资源 | 线程结束后资源被自动回收 | 需要主线程显式调用pthread_join函数获取返回值,回收线程资源 |
在类的析构函数中调用detach函数是为了即使没有调用join函数也可以保证线程所持有的资源被正确释放。
在join函数中,将pthread_设置为0是为了标记线程已经完成,并且资源被释放,避免重复调用join。
还有在callbackFunc中,之所以用swap而不是直接调用,是为了保证执行完后callback_func_变成一个空的函数对象,不再持有任何资源。同时交换函数本身不会抛出异常,因此可以在异常安全的环境中使用,如果交换操作抛出异常,callback_func_中的函数对象不会被修改,保证程序的稳定性。同时若是在多线程下,假设callback_func_是一个可重入的函数对象,swap函数不会改变callback_func_的状态而只是交换了两个对象的内部状态。
最后给出测试函数:
#include "thread.h"
#include <iostream>
void print() {
std::cout << "Thread " << Thread::GetCurrentThreadName() << " begin " << "pid: " <<
Thread::GetCurrentThreadPtr()->GetPID() << " | " << Thread::GetCurrentThreadPtr()->getPID()
<< " thread id: " << Thread::GetCurrentThreadPtr()->GetThreadID() << std::endl;
Thread::GetCurrentThreadPtr()->setName("线程1");
std::cout << "change name: " << Thread::GetCurrentThreadPtr()->getName();
std::this_thread::sleep_for(std::chrono::seconds(2));
Thread::SetCurrentThreadName("thread test");
std::cout << "Thread " << Thread::GetCurrentThreadName() << " end" << std::endl;
}
void testThread() {
Thread thread1("thread1", print);
Thread thread2("thread2", print);
thread1.join();
thread2.join();
}
int main() {
testThread();
return 0;
}