CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 第1章高频C++11重难点知识解析

  • 第2章Linux GDB高级调试指南

  • 第3章C++多线程编程从入门到进阶

    • 3.1 线程的基本概念
    • 3.2 线程基本操作
    • 3.3 线程函数传C++类实例指针惯用法
    • 3.4 整型变量的原子操作
    • 3.5 Linux线程同步对象
    • 3.6 Windows 线程资源同步对象
    • 3.7 C++ 11/14/17 线程同步对象
    • 3.8 如何确保创建的线程一定运行起来?
    • 3.9 多线程使用锁实践经验总结
    • 3.10 线程局部存储
    • 3.11 C 库的非线程安全函数
    • 3.12 线程池与队列系统的设计
    • 3.13 纤程(Fiber)与协程(Coroutine)
    • 3.14 本章总结
  • 第4章C++网络编程重难点解析

  • 第5章网络通信故障排查常用命令

  • 第6章高性能网络通信协议设计精要

  • 第7章高性能服务结构设计

  • 第8章Redis 网络通信模块源码分析

  • 第9章后端服务重要模块设计探索

  • C++后端开发进阶
  • 第3章C++多线程编程从入门到进阶
zhangxf
2023-04-05

3.3 线程函数传C++类实例指针惯用法

前面的章节介绍了除了C++11的线程库提供了的std::thread类对线程函数签名没有特殊要求外,无论是Linux还是Windows的线程函数的签名都必须是指定的格式,即参数和返回值必须是规定的形式。如果使用C++面向对象的方式对线程函数进行封装,那么线程函数就不能是类的实例方法,即必须是类的静态方法。那么,为什么不能是类的实例方法呢?我们以Linux的线程函数签名为例:

void* threadFunc(void* arg);
1

假设我们将线程的基本功能封装到一个 Thread 类中,部分代码如下:

class Thread
{
public:
    Thread();
    ~Thread();

    void start();
    void stop();

    void* threadFunc(void* arg);
};
1
2
3
4
5
6
7
8
9
10
11

由于threadFunc是一个类实例方法,无论是类的实例方法还是静态方法,C++编译器在编译时都会将这些函数”翻译“成全局函数,即去掉类的域限制。对于实例方法,为了保证类方法的正常功能,C++编译器在翻译时,会将类的实例对象地址(也就是this指针)作为第一个参数传递给该方法,也就是说,翻译后的threadFunc的签名变成了如下形式(伪代码):

void* threadFunc(Thread* this, void* arg);
1

这样的话,就不符合线程函数签名要求了。因此如果使用类方法作为线程函数则只能是类的静态方法而不能是类的实例方法。

当然,如果是使用C++11的std::thread类就没有这个限制,即使类成员函数是类的实例方法也可以,但是必须显式地将线程函数所属的类对象实例指针(在类的内部就是this指针)作为构造函数参数传递给std::thread,还是需要传递类的this指针,这在本质上是一样的,代码实例如下:

#include <thread>
#include <memory>
#include <stdio.h>

class Thread
{
public:
    Thread()
    {
    }

    ~Thread()
    {
    }

    void Start()
    {
        m_stopped = false;
        //threadFunc是类的非静态方法,所以作为线程函数,第一个参数必须传递类实例地址,即this指针
        m_spThread.reset(new std::thread(&Thread::threadFunc, this, 8888, 9999));
    }

    void Stop()
    {
        m_stopped = true;
        if (m_spThread)
        {
            if (m_spThread->joinable())
                m_spThread->join();
        }
    }

private:
    void threadFunc(int arg1, int arg2)
    {
        while (!m_stopped)
        {
            printf("Thread function use instance method.\n");
        }      
    }

private:
    std::shared_ptr<std::thread>  m_spThread;
    bool                          m_stopped;
};

int main()
{
    Thread mythread;
    mythread.Start();

    //权宜之计,让主线程不要提前退出
    while (true)
    {
    }
    
    return 0;
}
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
55
56
57
58

上述代码中使用了C++11新增的智能指针std::shared_ptr类来包裹了一下new出来的std::thread对象,这样我们就不需要自己手动delete这个std::thread对象了。

综上所述,如果不使用C++11的语法,那么线程函数只能使用类的静态方法,且函数签名必须符合线程函数的签名要求。如果是类的静态方法,那么就没法访问类的实例方法了,为了解决这个问题,我们在实际开发中往往会在创建线程时将当前对象的地址(this指针)传递给线程函数,然后在线程函数中,将该指针转换成原来的类实例,再通过这个实例就可以访问类的所有方法了。代码示例如下:

.h文件代码如下:

/**
 * Thread.h
 */
#ifdef WIN32
typedef HANDLE THREAD_HANDLE ;
#else
typedef pthread_t THREAD_HANDLE ;
#endif

/**定义了一个线程对象
 */
class  CThread  
{
public:
	/**构造函数
	 */
	CThread();
	
	/**析构函数
	 */
	virtual ~CThread();
	
	/**创建一个线程
	 * @return true:创建成功 false:创建失败
	 */
	virtual bool Create();
	
	/**获得本线程对象存储的线程句柄
	 * @return 本线程对象存储的线程句柄线程句柄
	 */
	THREAD_HANDLE GetHandle();

