我是一个线程(节选)

又是一个阳光明媚的周末,给广大学生朋友和初学者写点基础教程吧。以下是正文,请您鉴赏。

很多年以前,技术面试的时候面试官经常会问“程序什么时候需要开启新的线程”这样的问题,那个时候多核CPU才刚开始普及,很多人也是开始逐渐接触多线程技术。而如今多核CPU和多线程编程技术已经是下里巴人的技术了,所以本文不会花大气力再去回答“程序什么时候需要开启新的线程”,简单地解释一下,就是为了提高解决问题的效率,毕竟大多数情况下,多个cpu分工做一件事总比单个CPU做快许多吧。(主要是现在的面试官也很少再问“程序什么时候需要开启新的线程”这样的问题了,哈哈。)

多线程编程在现代软件开发中是如此的重要,以至于熟练使用多线程编程是一名合格的后台开发人员的基本功,注意,我这里用的是基本功一词。它是如此的重要,所以您应该掌握它。本文将介绍多线程的方方面面,从基础的知识到高级进阶。让我们开始吧。

线程的基础知识

线程的英文单词是thread,翻译成对应的中文有”分支“、”枝干“的意思,当然这里翻译成”线程“属于意译了。提高线程就不得不提与线程相关联的另外一个概念”进程“,一个”进程“代表中计算机中实际跑起来的一个程序,在现代操作系统的保护模式下,每个进程拥有自己独立的进程地址空间和上下文堆栈。但是就一个程序本身执行的操作来说,进程其实什么也不做(不执行任何进程代码),它只是提供一个大环境容器,在进程中实际的执行体是”线程“。wiki百科上给线程的定义是:

In computer science, a thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler, which is typically a part of the operating system. 计算机科学中,线程是操作系统管理的、可以执行编制好的最小单位的指令序列的调度器。

翻译的有点拗口,通俗地来说,进程是进程中实际执行代码的最小单元,它由操作系统安排调度(何时启动、何时运行和暂停以及何时消亡)。

进程与线程的区别与关系这里就不再多说了,任何一本关于操作系统的书籍都会有详细的介绍。这里需要重点强调的是如下几个问题,这也是我们在实际开发中使用多线程需要搞明白的问题。

一个进程至少有一个线程

上文也说了,线程是进程中实际干活的单位,因此一个进程至少得有一个线程,我们把这个线程称之为”主线程“,也就是说,一个进程至少要有一个主线程

主线程退出,支线程也将退出

当一个进程存在多个线程时,如果主线程执行结束了,那么这个时候即使支线程(也可以叫工作线程)还没完成相关的代码执行,支线程也会退出,也就是说,主线程一旦退出整个进程也就结束了。之所以强调这一点是,很多多线程编程的初学者经常犯在工作线程写了很多逻辑代码,但是没有注意到主线程已经提前退出,导致这些工作线程的代码来不及执行。解决这一问题的方案很多,核心就是让主线程不要退出,或者至少在工作线程完成工作之前主线程不要退出。常见的解决方案有主线程启动一个循环或者主线程等待工作线程退出后再退出(下文将会详细介绍)。

某个线程崩溃,会导致进程退出吗?

这是一个常见的面试题,还有一种问法是:进程中某个线程崩溃,是否会对其他线程造成影响?

一般来说,每个线程都是独立执行的单位,每个线程都有自己的上下文堆栈,一个线程的的崩溃不会对其他线程造成影响。但是通常情况下,一个线程崩溃会产生一个进程内的错误,例如在linux操作系统中,可能会产生一个segment fault错误,这个错误会产生一个信号,操作系统默认对这个信号的处理就是关闭进程,整个进程都被销毁了,这样的话这个进程中存在的其他线程自然也就不存在了。

线程基本操作

线程的创建

在使用线程之前,我们首先要学会如何创建一个新的线程。不管是哪个库还是哪种高级语言(如Java),线程的创建最终还是调用操作系统的API来进行的。我们这里先介绍操作系统的接口,这里分linux和Windows两个常用的操作系统平台来介绍。当然,这里并不是照本宣科地把linux man手册或者msdn上的函数签名搬过来,这里只介绍我们实际开发中常用的参数和需要注意的重难点。

linux线程创建

linux平台上使用pthread_create这个API来创建线程,其函数签名如下:

