我们讲到进程的时候就提到了线程。

线程就相当于一个程序的灵魂,所以每个进程都一定存在至少一个线程。

所以线程十分重要。

1. 创建线程

Win32 API中创建线程的函数为 CreateThread(), MSDN的解释如下:

1
2
3
4
5
6
HANDLE CreateThread(  LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
SIZE_T dwStackSize, // initial stack size
LPTHREAD_START_ROUTINE lpStartAddress, // thread function
LPVOID lpParameter, // thread argument
DWORD dwCreationFlags, // creation option
LPDWORD lpThreadId // thread identifier);

lpThreadAttributes: 这个参数很熟悉了,如果定义了这个结构体代表继承句柄表,一定要先把结构体大小放进去。不继承直接放入NULL

lpStartAddress: 函数地址,线程执行的函数

lpParameter: 函数参数,LPVOID可以是任意类型的指针

dwCreationFlags: NULL则创建线程后直接执行, CREATE_SUSPENDED会创建线程后先挂起线程。直到ResumeThread()执行后开始启动线程。

lpThreadId:OUT类型参数,返回Thread ID。输入NULL则不接收这个参数

2. 线程控制

  1. 如何让线程停下来

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    VOID Sleep(  
    DWORD dwMilliseconds // sleep time
    );

    DWORD SuspendThread(
    HANDLE hThread // handle to thread
    );

    DWORD ResumeThread(
    HANDLE hThread // handle to thread
    );
  2. 等待线程结束

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    DWORD 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型参数,接收返回值
    );
  3. 设置,获取Thread上下文

    想象一种情况:假如你的电脑是单核的,为什么依然可以使用多线程呢?

    当从一个线程跳到另一个线程之后,CPU中的Register又怎么变换保存呢?

    操作系统已经为我们想好了怎么做,就是每次转换线程的时候,ETHREAD中都会有CONTEXT结构体来保存当前线程CPU的状态。所以我们可以通过CONTEXT来获取或者修改THREAD的寄存器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    BOOL 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
    16
    int 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
    15
    CRITICAL_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
2
3
4
5
6
7
8
9
10
11
12
13
14
HANDLE CreateMutex(  
LPSECURITY_ATTRIBUTES lpMutexAttributes, // SD 如果不允许被继承,直接给NULL
BOOL bInitialOwner, // initial owner 初始化拥有者,TRUE为初始化拥有者就是当前进程,FALSE代表此进程不是初始化的永远者。如果所有进程初始化都为FALSE,都不是MUTEX的拥有者,那么锁就打不开
LPCTSTR lpName // 这个锁的名字
);

DWORD WaitForSingleObject(
HANDLE hHandle, // handle to object // 把CreateMutex返回的HANDLE当成第一个参数, Create Mutex相当于在内核创建了一个线程锁
DWORD dwMilliseconds // time-out interval // 可以设置给互斥锁等待时间
);

BOOL ReleaseMutex(
HANDLE hMutex // handle to mutex 把锁释放掉
);

5. 事件

先看API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HANDLE CreateEvent(  
LPSECURITY_ATTRIBUTES lpEventAttributes, // SD
BOOL bManualReset, // reset type, TRUE为手动Reset,这种情况WaitForObject之后不会自动Reset,称为通知。FALSE为自动,WaitForObject之后会自动Reset为0,为互斥体
BOOL bInitialState, // initial state, TRUE为初始化时候就有信号,FALSE为初始化的时候没有信号
LPCTSTR lpName // object name
);

BOOL ResetEvent( // 用于清除信号
HANDLE hEvent // handle to event
);

BOOL SetEvent( // 用于重新设置出信号
HANDLE hEvent // handle to event
);

// ResetEvent 和 SetEvent是两个相反的操作

这个就有点像进程内的MUTEX。

第二个参数设置为TRUE代表通知,通知会同时发信号给接收者。

如果第二个参数设置为FALSE,代表互斥体。

事件和前面的Critical Section和Mutex的区别是,信号必须要在临界区执行完归还(在哪用的资源就要在哪里归还),要不然锁就会始终处于一种无信号的状态。

但是Event作为互斥体,可以在不同的线程对信号进行操作,setEvent, resetEvent。比如为消费者和生产者初始化两个互斥Event,生产者的InitialState设置为True, 消费者的InitialState设置成False。首先消费者得到信号,但是消费者没有信号。当生产者执行完后,我们不归还生产者的信号,直接set消费者的信号为有信号的状态。那么消费者开始执行,但是生产者信号没有归还,所以不执行。当消费者结束,我们不归还消费者信号,把生产者信号set为由信号的状态。 那么这样就形成了一种 互相调用的状态

这种跨线程对信号的操作就可以实现 线程同步(互斥 + 有序)

Conclusion

线程操作会涉及内核中的3个对象: 线程对象,MUTEX 对象,Event对象。 不用了需要CloseHandle来减少内核对象的计数器。