	/**线程睡眠seconds秒
	 * @param seconds 睡眠秒数
	 */
	void OSSleep(int nSeconds);

	void SleepMs(int nMilliseconds);

	bool Join();

	bool IsCurrentThread();

	void ExitThread();

private:	
#ifdef WIN32
	static DWORD WINAPI _ThreadEntry(LPVOID pParam);
#else
	static void* _ThreadEntry(void* pParam);
#endif

	/**虚函数,子类可做一些实例化工作
	 * @return true:创建成功 false:创建失败
	 */
	virtual bool InitInstance();
	
	/**虚函数,子类清楚实例
	 */
	virtual void ExitInstance();
	
	/**线程开始运行,纯虚函数,子类必须继承实现
	 */
	virtual void Run() = 0;
	
private:
     //线程句柄
	 THREAD_HANDLE  m_hThread;
	 DWORD          m_IDThread;
};
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

.cpp文件如下:

/**
 * Thread.cpp
 */
#include "Thread.h"

#ifdef WIN32
DWORD WINAPI CThread::_ThreadEntry(LPVOID pParam)
#else
void* CThread::_ThreadEntry(void* pParam)
#endif
{
	CThread *pThread = (CThread *)pParam;
    if(pThread->InitInstance())
    {
    	pThread->Run();
    }

	pThread->ExitInstance();

	return NULL;
}

CThread::CThread()
{
	m_hThread = (THREAD_HANDLE)0;
	m_IDThread = 0;
}

CThread::~CThread()
{
}

bool CThread::Create()
{
	if (m_hThread != (THREAD_HANDLE)0)
	{
		return true;
	}
	bool ret = true;
#ifdef WIN32
	m_hThread = ::CreateThread(NULL,0,_ThreadEntry,this,0,&m_IDThread);
	if(m_hThread==NULL)
	{
		ret = false;
	}
#else
	ret = (::pthread_create(&m_hThread,NULL,&_ThreadEntry , this) == 0);
#endif
	return ret;
}

bool CThread::InitInstance()
{
	return true;
}

void CThread::ExitInstance()
{
}

void CThread::OSSleep(int seconds)
{
#ifdef WIN32
	::Sleep(seconds*1000);
#else
	::sleep(seconds);
#endif
}

void CThread::SleepMs(int nMilliseconds)
{
#ifdef WIN32
	::Sleep(nMilliseconds);
#else
	::usleep(nMilliseconds);
#endif
}

bool CThread::IsCurrentThread()
{
#ifdef WIN32
	return ::GetCurrentThreadId() == m_IDThread;
#else
	return ::pthread_self() == m_hThread;
#endif
}

bool CThread::Join()
{	
	THREAD_HANDLE hThread = GetHandle();
	if(hThread == (THREAD_HANDLE)0)
	{
		return true;
	}
#ifdef WIN32
	return (WaitForSingleObject(hThread,INFINITE) != 0);
#else
	return (pthread_join(hThread, NULL) == 0);
#endif
}

void CThread::ExitThread()
{
#ifdef WIN32
	::ExitThread(0);
#else
#endif
}
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

上述代码CThread类封装了一个线程的常用的操作,使用宏WIN32来分别实现了Windows和Linux两个操作系统平台的线程操作。其中InitInstance和ExitInstance方法为虚函数,在继承CThread的子类中可以改写这两个方法,根据实际需要在线程函数正式业务逻辑前后做一些初始化和反初始化工作,而纯虚接口Run方法必须改写,改写成线程实际执行函数。

在线程函数中通过在创建线程(调用CreateThread或pthread_create方法)时,将当前对象的this指针作为线程函数的唯一参数传入,这样在线程函数中,可以通过线程函数参数得到对象的指针,通过这个指针就可以自由访问类的实例方法了。这一技巧非常常用,它广泛地用于各类开源C++项目或者实际的商业C++项目中,希望读者能理解并熟练掌握它。

那么类的实例方法就一定不能作为线程函数了吗?不一定,在C++11语法中我们可以使用std::bind这一工具来给线程函数绑定一个this指针,这样就能使用类实例方法作为线程函数了,示例代码如下:

//代码来源:
//https://github.com/balloonwj/flamingo/blob/master/flamingoclient/Source/net/IUSocket.h
#include <thread>
#include <memory>

class CIUSocket
{
    //这里省略构造、析构等函数
public:
    void Init();
    void Uninit();

    void Join();

private:   
    void    SendThreadProc();
    void    RecvThreadProc();

private:	
    std::unique_ptr<std::thread>    m_spSendThread;
    std::unique_ptr<std::thread>    m_spRecvThread;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

重点来看下CIUSocket::Init函数:

void CIUSocket::Init()
{
	//SendThreadProc和RecvThreadProc都是类的实例方法
	m_spSendThread.reset(new std::thread(std::bind(&CIUSocket::SendThreadProc, this)));
	m_spRecvThread.reset(new std::thread(std::bind(&CIUSocket::RecvThreadProc, this)));
}
1
2
3
4
5
6
上次更新: 2025/04/01, 20:53:14
3.2 线程基本操作
3.4 整型变量的原子操作

← 3.2 线程基本操作 3.4 整型变量的原子操作→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式