分享

Windows API教程(五) 线程编程

 快乐成群m5ct4t 2016-11-02

Windows API教程(五) 线程编程

经过了一段时间的思考,以及一阵子的忙碌时期之后觉得“进程通信”放在进程编程之后有点太早了。而且,有些背离初衷了。所以我们还是继续看下下一个知识点,也就是这一讲的主要内容 —— “线程”

作为多任务操作系统,一台电脑同时跑多个程序是一件理所当然的事情,那么实际上对于一个程序而言自然也是可以同时做多件时间。那么简单的抽象一下就能初步的理解进程与线程的概念。

操作系统管理多个程序的时候,多个程序对于系统而言是进程(Process),而一个程序同时做多件事,那么每一件事的执行线路就是该程序的线程(Thread)。即系统与进程是一对多的关系,单个进程与线程也是一对多的关系。

CreateThread

创建进程

1
2
3
4
5
6
7
8
HANDLE WINAPI CreateThread(
  _In_opt_   LPSECURITY_ATTRIBUTES lpThreadAttributes,
  _In_       SIZE_T dwStackSize,
  _In_       LPTHREAD_START_ROUTINE lpStartAddress,
  _In_opt_   LPVOID lpParameter,
  _In_       DWORD dwCreationFlags,
  _Out_opt_  LPDWORD lpThreadId
);

lpThreadAttributes [in, optional]

一个指向SECURITY_ATTRIBUTES结构体的指针该结构体决定了返回的句柄能否被子进程继承。如果lpThreadAttributes为NULL, 那么这个句柄不能被继承。

dwStackSize [in]

栈(Stack)最初的长度(reserved size),单位是字节。 The system rounds this value to the nearest page。 如果这个参数为零, 那么新线程的栈将使用默认长度。更多信息, 参见 Thread Stack Size

lpStartAddress [in]

一个指向线程执行过程的函数指针。该指针存储的地址(函数名的值是函数代码的起始地址)是线程的入口。关于更多线程函数(thread function)的信息, 请参见 ThreadProc

lpParameter [in, optional]

一个将要传递给线程的变量的地址。

dwCreationFlags [in]

该标志(flags)控制着线程的创建。

ValueMeaning
0线程创建后立即开始执行。
CREATE_SUSPENDED
0x00000004
该线程创建之后处于悬挂状态, 知道调用 ResumeThread 函数才开始执行。
STACK_SIZE_PARAM_IS_A_RESERVATION
0x00010000
前面的 dwStackSize 参数指明栈初始的保留大小。 如果这个 flag 没有立起来,则由 dwStackSize 参数来指明提交大小(commit size)。(以上来组MSDN)注意区别commit size 和 reserved size。没有竖该flag的情况下,dwStackSize这个参数用来调整一开始commit给栈的空间,即initially commit size。那么调整了commit size后,reserved size应该怎么有什么相应的调整呢?如果dwStackSize小于默认reserve大小,则reserve size使用默认reserve大小;如果dwStackSize大于默认reserve size,则reserve size将会向上取整变成1MB的整数倍。如果要求默认的StackSize为64K,则将设置/Stack:65536

lpThreadId [out, optional]

输出参数,指向返回的线程ID。如果执行完该函数之后其值为NULL, 那么表示函数执行失败。

返回值

如果函数执行成功, 那么返回值是新线程的句柄。
如果函数执行失败,那么返回值是NULL。想要获得 更多的错误信息, 请使用 GetLastError。

注意 CreateThread 这个函数在 lpStartAddress 指向一个数据,一段代码的地址,甚至一个不可读的地址时都可能成功(这是线程注入的基础吗,博主吐槽道)。如果线程执行时,过程指向的地址是无效的, 意味着异常产生, 与此同时线程会被终止,并返回一个错误码给主管它的进程。

创建两个线程

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
#include <Windows.h>
#include <stdio.h>
 
DWORD WINAPI ThreadProc (LPVOID lpThreadParameter)
{
    while(1)
    {
        printf("%s \n", (char *)lpThreadParameter);
        Sleep(533);
    }
    return 0;
}
 