int pthread_create(pthread_t *thread, 
                   const pthread_attr_t *attr,
                   void *(*start_routine) (void *),
                   void *arg);
  • 参数thread,是一个输入参数,如果线程创建成功,通过这个参数可以得到创建成功的线程ID(下文会介绍线程ID的知识)。
  • 参数attr指定了该线程的属性,一般设置为NULL,表示使用默认属性。
  • 参数start_routine指定了线程函数,这里需要注意的是这个函数的调用方式必须是__cedel调用,由于在C/C++中定义函数时默认的调用方式就是__cedel调用,所以一般很少有人注意到这一点。而后面我们介绍在Windows操作系统上使用CreateThread定义线程函数时必须使用__stdcall调用方式时,我们就必须显式申明函数的调用方式了。

也就是说,如下函数的调用方式是等价的:

1//代码片段1: 不显式指定函数调用方式,其调用方式为默认的__cdecl
2void start_routine (void* args)
3  {
4}
5
6//代码片段2: 显式指定函数调用方式为默认的__cdecl,等价于代码片段1
7void __cdecl start_routine (void* args)
8  {
9}
  • 参数arg,通过这一参数可以在创建线程时将某个参数传入线程函数中,由于这是一个void*类型,我们可以方便我们最大化地传入任意多的信息给线程函数。(下文会介绍一个使用示例)
  • 返回值:如果成功创建线程,返回0;如果创建失败,则返回响应的错误码,常见的错误码有EAGAINEINVALEPERM

下面是一个使用pthread_create创建线程的简单示例:

 1#include <pthread.h>
 2#include <unistd.h>
 3
 4void threadfunc(void* arg)
 5  {
 6  while(1)
 7  {
 8    //睡眠1秒
 9    sleep(1);
10
11    printf("I am New Thread!\n");
12  }
13}
14
15int main()
16  {
17  pthread_t threadid;
18  pthread_create(&threadid, NULL, threadfunc, NULL);
19
20  while (1)
21  {
22    sleep(1);
23    //权宜之计,让主线程不要提前退出
24  }
25
26  return 0;
27}
Windows线程创建

Windows上创建线程使用CreateThread,其函数签名如下:

1HANDLE CreateThread(
2  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
3  SIZE_T                  dwStackSize,
4  LPTHREAD_START_ROUTINE  lpStartAddress,
5  LPVOID                  lpParameter,
6  DWORD                   dwCreationFlags,
7  LPDWORD                 lpThreadId
8);
  • 参数lpThreadAttributes,是线程的安全属性,一般设置为NULL。
  • 参数dwStackSize,线程的栈空间大小,单位为字节数,一般指定为0,表示使用默认大小。
  • 参数lpStartAddress,为线程函数,其类型是LPTHREAD_START_ROUTINE,这是一个函数指针类型,其定义如下: typedef DWORD ( __stdcall *LPTHREAD_START_ROUTINE )(LPVOID lpThreadParameter); 需要注意的是,Windows上创建的线程的线程函数其调用方式必须是__stdcall,如果您将如下函数设置成线程函数是不行的: DWORD threadfunc(LPVOID lpThreadParameter); 如上文所说,如果您不指定函数的调用方式,默认使用默认调用方式__cdecl,而这里的线程函数要求是__stdcall,因此你必须在函数名前面显式指定函数调用方式为__stdcall。 DWORD __stdcall threadfunc(LPVOID lpThreadParameter); Windows上的宏WINAPICALLBACK这两个宏的定义都是__stdcall。因为您在项目中看到的线程函数的签名大多写成如下两种形式的一种: 1//写法1 2DWORD WINAPI threadfunc(LPVOID lpThreadParameter); 3//写法2 4DWORD CALLBACK threadfunc(LPVOID lpThreadParameter);
  • 参数lpParameter为传给线程函数的参数,和linux下的pthread_create函数的arg一样,这实际上也是一个void类型(LPVOID类型是用过typedef包装后的void类型)。
  • 参数wCreationFlags,是一个32位无符号整型(DWORD),一般设置为0,表示创建好线程后立即启动线程的运行;有一些特殊的情况,我们不希望创建线程后立即开始执行,可以将这个值设置为4(对应Windows定义的宏CREATE_SUSPENDED),后面在需要的时候,再使用ResumeThread这个API让线程运行起来。
  • 参数lpThreadId,为线程创建成功返回的线程ID,这也是一个32位无符号整数(DWORD)。
  • 返回值:Windows上使用句柄(HANDLE类型)来管理线程对象,句柄本质上是内核句柄表中的索引值。如果成功创建线程,返回该线程的句柄;如果创建失败,返回NULL。

