考虑到内容的连贯性,我对几乎重写了这篇博客,在这一小节,主要介绍线程以及线程间的同步,而把那个聊天工具放到下一节。
什么是程序?程序是计算计指令的集合,它以文件的形式储存在磁盘上。
什么是进程?进程是一个正在运行的程序的实例,是一个程序在其自身的地址空间内中的一次执行活动。因此,一个程序可以对应多个进程,比如我们可以把自己编写的简单的“hello, world”程序执行很多遍。
进程是资源申请、调度很运行的基本单位,因此:
#include <stdio.h>
int a = 0;
int main()
{
printf("%d",a);
++a;
return 0;
}
尽管a是“全局”变量,但假如我们把这个程序执行2遍,每次打印出来的结果都是0。
在Windows系统下,进程有两部分组成:
(1)操作系统用来管理进程的内核对象。
这些对象用来存放进程统计信息。他们是操作系统内部分配的内存块,只能被内核访问使用,用用程序无法找到该数据结构,并直接改变其内容。Windows提供了一些函数来对内核对象进行操作。
(2)地址空间
它包含所有可执行的模块或DLL模块的代码和数据,也包含动态分配的空间。例如线程的栈和堆。
关于进程的知识我们会在后面的章节仔细讲解。
实际上,进程从来不执行任何东西,若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。也就是说,真正完成代码执行的是线程,而进程只是线程的容器。线程是使用系统资源的基本单位。
线程也由两部分组成:
(1)线程的内核对象。操作系统用它来对线程进行管理,存放线程的统计信息。
(2)线程栈。它用于维护线程执行代码时所需要的所有函数和参数的局部变量。
线程只有一个内核对象和一个栈,开销相对较少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程。
与线程相关的基本函数包括:
CreateThread:创建线程
CloseHandle:关闭线程句柄。注意,这只会使指定的线程句柄无效(减少该句柄的引用计数),启动句柄的检查操作,如果一个对象所关联的最后一个句柄被关闭了,那么这个对象会从系统中被删除。关闭句柄不会终止相关的线程。
线程是如何运行的呢?这又与你的CPU有关系了,如果你是一个单核CPU,那么系统会采用时间片轮询的方式运行每个线程;如果你是多核CPU,那么线程之间就有可能并发运行了。这样就会出现很多问题,比如两个线程同时访问一个全局变量之类的。它们需要线程的同步来解决。所谓同步,并不是多个线程一起同时执行,而是他们协同步调,按预定的先后次序执行。
Windows下线程同步的基本方法有3种:互斥对象、事件对象、关键代码段(临界区),下面一一介绍:
互斥对象属于内核对象,包含3个成员:
1.使用数量:记录了有多少个线程在调用该对象
2.一个线程ID:记录互斥对象维护的线程的ID
3.一个计数器:前线程调用该对象的次数
与之相关的函数包括:
创建互斥对象:CreateMutex
判断能否获得互斥对象:WaitForSingleObject
对于WaitForSingleObject,如果互斥对象为有信号状态,则获取成功,函数将互斥对象设置为无信号状态,程序将继续往下执行;如果互斥对象为无信号状态,则获取失败,线程会停留在这里等待。等待的时间可以由参数控制。
释放互斥对象:ReleaseMutex
当要保护的代码执行完毕后,通过它来释放互斥对象,使得互斥对象变为有信号状态,以便于其他线程可以获取这个互斥对象。注意,只有当某个线程拥有互斥对象时,才能够释放互斥对象,在其他线程调用这个函数不得达到释放的效果,这可以通过互斥对象的线程ID来判断。
我们看一个例子:
#include <Windows.h>
#include <stdio.h>
//线程函数声明
DWORD WINAPI Thread1Proc( LPVOID lpParameter);
DWORD WINAPI Thread2Proc( LPVOID lpParameter);
//全局变量
int tickets = 100;
HANDLE hMutex;
int main()
{
HANDLE hThread1;
HANDLE hThread2;
//创建互斥对象
hMutex = CreateMutex( NULL, //默认安全级别
FALSE, //创建它的线程不拥有互斥对象
NULL); //没有名字
//创建线程1
hThread1 = CreateThread(NULL, //默认安全级别
0, //默认栈大小
Thread1Proc,//线程函数
NULL, //函数没有参数
0, //创建后直接运行
NULL