int main()
{
    DWORD Tid;
    char *PassData;
 
    PassData = "Hello 线程1";
 
    CreateThread(
        NULL,       // 不能被子进程继承
        0,          // 默认堆栈大小
        ThreadProc, // 线程调用函数过程
        PassData,   // 传递参数
        0,          // 创建后立即执行
        &Tid        // 保存创建后的线程ID
    );
 
    printf("线程1 创建成功 线程ID:%u \n", Tid);
 
    PassData = "hi    线程2";
 
    CreateThread(
        NULL,       // 不能被子进程继承
        0,          // 默认堆栈大小
        ThreadProc, // 线程调用函数过程
        PassData,   // 传递参数
        0,          // 创建后立即执行
        &Tid        // 保存创建后的线程ID
    );
 
    printf("线程2 创建成功 线程ID:%u \n", Tid);
 
    system("pause");
    // 主线程暂停在这里等待用户按任意键,其他线程则继续执行
    // 不过一旦按下任意键,主线程退进程,则其他线程均终止
    // 注意这不是因为主线程的原因,在子线程中退进程效果一样
    // 因为线程是归属于进程的,进程退出那么旗下的所有线程也都会终止
 
    return 0;
}

输出效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
线程1 创建成功 线程ID:1168
Hello 线程1
线程2 创建成功 线程ID:3696
hi    线程2
Press any key to continue . . . hi    线程2
Hello 线程1
hi    线程2
Hello 线程1
hi    线程2
Hello 线程1
Hello 线程1
hi    线程2
hi    线程2
Hello 线程1
hi    线程2
Hello 线程1

很多年以前 Linus 编写最早期的 Linux 操作系统时,曾花了不少的力气来实现一个效果,即在电脑屏幕上分别有两个进程一个打印A一个打印B,显示的效果便是在屏幕上交错的出现,这个功能曾让Linus兴奋不已,因为这是多任务操作系统的标识。

有人会觉得可能会觉得疑惑,要实现这个功能的话到底是用多进程比较好还是用多线程?在我看来这二者都是可以的。就一个比较大的区别是,多个线程之间共用的是一个进程空间,所以在该进内的全局变量是所有线程都能访问的到。但是多个进程之间就不一样了,他们各自的空间都是被操作系统独立。甚至可以说系统还会提防多个进程之间的互相访问。

原因很简单,例如,我可以写一个程序偷偷的搜集你电脑上的东西,从这一个程序的进程去访问你电脑上的其他进程(可以是QQ、浏览器、你在玩的游戏或者是你的任何操作),这听起来就不那么让人愉快吧。当然,系统的主要工作不是去防止这个,这样的工作通常是由杀毒软件之类的东西去保护。

好吧,话题绕回来,继续说我们的 Linus ,当初的时候他实现多任务的功能跟我们现在已经完全不一样了。现在的我们只需要简单的调用一下系统的API函数,就可以轻松的使用这样的功能。有些人可能会有些偏执的去深究这个底层的问题(比如博主Orz),这里博主的建议是————要学会站在巨人的肩膀上。善于利用前人留下来的(咳咳,某些人还活着)知识,走一条通向未来的路,而不是把以前的老路翻出来走一遍,这也是我们学习 API 的意义所在。

贪吃蛇

那么接下来我们来写一个代码多一点的小游戏,要实现游戏的话,多线程是一个很常用的功能。比如蛇在屏幕上移动,于此同时还要有另外一个线程去监听用户输入的方向键,方向有改变的话,相应的蛇的移动也会改变。

由于时间问题,博主这里先吧完整的代码贴上来,回头会会拆成几个步骤慢慢的说。

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
#include<Windows.h>
#include<stdio.h>
#include<conio.h>
#include<time.h>
 
// 后方函数申明
void restart();
void gotoxy(int x,int y);
 
// 方向对应值
enum MoveDir { RIGHT = 0, UP = 1, LEFT = 2, DOWN = 3  };
 
// 开始时间戳, 结束时间戳
time_t start,end;
 
int food[25][20] = {0};  // 果实对应数组
int snakel[25][20] = {0};// 蛇身对应数组
 
int score = 0;      // 得分
int x = 0, y = 0;   // 坐标
int direct = RIGHT; // 移动方向
int mark = 0;       // 标记原方向
 
int length = 0;     // 蛇身链表长度
int slength = 1;    // 蛇身记录长度
 
// 方向数组
int  dir[4][2]={
        { 1,  0 }, // 0 向右  x坐标 +1  y坐标 +0
        { 0, -1 }, // 1 向上  x坐标 +0  y坐标 -1
        {-1,  0 }, // 2 向左  x坐标 -1  y坐标 +0
        { 0,  1 }  // 3 向下  x坐标 +0  y坐标 +1
    };
 
