我们讲到进程的时候就提到了线程。
线程就相当于一个程序的灵魂,所以每个进程都一定存在至少一个线程。
所以线程十分重要。
1. 创建线程
Win32 API中创建线程的函数为 CreateThread(), MSDN的解释如下:
1 | HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD |
lpThreadAttributes: 这个参数很熟悉了,如果定义了这个结构体代表继承句柄表,一定要先把结构体大小放进去。不继承直接放入NULL
lpStartAddress: 函数地址,线程执行的函数
lpParameter: 函数参数,LPVOID可以是任意类型的指针
dwCreationFlags: NULL则创建线程后直接执行, CREATE_SUSPENDED会创建线程后先挂起线程。直到ResumeThread()执行后开始启动线程。
lpThreadId:OUT类型参数,返回Thread ID。输入NULL则不接收这个参数
2. 线程控制
如何让线程停下来
1
2
3
4
5
6
7
8
9
10
11VOID Sleep(
DWORD dwMilliseconds // sleep time
);
DWORD SuspendThread(
HANDLE hThread // handle to thread
);
DWORD ResumeThread(
HANDLE hThread // handle to thread
);等待线程结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16DWORD WaitForSingleObject(
HANDLE hHandle, // handle to object
DWORD dwMilliseconds // time-out interval
);
DWORD WaitForMultipleObjects(
DWORD nCount, // Wait Thread的个数
CONST HANDLE *lpHandles, //Thread Handle Array
BOOL fWaitAll, // FALSE是等待到任意一个结束了,就不等待了。TRUE就是等待所有Thread都结束
DWORD dwMilliseconds // 等待时间, 设置成INFINITE代表一直等下去
);
BOOL GetExitCodeThread(
HANDLE hThread, // handle to the thread
LPDWORD lpExitCode // termination status 这个是OUT型参数,接收返回值
);设置,获取Thread上下文
想象一种情况:假如你的电脑是单核的,为什么依然可以使用多线程呢?
当从一个线程跳到另一个线程之后,CPU中的Register又怎么变换保存呢?
操作系统已经为我们想好了怎么做,就是每次转换线程的时候,
ETHREAD
中都会有CONTEXT
结构体来保存当前线程CPU的状态。所以我们可以通过CONTEXT
来获取或者修改THREAD的寄存器1
2
3
4
5
6
7
8
9BOOL GetThreadContext(
HANDLE hThread, // handle to thread with context
LPCONTEXT lpContext // context structure OUT型参数,返回当前Thread的CONTEXT对象
);
BOOL SetThreadContext(
HANDLE hThread, // handle to thread
CONST CONTEXT *lpContext // context structure // IN类型参数,修改的CONTEXT
);一般获取和修改前都会SuspendThread
以下是获取一个Thread中CPU寄存器信息的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int main()
{
HANDLE hThread1;
hThread1 = CreateThread(NULL, 0, fun1, 0, 0, NULL);
SuspendThread(hThread1);
CONTEXT context = { 0 };
context.ContextFlags = CONTEXT_INTEGER;
GetThreadContext(hThread1, &context);
printf("EAX: %x, EBX: %x\n", context.Eax, context.Ebx);
ResumeThread(hThread1);
WaitForSingleObject(hThread1, INFINITE);
CloseHandle(hThread1);
return EXIT_SUCCESS;
}3. 临界区
C中的如果多段代码要同时修改一个变量,那么这个变量就是临界资源,那几段代码叫做临界区。
如果我们要完成互斥,那么每次只能允许临界资源在一个临界区内。而其它临界区拿不到临界资源。
Win32 API中用Critical Section实现了临界区的互斥。
WIN32 接口如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15CRITICAL_SECTION cs; //全局变量,模拟临界资源
DWORD WINAPI ThreadProc(LPVOID lpParameter) {
EnterCriticalSection(&cs);
.......使用临界资源
这个里面的代码叫做临界区
临界区中的代码会一次性执行完
再把临界资源还回给去
LeaveCriticalSection(&cs);
}
int main() {
InitializeCriticalSection(&cs); //初始化临界资源
}
4. MUTEX
MUTEX相当于存在系统内核中的一个临界资源。
Critical Section是只适用于同一个进程中的不同线程。
而MUTEX的用处在于跨进程的线程互相调度。(可用于防止软件多开)
用法类似Critical Section, API如下:
1 | HANDLE CreateMutex( |
5. 事件
先看API
1 | HANDLE CreateEvent( |
这个就有点像进程内的MUTEX。
第二个参数设置为TRUE代表通知,通知会同时发信号给接收者。
如果第二个参数设置为FALSE,代表互斥体。
事件和前面的Critical Section和Mutex的区别是,信号必须要在临界区执行完归还(在哪用的资源就要在哪里归还),要不然锁就会始终处于一种无信号的状态。
但是Event作为互斥体,可以在不同的线程对信号进行操作,setEvent, resetEvent。比如为消费者和生产者初始化两个互斥Event,生产者的InitialState设置为True, 消费者的InitialState设置成False。首先消费者得到信号,但是消费者没有信号。当生产者执行完后,我们不归还生产者的信号,直接set消费者的信号为有信号的状态。那么消费者开始执行,但是生产者信号没有归还,所以不执行。当消费者结束,我们不归还消费者信号,把生产者信号set为由信号的状态。 那么这样就形成了一种 互相调用的状态。
这种跨线程对信号的操作就可以实现 线程同步(互斥 + 有序)
Conclusion
线程操作会涉及内核中的3个对象: 线程对象,MUTEX 对象,Event对象。 不用了需要CloseHandle来减少内核对象的计数器。