下面的代码片段,演示了Windows上如何创建一个线程:

 1#include <Windows.h>
 2#include <stdio.h>
 3
 4DWORD WINAPI ThreadProc(LPVOID lpParameters)
 5  {
 6    while (true)
 7    {
 8        //睡眠1秒,Windows上的Sleep函数参数事件单位为毫秒
 9        Sleep(1000);
10
11        printf("I am New Thread!\n");
12    }
13}
14
15int main()
16  {
17    DWORD dwThreadID;
18    HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadID);
19    if (hThread == NULL)
20    {
21        printf("Failed to CreateThread.\n");
22    }
23
24    while (true)
25    {
26        Sleep(1000);
27        //权宜之计,让主线程不要提前退出
28    }
29
30    return 0;
31}
CRT线程创建

这里的CRT,指的是C Runtime(C运行时),通俗地说就是C函数库。C库也提供了一套用于创建线程的函数(当然这个函数底层还是调用相应的操作系统平台的线程创建API),这里之所以提到这点是因为,由于C库函数是同时被Linux和Windows等操作系统支持的,所以使用C库函数创建线程可以直接写出跨平台的代码。由于其跨平台性,实际项目开发中推荐使用这个函数来创建线程。

C库创建线程常用的函数是_beginthreadex,申明位于process.h头文件中,其签名如下:

1uintptr_t _beginthreadex( 
2   void *security,  
3   unsigned stack_size,  
4   unsigned ( __stdcall *start_address )( void * ),  
5   void *arglist,  
6   unsigned initflag,  
7   unsigned *thrdaddr   
8);  

函数签名基本上和Windows上的CreateThread函数基本一致,这里就不再赘述了。

以下是使用_beginthreadex创建线程的一个例子:

 1#include <process.h>
 2//#include <Windows.h>
 3#include <stdio.h>
 4
 5unsigned int __stdcall threadfun(void* args)
 6  {
 7    while (true)
 8    {        
 9        //Sleep(1000);
10
11        printf("I am New Thread!\n");
12    }
13}
14
15int main(int argc, char* argv[])
16  {
17    unsigned int threadid;
18    _beginthreadex(0, 0, threadfun, 0, 0, &threadid);
19
20    while (true)
21    {
22        //Sleep(1000);
23        //权宜之计,让主线程不要提前退出
24    }
25
26    return 0;
27}
C++11提供的std::thread类

无论是linux还是Windows上创建线程的API,都有一个非常不方便的地方,就是线程函数的签名必须是固定的格式(参数个数和类型、返回值类型都有要求)。新的C++11标准引起了一个新的类std::thread(需要包含头文件<thread>),使用这个类的可以将任何签名形式的函数作为线程函数。以下代码分别创建两个线程,线程函数签名不一样:

 1#include <stdio.h>
 2#include <thread>
 3
 4void threadproc1()
 5  {
 6    while (true)
 7    {
 8        printf("I am New Thread 1!\n");
 9    }
10}
11
12void threadproc2(int a, int b)
13  {
14    while (true)
15    {
16        printf("I am New Thread 2!\n");
17    }
18}
19
20int main()
21  {
22    //创建线程t1
23    std::thread t1(threadproc1);
24    //创建线程t2
25    std::thread t2(threadproc2, 1, 2);
26
27    while (true)
28    {
29        //Sleep(1000);
30        //权宜之计,让主线程不要提前退出
31    }
32
33    return 0;
34}

当然,初学者在使用std::thread时容易犯如下错误:即在std::thread对象在线程运行期间必须是有效的。看下面的代码:

 1#include <stdio.h>
 2#include <thread>
 3
 4void threadproc()
 5  {
 6    while (true)
 7    {
 8        printf("I am New Thread!\n");
 9    }
10}
11
12void func()
13  {
14    std::thread t(threadproc);
15}
16
17int main()
18  {
19    func();
20
21    while (true)
22    {
23        //Sleep(1000);
24        //权宜之计,让主线程不要提前退出
25    }
26
27    return 0;
28}