/*
    snake 结构体 用于构建贪吃蛇链表
*/
struct snake
{
    int x,y;
    struct snake *next,*pre;
} *head, *tail, *p; // 声明结构体的同时定义头指针、尾指针、临时指针
 
 
DWORD WINAPI ThreadProc(LPVOID lpPraram)
{
    SYSTEMTIME sys; // 系统时间结构体
 
    // 线程启动时, 头尾指针均为空
    head = tail = NULL;
    int a , b;
 
    // 随即获取果实坐标 (a,b)
    a=(rand()+start)%25; // [0,24]
    b=(rand()+start)%20; // [0,19]
 
    /* Note 起始时间 start + 返回的随机值 rand() = 一个不规律的随机值
     *      随后 % 求模 25 即将这个书限制在0到25之内 */
 
    // 子线程循环打印蛇身和果实
    while(1)
    {
        //打印已耗游戏时间;
        end = time(NULL);// 当前时间
        gotoxy(35,4);
        printf("%03d", (int)difftime(end,start)); // 当前时间与开始时间的差值
         
        GetLocalTime(&sys); // 获取当前时间
 
        //打印系统时间;
        gotoxy(28,18);     
        printf("%4d/%02d/%02d", sys.wYear, sys.wMonth, sys.wDay );
        gotoxy(29,19);
        printf("%02d:%02d:%02d", sys.wHour, sys.wMinute, sys.wSecond);
 
        //判断果实是否被吃了
        if(food[a][b] != '*' || snakel[a][b] == 1)
        {
            a=(rand()+start)%25; // [0,24]
            b=(rand()+start)%20; // [0,19]
            food[a][b]='*';
        }
 
        // 到屏幕的(a,b)坐标打印果实
        gotoxy(a,b); printf("*");
 
        //获取下一个坐标
        x += dir[direct][0];
        y += dir[direct][1];
        x += 25; x %= 25;
        y += 20; y %= 20;
 
        // 如果果实(food)数组中当前坐标对应为果实
        if(food[x][y]=='*')
        {          
            slength++;  // 蛇身长度自增
            score++;    // 分数自增
            // 刷新分数
            gotoxy(35,3); printf("%03d",score);
            // 清理果实(food)数组
            food[x][y]=0;
        }
        // 若不是果实则,判断当前坐标是否为蛇身
        else if(snakel[x][y]==1)
        {
            // 如果是蛇身即玩家吃到自己,游戏重启
            restart();
        }
 
        snakel[x][y]=1; // 蛇身对应新坐标置1
         
        // 创建新坐标对应的链表节点
        p = (snake*)malloc(sizeof(snake));
        p->x = x;
        p->y = y;
        p->next = p->pre = NULL;
                 
        if(head == NULL) {
        // 若只有一个节点(即蛇的长度为1,链表长度为0)
            head = tail = p;
        }
        else
        {
        // 若不只一个节点
            head->pre=p; // 头结点的前一个指向 p 节点
            p->next=head;    // p 节点下一个指向当前头结点
            head=p;         // 新的 p 变成头结点变成
        }
 
        length++; // 链表长度加1
 
        // 到新坐标,也就是头部打印 #
        gotoxy(head->x,head->y);
        printf("#");
 
        // 输出调试信息
        gotoxy(49, 10); printf("length: %d slength: %d", length, slength);
 
        // 删除蛇尾
        if(length > slength)
        {
            // 清空蛇尾数组对应值
            snakel[tail->x][tail->y]=0;
            // 控制台输出流指针移动到蛇尾
            gotoxy(tail->x,tail->y);
            // 输出空格
            printf(" ");
 
            // 临时指针指向蛇尾
            p = tail;
            // 尾指针前移
            tail = tail->pre;
            // 释放原本的尾指针
            free(p);
            // 蛇身链表长度减1
            length--;
        }
 
        /*
         * 调控速度暂时休眠
         * 如果需要控制游戏速度只需要修改休眠的值即可
         */
        _sleep(50);
 
        // 标记当前方向
        mark = direct;
    }
}
 
