5月10日更新

前两天用之前的那个线程类去交工,然后就被批了,因为那个线程类是有bug的(如果实现类自杀的话,就不存在这个问题),韩老师的书上也有说明,但是我并没有注意。

在说明bug之前,我先给个完全复现bug的程序。

把Wrapper函数更为此。即在调用Execute函数之前,先等待2秒。

1
2
3
4
5
6
7
DWORD WINAPI Thread::Wrapper(LPVOID arg){
    Thread *thread_this = (Thread*)arg;
    Sleep(2000);
    thread_this->Execute();
    ExitThread(thread_this->return_value);
    return 0;
}

其他类成员函数不变。主函数以及子类如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class my_thread :public Thread{
protected:
    void Execute(){
        printf("this is execute\n");
        return ;
    }
};
int main(){
    my_thread *th = new my_thread();
    th->Run();
    delete th;
    return 0;
}

运行程序之后,会出现错误:

调用纯虚函数了。。。。说句实话,这个bug很是让我摸不着头脑。

我大概说一下问题出在哪里,问题主要出在虚函数上(为此专门把虚函数给好好看了看)。

在代码中有这么几个关键地方。

父类的析构函数。

1
2
3
4
5
6
Thread::~Thread(){
    if (thread_id != 0){
        this->WaitFor();
        CloseHandle(thread_handle);
    }
}

静态函数。

1
2
3
4
5
6
7
8
DWORD WINAPI Thread::Wrapper(LPVOID arg){
    Thread *thread_this = (Thread*)arg;
    //注意这个Sleep函数
    Sleep(2000);
    thread_this->Execute();
    ExitThread(thread_this->return_value);
    return 0;
}

主函数中的delete。

1
2
3
4
5
6
int main(){
    my_thread *th = new my_thread();
    th->Run();
    delete th;
    return 0;
}

在讲解问题之前,还要再声明一下,当调用子类析构函数的时候,类的虚表是子类的虚表,当调用父类的析构函数的时候,这个时候虚表是父类的虚表。

在程序的main函数中,调用线程类的Run方法,然后创建线程,这个时候,静态函数被调用,但是因为执行了Sleep函数,所以这个时候,Execute函数还并未执行。这个时候,主线程中已经调用delete函数去释放内存了。先调用子类的默认析构函数,然后调用父类的析构函数,这个时候虚表就被替换成父类的虚表。然后在析构函数中调用WaitFor函数进行等待。在线程中,Sleep函数返回,然后调用Execute函数,但是由于Execute函数是虚函数,由虚表的函数指针指向实际执行的内容,但是那个虚表已经被替换成父类的虚表,所以这时候调用Execute函数的时候,自然就调用父类的纯虚函数了,因此也就产生了这个bug。

如果要进行修复的话,也很简单,把父类析构函数中的内容写到子类的析构函数中,亦或是直接在主函数调用Run方法后就执行WaitFor方法,然后再执行delete。

对于第一种方法,因为等待函数是在子类的析构函数中调用的,这个时候虚表还是子类的虚表,所以执行Execute函数不会出错。

第二种方法,在调用delete之前就等待线程执行完毕,自然也不会出现任何问题。

除了上述的两种方法外,还有一种更为通用的方式。韩老师称之为策略者模式。貌似在java中有两种创建线程的方法,一种是和我之前写的相似,一种是Runnable方法。不过我没学过java,所以具体的不是很清楚。而这种更为通用的方法貌似就类似与java中的Runnable方法。

新的线程类如下:

  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
#include <WinSock2.h>
#include <stdio.h>

//Runnable类,在该类中定义纯虚函数
class Runnable{
public:
    virtual unsigned long Execute() = 0;
};

//线程类
class Thread{
private:
    static DWORD WINAPI Wrapper(LPVOID arg);
    int return_value;
    unsigned long thread_id;
    HANDLE thread_handle;

    //在这里定义类成员Runnable变量指针
    Runnable *FRunnable;
public:
    Thread(Runnable *run);
    virtual ~Thread();
    void Run();
    void WaitFor();
    int GetThreadID();
    HANDLE GetHandle();
    int GetReturnValue();
    DWORD Suspend();
    DWORD Resume();
};

Thread::Thread(Runnable *run){
    FRunnable = run;
    thread_id = 0;
    thread_handle = 0;
    return_value = 0;
}

Thread::~Thread(){
    if (thread_id != 0){
        this->WaitFor();
        CloseHandle(thread_handle);
    }
}

void Thread::Run(){
    thread_handle = CreateThread(NULL, 0, Wrapper, this, 0, &thread_id);
}

void Thread::WaitFor(){
    if (thread_id == 0){
        return;
    }
    WaitForSingleObject(thread_handle, INFINITE);
}

int Thread::GetThreadID(){
    return thread_id;
}

HANDLE Thread::GetHandle(){
    return thread_handle;
}

int Thread::GetReturnValue(){
    return return_value;
}

DWORD WINAPI Thread::Wrapper(LPVOID arg){
    Thread *thread_this = (Thread*)arg;
    //在这里执行Runnable类的虚函数方法
    thread_this->FRunnable->Execute();
    ExitThread(thread_this->return_value);
    return 0;
}

DWORD Thread::Suspend(){
    return SuspendThread(this->thread_handle);
}

DWORD Thread::Resume(){
    return ResumeThread(this->thread_handle);
}

//Runnable类的子类,重写虚函数
class my_runnable :public Runnable{
protected:
    unsigned long Execute(){
        printf("this is son runnable\n");
        return 0;
    }
};

//main函数,在这里创建my_runnable类,并作为参数传给线程类
int main(){
    my_runnable *r = new my_runnable();
    Thread *th = new Thread(r);
    th->Run();
    delete th;
    return 0;
}