上述代码在func中创建了一个线程,然后又在main函数中调用func方法,乍一看好像代码没什么问题,但是在实际运行时程序会崩溃。崩溃的原因是,当func函数调用结束后,func中局部变量t线程对象)就会被销毁了,而此时线程函数仍然在运行。这就是我所说的,使用std::thread类时,必须保证线程运行期间,其线程对象有效。这是一个很容易犯的错误,解决这个问题的方法是,std::thread对象提供了一个detach方法,这个方法让线程对象线程函数脱离关系,这样即使线程对象被销毁,仍然不影响线程函数的运行。我们只需要在在func函数中调用detach方法即可,代码如下:

1//其他代码保持不变,这里就不重复贴出来了
2void func()
3  {
4    std::thread t(threadproc);
5    t.detach();
6}

然而,更多的时候,我们需要线程对象去控制和管理线程的运行和生命周期,我们的代码应该尽量保证线程对象在线程运行期间有效,而不是单纯地调用detach方法。

线程ID

一个线程创建成功以后,我们可以拿到一个线程ID,线程ID是在整个操作系统范围内是唯一的。我们可以使用线程ID来标识和区分线程,例如我们在日志文件中,我们把打印日志的所在的线程ID也一起打印出来,这样也方便我们判断和排查问题。创建线程时,上文也介绍了可以通过pthread_create函数的第一个参数thread(linux平台)和CreateThread函数的最后一个参数lpThreadId(Windows平台)得到线程的ID。大多数时候,我们需要在当前调用线程中获取当前线程的ID,在linux平台上可以使用pthread_self函数,在Windows平台上可以使用GetCurrentThreadID函数获取,这两个函数的签名分别如下:

pthread_t pthread_self(void);

DWORD GetCurrentThreadId();

这两个函数比较简单,这里就不介绍了,无论是pthread_t还是DWORD类型,都是一个32位无符号整型值。

Windows操作系统中可以在任务管理器中查看某个进程的线程数量:

pstack命令

linux系统中可以通过pstack命令查看一个进程的线程数量和每个线程的调用堆栈情况。

pstack pid

pid设置为要查看的进程的ID即可。以我机器上nginx的worker进程为例,首先使用ps命令查看下nginx进程ID,然后使用pstack即可查看该进程每个线程的调用堆栈(我这里的nginx只有一个线程,如果有多个线程,会显示每个线程的调用堆栈):

 1[root@iZ238vnojlyZ ~]# ps -ef | grep nginx
 2root      2150     1  0 May22 ?        00:00:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
 3nginx     2151  2150  0 May22 ?        00:00:07 nginx: worker process
 4root     16621 16541  0 18:53 pts/0    00:00:00 grep --color=auto nginx
 5[root@iZ238vnojlyZ ~]# pstack 2151
 6#0  0x00007f70a61ca2a3 in __epoll_wait_nocancel () from /lib64/libc.so.6
 7#1  0x0000000000437313 in ngx_epoll_process_events ()
 8#2  0x000000000042efc3 in ngx_process_events_and_timers ()
 9#3  0x0000000000435681 in ngx_worker_process_cycle ()
10#4  0x0000000000434104 in ngx_spawn_process ()
11#5  0x0000000000435854 in ngx_start_worker_processes ()
12#6  0x000000000043631b in ngx_master_process_cycle ()
13#7  0x0000000000412229 in main ()
C++11的获取当前线程ID的方法

C++11的线程库可以使用std::this_thread类的get_id获取当前线程的ID,这是一个静态类方法。

当然也可以使用std::threadget_id获取指定线程的ID,这是一个实例方法。

但是get_id方法返回的是一个包装类型的std::thread::id对象,不可以直接强转成整型,也没有提供任何转换成整型的接口。所以,我们一般使用std::cout这样的输出流来输出,或者先转换为std::ostringstream对象,再转换成字符串类型,然后把字符串类型转换成我们需要的整型。这一点,算是C++11的线程库不是很方便的地方。

 1#include <thread>
 2#include <iostream>
 3#include <sstream>
 4
 5void worker_thread_func()
 6  {
 7    while (true)
 8    {
 9
10    }
11}
12
13int main()
14  {
15    std::thread t(worker_thread_func);
16    //获取线程t的ID
17    std::thread::id worker_thread_id = t.get_id();
18    std::cout << "worker thread id: " << worker_thread_id << std::endl;
19
20    //获取主线程的线程ID
21    std::thread::id main_thread_id = std::this_thread::get_id();
22    //先将std::thread::id转换成std::ostringstream对象
23    std::ostringstream oss;
24    oss << main_thread_id;
25    //再将std::ostringstream对象转换成std::string
26    std::string str = oss.str();
27    //最后将std::string转换成整型值
28    int threadid = atol(str.c_str());
29
30    std::cout << "main thread id: " << threadid << std::endl;
31
32    while (true)
33    {
34        //权宜之计,让主线程不要提前退出
35    }
36
37    return 0;
38}