/*
    初始化配置
*/
void init()
{
    int i;
 
    // 绘制外围边框
    for( i=0; i<20; i++)
    {
        gotoxy(39,i); printf("|");
        gotoxy(25,i); printf("|");
    }
    for( i=0; i<40; i++)
    {
        gotoxy(i,20); printf("~");
    }
 
    // 右侧游戏记录, 规则说明
    gotoxy(26, 3); printf("游戏得分:%03d",score);
    gotoxy(26, 4); printf("游戏时间:");
    gotoxy(29,17); printf("系统时间");
 
    gotoxy(49, 1); printf("@lellansin");
    gotoxy(49, 2); printf("www.");
     
    gotoxy(49, 4); printf("一群  10191598");
    gotoxy(49, 5); printf("二群 163859361");
    gotoxy(49, 6); printf("三群  10366953");
 
    gotoxy(49, 8); printf("按ESC键退出");
 
    // 初始化游戏开始时间
    start = time(NULL);
}
 
int main()
{  
    int i = 0;
    HANDLE sonThreadHandle; // 子线程句柄
    DWORD dwThreadId;       // 子线程ID
 
    // 按键上半部分以及下半部分
    char keyCodePart1,keyCodePart2;
 
    // 执行初始化
    init();
     
    //创建线程
    sonThreadHandle = CreateThread(
        NULL,       // 不能被子进程继承
        0,          // 默认堆栈大小
        ThreadProc, // 线程调用函数过程
        NULL,       // 传递参数
        0,          // 创建后立即执行
        &dwThreadId // 保存创建后的线程ID
    );
 
    // 如果线程句柄为空,即新建线程失败
    if(sonThreadHandle == NULL)
    {
        ExitProcess(i); // 退进程, 程序结束
    }
 
    // 主线程循环获取按键
    while(1)
    {
        // getch 一次只能从输入流中读取一个字节
        // 不过一个按键通常是两个字节
        keyCodePart1 = getch(); // 获取第一个字节
 
        // 判断是否等于逃脱键(Esc)的第一个字节
        if(keyCodePart1 == 0x1b)
        {
            ExitProcess(2); // 是的话就退出进程
        }
 
        // 方向键第一个字节的值等于 -32
        if(keyCodePart1 == -32)
        {
            keyCodePart2 = getch(); // 获取第二个字节
            switch(keyCodePart2)
            {
                case(72):
                    // direct=1;
                    direct = UP;
                break;
                case(75):
                    // direct=2;
                    direct = LEFT;
                break;
                case(77):
                    // direct=0;
                    direct = RIGHT;
                break;
                case(80):
                    // direct=3;
                    direct = DOWN;
                break;
            }
            // 如果当前方向与原方向相反
            if( direct != mark && (direct+mark==RIGHT+LEFT||direct+mark==UP+DOWN) )
                direct=mark;
        }
        /*
            为什么是 0x1b 为什么是 -32 之类的值?
            这些都是原本用同样的办法,两次 getch 之后
            打印并记录下各个按键的值得到的判断
        */
    }
    return 0;
}
 
void restart()
{
    //判断死亡,重新开始游戏;
    MessageBox(NULL,(LPCSTR)"傻逼,死了吧!!!\Once More???",(LPCSTR)"GAME VOER!",MB_OK);
 
    // 分数清零
    score=0; gotoxy(35,3); printf("%03d",score);
    // 开始时间重记
    start = time(NULL);
    // 游戏已进行时间打印为0
    gotoxy(35,4); printf("%03d",0);
 
    // 从尾部开始循环消除蛇身
    while(tail)
    {
        // 休眠以延缓消除时间
        _sleep(200);
        snakel[tail->x][tail->y]=0;   // 清除当前蛇身表的值
        // 移动到屏幕对应坐标输出宫格
        gotoxy(tail->x,tail->y); printf(" ");
        // 临时指针指向尾指针的前一个节点
        p = tail->pre;
        // 释放最后一个节点
        free(tail);
        // 临时节点变成新的尾部
        tail = p;
    }
    // 所有节点释放完毕,头尾指针清空
    head = tail = NULL;
    slength = 1; // 蛇身长度置1
    length=0;    // 链表长度置0
}
 
 
/*
    跳至控制台的(x,y)坐标
*/
void gotoxy(int x,int y)
{
    COORD coord; // 控制台坐标结构体
    coord.X = x;
    coord.Y = y;
    SetConsoleCursorPosition(
        GetStdHandle( STD_OUTPUT_HANDLE ), // 获取控制台输出流的句柄
        coord   // 设置输出位置的坐标
    );
}

源代码地址:snake.zip

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多