分割线,下面是老旧的内容。

之前重构之前项目的代码,需要将代码封装成类,其中一个任务就是将线程的操作封装成一个类,当时遇到的一个主要问题就是怎么在类中创建线程。

网上查了一些资料,发现有三种方法,三种只记得前两种,分别是使用静态函数以及使用友元。因为c++实在太菜,所以也就懒得看友元了,大概看了看静态函数的实现方式,本来感觉这种处理方式非常简单,但是后来文章又说使用静态函数的时候就无法使用类成员了。对此感到非常蛋疼。后来这个问题请教了韩老师。

一问韩老师才知道,他写的书《老码识图》中就已经讲了线程类的封装。他的方法还是使用静态函数,只是将类的this指针作为参数传到静态函数中,从而继续使用类成员变量。看到这里,我才想起来,在我很早写MFC程序的时候,就使用过这种方法。当时在MFC中需要使用多线程,但是我又不想使用MFC的那一套东西,习惯了直接调用windows sdk函数,但是又要访问MFC的类成员变量,当时网上找了一种方法就和韩老师书中的方法类似,在MFC中定义一个全局函数,然后使用该函数作为参数创建线程,因为要使用类成员变量,所以把this指针作为参数传给该函数。没想到当年使用的这个方法到现在反而忘了。。。

最后,参考韩老师的书,整理出来的代码如下:

 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
//定义Thread类
class Thread{
private:
    unsigned long thread_id;
    HANDLE thread_handle;
    //该函数为静态函数,被用来创建线程
    static DWORD WINAPI Wrapper(LPVOID arg);
protected:
    int return_value;
    //纯虚函数,用来被重写
    virtual void Execute() = 0;
public:
    Thread();
    ~Thread();
    void Run();
    void WaitFor();
    int GetThreadID();
    HANDLE GetHandle();
    int GetReturnValue();
    DWORD Suspend();
    DWORD Resume();
};

//线程类的构造函数
Thread::Thread(){
    thread_id = 0;
    thread_handle = 0;
    return_value = 0;
}

//线程类的析构函数
Thread::~Thread(){
    if (thread_id != 0){
        this->WaitFor();
        CloseHandle(thread_handle);
    }
}

//线程类的执行函数,用来创建线程
void Thread::Run(){
    thread_handle = CreateThread(NULL, 0, Wrapper, this, 0, &thread_id);
}

//线程类的等待函数,用来等待线程执行完毕后才继续执行下一条命令
void Thread::WaitFor(){
    if (thread_id == 0){
        return;
    }
    WaitForSingleObject(thread_handle, INFINITE);
}

//获取线程的ID
int Thread::GetThreadID(){
    return thread_id;
}

//获取线程的句柄值
HANDLE Thread::GetHandle(){
    return thread_handle;
}

//获取线程函数的返回值
int Thread::GetReturnValue(){
    return return_value;
}

//静态函数,该函数执行Execute虚函数,Execute虚函数由派生类重写
DWORD WINAPI Thread::Wrapper(LPVOID arg){
    Thread *thread_this = (Thread*)arg;
    thread_this->Execute();
    ExitThread(thread_this->return_value);
    return thread_this->return_value;
}

//挂起线程
DWORD Thread::Suspend(){
    return SuspendThread(this->thread_handle);
}

//重新运行线程
DWORD Thread::Resume(){
    return ResumeThread(this->thread_handle);
}

整个线程类的大致代码如上,当需要使用线程类的时候,直接继承这个类,并进行重写Execute函数,再调用类的Run函数,然后新的线程就开始运行了。就执行方式来看,这和python当中使用线程类的方法是非常类似的,表示我当年最初学python多线程的时候,并不明白为什么要继承python当中Thread类,然后再进行重写run函数。而现在,差不多就明白了原因了。

后来在这个线程类的基础上,我们又遇到一个问题,就是我们不知道我们创建的这个线程什么时候会结束。因为这个一个类,当线程结束的时候,一方面需要释放线程的资源,另一方面也需要释放这个类的资源,但是我们并不知道线程什么时候会结束,这样的话,我们就只能够在类中释放线程的资源,但是我们该在何处调用delete去释放这个类的资源呢?

最先想到的就是,在外面设置一个全局布尔变量,当线程执行完毕的时候,就设置这个全局变量为真,然后另外有一个函数在监视这个变量,一旦这个变量为真,然后就去执行delete释放线程类的资源。但是这样的话,就要还有一个方法时刻监视这个变量,这样的话就会增加额外的开销,能不能想一种方法实现类的自杀呢?

后来和韩老师探讨了一下,最后解决方法大致如下。

在上面那个类的Wrapper函数中进行如下更改。

1
2
3
4
5
6
7
8
9
DWORD WINAPI Thread::Wrapper(LPVOID arg){
    Thread *thread_this = (Thread*)arg;
    thread_this->Execute();
    //ExitThread函数执行之后,线程就退出了
    ExitThread(thread_this->return_value);
    //delete执行之后,类内存就被释放了
    delete thread_this;
    return 0;
}

在执行Execute函数后,直接delete就行了。因为Wrapper是一个静态函数,实际上是个全局函数,所以可以执行delete并不发生错误。就实际运行情况来看,这种使用方法并不会导致错误的产生。

但是这里有个问题,是先执行ExitThread函数还是先调用delete方法?如果调用ExitThread函数了,那么线程就退出了,线程退出了,自然也就不会再继续调用delete方法了。当顺序反过来的时候,如果先调用了delete方法,那么类的内存就释放了,所以ExitThread函数也就不会执行了。在这点如何完美的进行扫尾工作,我还暂时并不清楚,待研究之后,我再进行补充。