程序运行结果如下:

等待线程结束

实际项目开发中,我们常常会有这样一种需求,即一个线程需要等待另外一个线程执行完任务退出后再继续执行。这在linux和Windows操作系统中都提供了相应的操作系统API,我们来分别介绍一下。

linux下等待线程结束

linux下使用pthread_join函数等待某个线程结束,其函数签名如下:

int pthread_join(pthread_t thread, void **retval);
  • 参数thread,需要等待的线程id。
  • 参数retval,输出参数,用于接收等待退出的线程的退出码(Exit Code)。

pthread_join函数等待其他线程退出期间会挂起等待的线程,被挂起的线程不会消耗宝贵任何CPU时间片。直到目标线程退出后,等待的线程会被唤醒。

我们通过一个实例来演示一下这个函数的使用方法,实例功能如下:

程序启动时,开启一个工作线程,工作线程将当前系统时间写入文件中后退出,主线程等待工作线程退出后,从文件中读取出时间并显示在屏幕上。

 1#include <stdio.h>
 2#include <pthread.h>
 3
 4#define TIME_FILENAME "time.txt"
 5
 6void fileThreadFunc(void* arg)
 7  {
 8    time_t now = time(NULL);
 9    struct tm* t = localtime(&now);
10    char timeStr[32] = {0};
11    snprintf(timeStr, 32, "%04d/%02d/%02d %02d:%02d:%02d", 
12             t->tm_year+1900,
13             t->tm_mon+1,
14             t->tm_mday,
15             t->tm_hour,
16             t->tm_min,
17             t->tm_sec);
18    //文件不存在,则创建;存在,则覆盖。
19    FILE* fp = fopen(TIME_FILENAME, "w");
20    if (fp == NULL)
21    {
22      printf("Failed to create time.txt.\n");
23        return;
24    }
25
26    size_t sizeToWrite = strlen(timeStr) + 1;
27    size_t ret = fwrite(timeStr, 1, sizeToWrite, fp);
28    if (ret != sizeToWrite)
29    {
30        printf("Write file error.\n");
31    }
32
33    fclose(fp);
34}
35
36int main()
37  {
38    pthread_t fileThreadID;
39    int ret = pthread_create(&fileThreadID, NULL, fileThreadFunc, NULL);
40    if (ret != 0)
41    {
42        printf("Failed to create fileThread.\n");
43        return -1;
44    }
45
46    int* retval;
47    pthread_join(fileThreadID, &retval);
48
49    //使用r选项,要求文件必须存在
50    FILE* fp = fopen(TIME_FILENAME, "r");
51    if (fp == NULL)
52    {
53        printf("open file error.\n");
54        return -2;
55    }
56
57    char buf[32] = {0};
58    int sizeRead = fread(buf, 1, 32, fp);
59    if (sizeRead == 0)
60    {
61      printf("read file error.\n");
62      return -3;
63    }
64
65    printf("Current Time is: %s.\n", buf);
66
67    return 0;
68}

程序执行结果如下:

[root@localhost threadtest]# ./test
Current Time is: 2018/09/24 21:06:01.
Windows下等待线程结束

Windows下使用API WaitForSingleObjectWaitForMultipleObjects函数,前者用于等待一个线程结束,后者可以同时等待多个线程结束。当然,这两个函数的作用不仅可以用于等待线程退出,还可以用于等待其他线程同步对象,本文后面的将深入介绍这两个函数。与linux的pthread_join函数不同,Windows的WaitForSingleObject函数提供了可选择等待时间的精细控制。

这里我们仅演示等待线程退出。

