Windows API教程(五) 线程编程

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

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

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

CreateThread

创建进程

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)控制着线程的创建。

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

创建两个线程


#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 创建成功 线程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 的意义所在。

贪吃蛇

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

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

#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.lellansin.com");
	
	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

上一讲:Windows API 教程(四) 进程编程
下一讲:Windows API 教程(六) 动态链接库

Advertisements

5 thoughts on “Windows API教程(五) 线程编程

  1. 您的文章写的特别好,受益匪浅!

    我在 Visual Studio 2013 下跑您的代码,似乎 _sleep 之类的函数已经被认为过时了,建议使用 Sleep 替代。

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s