WaitForSingleObject函数签名如下:

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
  • 参数hHandle是需要等待的对象的句柄,等待线程退出,传入线程句柄。
  • 参数dwMilliseconds是需要等待的毫秒数,如果使用INFINITE宏,则表示无限等待下去。
  • 返回值:该函数的返回值有点复杂,我们后面文章具体介绍。当dwMilliseconds参数使用INFINITE值,该函数会挂起当前等待线程,直到等待的线程退出后,等待的线程才会被唤醒,WaitForSingleObject后的程序执行流继续执行。

我们将上面的linux示例代码改写成Windows版本的:

 1#include <stdio.h>
 2#include <string.h>
 3#include <time.h>
 4#include <Windows.h>
 5
 6#define TIME_FILENAME "time.txt"
 7
 8DWORD WINAPI FileThreadFunc(LPVOID lpParameters)
 9  {
10    time_t now = time(NULL);
11    struct tm* t = localtime(&now);
12    char timeStr[32] = { 0 };
13    sprintf_s(timeStr, 32, "%04d/%02d/%02d %02d:%02d:%02d",
14              t->tm_year + 1900,
15              t->tm_mon + 1,
16              t->tm_mday,
17              t->tm_hour,
18              t->tm_min,
19              t->tm_sec);
20    //文件不存在,则创建;存在,则覆盖。
21    FILE* fp = fopen(TIME_FILENAME, "w");
22    if (fp == NULL)
23    {
24        printf("Failed to create time.txt.\n");
25        return 1;
26    }
27
28    size_t sizeToWrite = strlen(timeStr) + 1;
29    size_t ret = fwrite(timeStr, 1, sizeToWrite, fp);
30    if (ret != sizeToWrite)
31    {
32        printf("Write file error.\n");
33    }
34
35    fclose(fp);
36
37    return 2;
38}
39
40
41int main()
42  {
43    DWORD dwFileThreadID;
44    HANDLE hFileThread = CreateThread(NULL, 0, FileThreadFunc, NULL, 0, 
45                                      &dwFileThreadID);
46    if (hFileThread == NULL)
47    {
48        printf("Failed to create fileThread.\n");
49        return -1;
50    }
51
52    //无限等待,直到文件线程退出,否则程序将一直挂起。
53    WaitForSingleObject(hFileThread, INFINITE);
54
55    //使用r选项,要求文件必须存在
56    FILE* fp = fopen(TIME_FILENAME, "r");
57    if (fp == NULL)
58    {
59        printf("open file error.\n");
60        return -2;
61    }
62
63    char buf[32] = { 0 };
64    int sizeRead = fread(buf, 1, 32, fp);
65    if (sizeRead == 0)
66    {
67        printf("read file error.\n");
68        return -3;
69    }
70
71    printf("Current Time is: %s.\n", buf);
72
73    return 0;
74}

程序执行结果:

C++11提供的等待线程结果函数

可以想到,C++11的std::thread既然统一了linux和Windows的线程创建函数,那么它应该也提供等待线程退出的接口,确实如此,std::threadjoin方法就是用来等待线程退出的函数。当然使用这个函数时,必须保证该线程还处于运行中状态,也就是说等待的线程必须是可以”join“的,如果需要等待的线程已经退出,此时调用join方法,程序会产生崩溃。因此,C++11的线程库同时提供了一个joinable方法来判断某个线程是否可以等待,如果您不确定您的线程是否可以”join”,可以先调用joinable函数判断一下是否需要等待。

还是以上面的例子为例,改写成C++11的代码:

 1#include <stdio.h>
 2#include <string.h>
 3#include <time.h>
 4#include <thread>
 5
 6#define TIME_FILENAME "time.txt"
 7
 8void FileThreadFunc()
 9  {
10    time_t now = time(NULL);
11    struct tm* t = localtime(&now);
12    char timeStr[32] = { 0 };
13    sprintf_s(timeStr, 32, "%04d/%02d/%02d %02d:%02d:%02d",
14              t->tm_year + 1900,
15              t->tm_mon + 1,
16              t->tm_mday,
17              t->tm_hour,
18              t->tm_min,
19              t->tm_sec);
20    //文件不存在,则创建;存在,则覆盖。
21    FILE* fp = fopen(TIME_FILENAME, "w");
22    if (fp == NULL)
23    {
24        printf("Failed to create time.txt.\n");
25        return;
26    }
27
28    size_t sizeToWrite = strlen(timeStr) + 1;
29    size_t ret = fwrite(timeStr, 1, sizeToWrite, fp);
30    if (ret != sizeToWrite)
31    {
32        printf("Write file error.\n");
33    }
34
35    fclose(fp);
36}
37
38int main()
39  {
40    std::thread t(FileThreadFunc);
41    if (t.joinable())
42        t.join();
43
44    //使用r选项,要求文件必须存在
45    FILE* fp = fopen(TIME_FILENAME, "r");
46    if (fp == NULL)
47    {
48        printf("open file error.\n");
49        return -2;
50    }
51
52    char buf[32] = { 0 };
53    int sizeRead = fread(buf, 1, 32, fp);
54    if (sizeRead == 0)
55    {
56        printf("read file error.\n");
57        return -3;
58    }
59
60    printf("Current Time is: %s.\n", buf);
61
62    return 0;
63}

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

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

void threadFunc(void* arg);

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

 1class Thread
 2  {
 3public:
 4    Thread();
 5    ~Thread();
 6
 7    void start();
 8    void stop();
 9
10    void threadFunc(void* arg);
11};

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

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

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

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

 1#include <thread>
 2#include <memory>
 3#include <stdio.h>
 4
 5class Thread
 6  {
 7public:
 8    Thread()
 9    {
10    }
11
12    ~Thread()
13    {
14    }
15
16    void Start()
17      {
18        m_stopped = false;
19        //threadFunc是类的非静态方法,所以作为线程函数,第一个参数必须传递类实例地址,即this指针
20        m_spThread.reset(new std::thread(&Thread::threadFunc, this, 8888, 9999));
21    }
22
23    void Stop()
24      {
25        m_stopped = true;
26        if (m_spThread)
27        {
28            if (m_spThread->joinable())
29                m_spThread->join();
30        }
31    }
32
33private:
34    void threadFunc(int arg1, int arg2)
35      {
36        while (!m_stopped)
37        {
38            printf("Thread function use instance method.\n");
39        }      
40    }
41
42private:
43    std::shared_ptr<std::thread>  m_spThread;
44    bool                          m_stopped;
45};
46
47int main()
48  {
49    Thread mythread;
50    mythread.Start();
51
52    while (true)
53    {
54        //权宜之计,让主线程不要提前退出
55    }
56
57    return 0;
58}

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

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

.h文件代码如下:

 1/**
 2   * Thread.h
 3   */
 4#ifdef WIN32
 5//#include <windows.h>
 6typedef HANDLE THREAD_HANDLE ;
 7#else
 8//#include <pthread.h>
 9typedef pthread_t THREAD_HANDLE ;
10#endif
11
12/**定义了一个线程对象
13   */
14class  CThread  
15  {
16public:
17    /**构造函数
18       */
19    CThread();
20
21    /**析构函数
22       */
23    virtual ~CThread();
24
25    /**创建一个线程
26       * @return true:创建成功 false:创建失败
27       */
28    virtual bool Create();
29
30    /**获得本线程对象存储的线程句柄
31       * @return 本线程对象存储的线程句柄线程句柄
32       */
33    THREAD_HANDLE GetHandle();
34
35    /**线程睡眠seconds秒
36       * @param seconds 睡眠秒数
37       */
38    void OSSleep(int nSeconds);
39
40    void SleepMs(int nMilliseconds);
41
42    bool Join();
43
44    bool IsCurrentThread();
45
46    void ExitThread();
47
48private:    
49#ifdef WIN32
50    static DWORD WINAPI _ThreadEntry(LPVOID pParam);
51#else
52    static void* _ThreadEntry(void* pParam);
53#endif
54
55    /**虚函数,子类可做一些实例化工作
56       * @return true:创建成功 false:创建失败
57       */
58    virtual bool InitInstance();
59
60    /**虚函数,子类清楚实例
61       */
62    virtual void ExitInstance();
63
64    /**线程开始运行,纯虚函数,子类必须继承实现
65       */
66    virtual void Run() = 0;
67
68private:
69     THREAD_HANDLE  m_hThread;  /**< 线程句柄 */
70     DWORD          m_IDThread;
71
72};

.cpp文件如下:

  1/**
  2   * Thread.cpp
  3   */
  4#include "Thread.h"
  5
  6#ifdef WIN32
  7DWORD WINAPI CThread::_ThreadEntry(LPVOID pParam)
  8#else
  9void* CThread::_ThreadEntry(void* pParam)
 10#endif
 11{
 12    CThread *pThread = (CThread *)pParam;
 13    if(pThread->InitInstance())
 14    {
 15        pThread->Run();
 16    }
 17
 18    pThread->ExitInstance();
 19
 20    return NULL;
 21}
 22
 23CThread::CThread()
 24{
 25    m_hThread = (THREAD_HANDLE)0;
 26    m_IDThread = 0;
 27}
 28
 29CThread::~CThread()
 30{
 31}
 32
 33bool CThread::Create()
 34{
 35    if (m_hThread != (THREAD_HANDLE)0)
 36    {
 37        return true;
 38    }
 39    bool ret = true;
 40#ifdef WIN32
 41    m_hThread = ::CreateThread(NULL,0,_ThreadEntry,this,0,&m_IDThread);
 42    if(m_hThread==NULL)
 43    {
 44        ret = false;
 45    }
 46#else
 47    ret = (::pthread_create(&m_hThread,NULL,&_ThreadEntry , this) == 0);
 48#endif
 49    return ret;
 50}
 51
 52bool CThread::InitInstance()
 53{
 54    return true;
 55}
 56
 57void CThread::ExitInstance()
 58{
 59}
 60
 61void CThread::OSSleep(int seconds)
 62{
 63#ifdef WIN32
 64    ::Sleep(seconds*1000);
 65#else
 66    ::sleep(seconds);
 67#endif
 68}
 69
 70void CThread::SleepMs(int nMilliseconds)
 71{
 72#ifdef WIN32
 73    ::Sleep(nMilliseconds);
 74#else
 75    ::usleep(nMilliseconds);
 76#endif
 77}
 78
 79bool CThread::IsCurrentThread()
 80{
 81#ifdef WIN32
 82    return ::GetCurrentThreadId() == m_IDThread;
 83#else
 84    return ::pthread_self() == m_hThread;
 85#endif
 86}
 87
 88bool CThread::Join()
 89{    
 90    THREAD_HANDLE hThread = GetHandle();
 91    if(hThread == (THREAD_HANDLE)0)
 92    {
 93        return true;
 94    }
 95#ifdef WIN32
 96    return (WaitForSingleObject(hThread,INFINITE) != 0);
 97#else
 98    return (pthread_join(hThread, NULL) == 0);
 99#endif
100}
101
102void CThread::ExitThread()
103{
104#ifdef WIN32
105    ::ExitThread(0);
106#else
107#endif
108}

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

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

原文发布于微信公众号 - 高性能服务器开发(easyserverdev)

原文发表时间:2018-10-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏海天一树

小朋友学Java(12):包

包(package)是Java语言提供的一种区别类名字命名空间的机制,它是类的一种文件组织和管理方式、是一组功能相似或相关的类或接口的集合。Java packa...

2976
来自专栏python3

python 文件操作

新建一个txt文件,内容是《Yesterday When I Was Young》一首歌的歌词

2002
来自专栏互联网开发者交流社区

jsp 内置对象(五)

1265
来自专栏杂烩

mongodb拾遗

851
来自专栏Java编程技术

一个有关定时生产与消费的问题

按照上面的逻辑看的话,每个队列里面最多有一个元素。其实不然,因为在多线程模型中每个线程占用cpu执行的时间是按照时间片来划分的,每个线程执行完自己的时间片后会被...

821
来自专栏测试驿栈

Jmeter(四)_16个逻辑控制器详解

1、 Jmeter官网对逻辑控制器的解释是:“Logic Controllers determine the order in which Samplers a...

1.5K2
来自专栏青玉伏案

iOS开发之Alamofire源码解析

今天博客中的Alamofire源码的版本是以3.4版本为例。上篇博客系统的对NSURLSession相关的东西进行了详细的解析,详情请看《详解NSURLSess...

2997
来自专栏Jimoer

JVM学习记录-线程安全与锁优化(一)

线程:程序流执行的最小单元。线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I...

772
来自专栏GreenLeaves

JS模块加载系统设计V1

一、require模块 +function() { var path = location.protocol + "//" + loca...

2075
来自专栏微信公众号:Java团长

Java网络爬虫基础知识

Java 网络爬虫具有很好的扩展性可伸缩性,其是目前搜索引擎开发的重要组成部分。例如,著名的网络爬虫工具 Nutch 便是采用 Java 开发,该工具以 Apa...

1572

扫码关注云+社区

领取腾讯云代金券