本篇文章总结一下多线程中,Win32 API和C# API的对应关系,它们时常是类似的,但也存在一些区别。

线程

创建线程

创建线程,一般要指定线程要“做什么”,即指定一个函数指针或者委托。

Win32使用CreateThread函数来创建线程:

1
2
3
4
5
6
7
8
HANDLE CreateThread(
PSECURITY_ATTRIBUTES pThreadAttributes,
DWORD dwStackSize,
PTHREAD_START_ROUTINE pStartAddress,
PVOID pParameter,
DWORD dwCreationFlags,
PDWORD pdwThreadId
)

C#对线程进行了封装,使用以下语句创建线程:

1
2
3
Thread t = new Thread(()=>{
Console.WriteLine("Hello,Thread");
});

对于同一件事情,两者的实现会不一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <windows.h>

DWORD WINAPI ThreadFunc(LPVOID);

int main()
{
HANDLE hThread;
DWORD threadId;
hThread = CreateThread(NULL, 0, ThreadFunc, 0, 0, &threadId);
return 0;
}

DWORD WINAPI ThreadFunc(LPVOID p)
{
int p0 = (int)p;
printf("Parameter:%d\n", p0);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Threading;

public class Program{
public static void Main(string[] args)
{
Thread t = new Thread(ThreadFunc);
t.Start(0);
}

static void ThreadFunc(Object o)
{
int p0 = (int)o;
Console.WriteLine($"Parameter:{p0}");
}
}

强制结束线程

我们偶尔需要强制结束某线程。

Win32可以通过在线程中调用ExitThread函数来强制结束主调线程:

1
VOID ExitThread(DWORD dwExitCode);

Win32也可以通过传递HANDLE参数来强制结束HANDLE所指向的线程对象:

1
BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);

C#也有多种方式结束线程:

  • Thread的Abort实例方法。t.Abort();Thread.CurrentThread.Abort();
  • Application.ExitThread();

挂起(Suspend)和恢复(Resume)

Win32使用SuspendThread函数来挂起线程,使用ResumeThread函数来恢复线程:

1
2
DWORD SuspendThread(HANDLE hThread);
DWORD ResumeThread(HANDLE hThread);

C#使用Thread的实例方法Suspend来挂起线程,使用Resume方法来恢复线程:

1
2
t.Suspend();
t.Resume();

Suspend和Resume已过时。

睡眠(Sleep)

Win32使用Sleep函数来睡眠主调线程:

1
2
3
VOID Sleep(DWORD dwMilliseconds);

Sleep(100);

C#使用Thread.Sleep方法来睡眠主调线程:

1
Thread.Sleep(100);

优先级(Priority)

Win32使用SetThreadPriority函数来设置线程的优先级:

1
2
3
4
5
6
BOOL SetThreadPriority(
HANDLE hThread,
int nPriority
);

SetThreadPriority(THREAD_PRIORITY_ABOVE_NORMAL);

Win32使用GetThreadPriority函数来获得线程的优先级:

1
2
3
int GetThreadPriority(
HANDLE hThread
);

C#使用Thread对象的Priority属性来设置和获得线程的优先级:

1
2
t.Priority = ThreadPriority.AboveNormal;
int priority = t.Priority;

线程同步机制

线程同步一般分为两种:用户模式下的线程同步和内核模式下的线程同步。

用户模式,特点是快,因为它与操作系统是无关的,所有操作基于硬件来完成。

内核模式基于操作系统,它慢于用户模式很多,但功能更全面,通过各种内核对象,我们能解决一些用户模式不能解决的线程同步问题。

一般而言,解决线程同步的问题时,如果要考虑最佳性能,要先考虑使用用户模式下的线程同步,如果不能满足要求,再考虑使用内核模式下的线程同步。

C# 提供了混合模式线程同步,它合并使用用户模式和内核模式的线程同步,综合了两者的优点,也解决了一些性能问题。

使用C#时,如果问题比较复杂,又要考虑性能,可以使用混合线程同步。

用户模式(User Mode)下的线程同步

原子访问(Atomic Access)

原子访问指的是一个线程在访问某个资源的同时保证没有其它线程会在同一时刻访问同一资源。

Win32提供了Interlocked系列函数来保证对变量进行原子级别的增、减、交换操作:

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
LONG InterlockedIncrement(PLONG plAddend);

LONG InterlockeDecrement(PLONG plAddend);

LONG InterlockedExchangeAdd(
PLONG volatile plAddend,
LONG lIncrement
);

LONGLONG InterlockedExchangeAdd64(
PLONGLONG volatile pllAddend,
LONGLONG llIncrement
);

LONG InterlockedExchange(
PLONG volatile plTarget,
LONG lValue
);

LONGLONG InterlockedExchange64(
PLONGLONG volatile plTarget,
LONGLONG lValue
);

PVOID InterlockedExchangePointer(
PVOID* volatile ppvTarget,
PVOID pvValue
);

PLONG InterlockedCompareExchange(
PLONG plDestination,
LONG lExchange,
LONG lComparand
);

PLONG InterlockedCompareExchange64(
PLONGLONG pllDestination,
LONGLONG llExchange,
LONGLONG llComparand
);

LONG InterlockedCompareExchangePointer(
PVOID* ppvDestination,
PVOID pvExchange,
PVOID pvComparand
);

C#封装了Volatile类、Interlocked类,方法甚多,详情请看文档:

C# .NET 5 Document: Volatile

C# .NET 5 Document: Interlocked

Volatile类基于易变构造,提供了原子级别的读(Read)/写(Write)操作。

Interlocked类基于互锁构造,提供了原子级别的加(Add正数, Increment)/减(Add负数, Decrement)/交换(Exchange, CompareExchange)操作,目前版本的.NET还加入了位运算(Add, Or, Xor)等操作,更加人性化。

用原子访问的方式去实现自旋锁(Spin Lock),即可完成线程同步,但是自旋锁是一种忙等(Busy Loop),最好避免使用这种方式完成线程同步。

Win32可以这样实现自旋锁:

1
2
3
4
5
6
7
8
9
10
11
12
BOOL globalResourceInUse = FALSE;
void Func1() {
// 等待访问公共资源
while(InterlockedExchange(&globalResourceInUse, TRUE) == TRUE)
Sleep(0);

// 获得到了公共资源,开始处理相应逻辑
// ......

// 逻辑做完了,要归还公共资源
InterlockedExchange(&globalResourceInUse, FALSE);
}

C#可以这样实现自旋锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
internal struct SimpleSpinLock
{
// 0代表false,1代表true
private int m_globalResourceInUse = 0;

public void Enter()
{
while(true)
{
if(Interlocked.Exchange(ref m_globalResourceInUse, 1) == 0)
return;

// 额外逻辑,黑科技
}
}

public void Leave()
{
Volatile.Write(ref m_globalResourceInUse, 0);
// 也可以使用Interlocked.Exchange(ref m_globalResourceInUse, 0);
}
}

临界区/关键段(Critical Section)

临界区是用户模式下的一种线程同步机制。相比原子访问,它可以不仅仅是对某一个变量进行值的更改,它可以做一系列更多的逻辑。同一时间只能有一个线程进入某临界区,完成相应逻辑,待该线程退出临界区后,其它线程才能有机会进入该临界区。

Win32提供了四个函数,分别初始化、销毁、进入、离开临界区:

  • 初始化使用InitializeCriticalSection函数

    1
    2
    3
    VOID InitializeCriticalSection(
    PCRITICAL_SECTION pCriticalSection
    );
  • 销毁使用DeleteCriticalSection函数:

    1
    2
    3
    VOID DeleteCriticalSection(
    PCRITICAL_SECTION pCriticalSection
    );
  • 进入使用EnterCriticalSection函数:

    1
    2
    3
    VOID EnterCriticalSection(
    PCRITICAL_SECTION pCriticalSection
    );
  • 离开使用LeaveCriticalSection函数:

    1
    2
    3
    VOID LeaveCriticalSection(
    PCRITICAL_SECTION pCriticalSection
    );

    C#使用Monitor.Enter方法进入临界区:

1
2
3
4
5
6
7
8
9
10
public static class Monitor 
{
// ..........
public static extern void Enter(Object obj);
// ..........
}

// Monitor.Enter用法
int criticalSectionValue;
Monitor.Enter(criticalSectionValue);

C#使用Monitor.Exit方法退出临界区:

1
2
3
4
5
6
7
8
9
10
public static class Monitor 
{
// ..........
public static extern void Exit(Object obj);
// ..........
}

// Monitor.Exit用法
int criticalSectionValue;
Monitor.Exit(criticalSectionValue);

Win32的临界区和C# Monitor实现的临界区只是功能类似,但内部实现方式并不相同。

Win32 API提供的临界区是Windows非托管代码实现的,内部使用自旋锁来提升性能。

C#的Monitor提供的临界区是托管的,基于同步块来设计实现的,并且,Monitor其实并不属于用户模式下的线程同步,它属于混合模式下的线程同步。

内核模式(Kernel Mode)下的线程同步

等待内核对象激发

常用内核对象包括:

  • 进程(Process)
  • 线程(Thread)
  • 文件(File)
  • 事件(Event)
  • 信号量(Semaphore)
  • 互斥器(Mutex)
  • 管道(Pipe)

它们都有两种状态:未激发状态和激发状态。激发意味着它们的内部状态发生了变化,对于线程而言,激发指线程结束。

Win32使用WaitForSingleObject函数来等待单个内核对象被激发:

1
2
3
4
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);

Win32使用WaitForMultipleObjects函数来等待多个内核对象被激发:

1
2
3
4
5
6
DWORD WaitForMultipleObjects(
DWORD dwCount,
CONST HANDLE *pHandles,
BOOL bWaitAll,
DWORD dwMilliseconds
);

C#提供了抽象类WaitHandle,里面的WaitOne方法本质上会调用Win32的WaitForSingleObject函数,WaitAll和WaitAny方法本质上会调用Win32的WaitForMultipleObjects函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class WaitHandle : MarshalByRefObject, IDisposable{
// 内部调用Win32 WaitForSingleObject函数
public virtual bool WaitOne();
public virtual bool WaitOne(int millisecondsTimeout);
public virtual bool WaitOne(TimeSpan timeout);

// 内部调用Win32 WaitForMultipleObjects函数
public static bool WaitAll(WaitHandle[] waitHandles);
public static bool WaitAll(WaitHandle[] waitHandles, int millisecondsTimeout);
public static bool WaitAll(WaitHandles[] waitHandles, TimeSpan timeout);

// 内部调用Win32 WaitForMultipleObjects函数
public static bool WaitAny(WaitHandles[] waitHandles);
public static bool WaitAny(WaitHandle[] waitHandles, int millisecondsTimeout);
public static bool WaitAny(WaitHandles[] waitHandles, TimeSpan timeout);

// 超时就从WaitAny返回这个
public const int WaitTimeout = 258;

// 内部调用Win32 CloseHandle函数
public void Dispose();
}

注意:

WaitHandle是静态抽象类。

FCL中WaitHandle的类层次结构:

  • WaitHandle
    • EventWaitHandle
      • AutoResetEvent
      • ManualResetEvent
    • Semaphore
    • Mutex

所以,AutoResetEvent、ManualResetEvent、Semaphore、Mutex的实例对象都可以调用WaitOne方法来等待内核对象被激发,也可以直接调用WaitHandle的公有静态方法WaitAll和WaitAny来处理等待多个内核对象的情况。

互斥器(Mutex)

互斥器与临界区类似,同一时间,只能有一个线程对某些资源做某些操作。但互斥器是内核模式(kernel-mode)下的一种线程同步机制,它比临界区要花费几乎100倍的时间。优点是它支持跨进程使用,而临界区不支持;它能指定等待时间,而临界区不能。

Win32使用CreateMutex函数来创建互斥器:

1
2
3
4
5
HANDLE CreateMutex(
PSECURITY_ATTRIBUTES pMutexAttributes,
BOOL bInitialOwner,
PCTSTR pName
);

Win32使用OpenMutex函数来得到一个已经存在的互斥器的句柄:

1
2
3
4
5
HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pName
);

Win32使用ReleaseMutex函数来释放一个互斥器:

1
2
3
BOOL ReleaseMutex(
HANDLE hMutex
);

C#封装了Mutex类,通过新建Mutex对象来创建互斥器;通过调用静态方法OpenExisting来获得一个已经存在的互斥器对象;通过调用ReleaseMutex方法来释放互斥器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public sealed class Mutex : WaitHandle
{
// 内部调用Win32 CreateMutex函数
public Mutex();
public Mutex (bool initiallyOwned);
public Mutex (bool initiallyOwned, string name);
public Mutex (bool initiallyOwned, string name, out bool createdNew);

// 内部调用Win32 OpenMutex函数
public static Mutex OpenExisting (string name);

// 内部调用Win32 ReleaseMutex函数
public void ReleaseMutex();
}

临界区与互斥器在使用上是差不多的:

初始化临界区 <-> 创建互斥器

进入临界区 <-> 等待互斥器内核对象被激发(被释放)

离开临界区 <-> 释放互斥器

销毁临界区 <-> 调用CloseHandle

互斥器是有拥有者(owner)的概念的,一旦互斥器被某线程拥有,则只有该线程能够释放它。

注意:

如果互斥器的拥有者线程在互斥器释放前终止了,则互斥器被遗弃(abandoned),将互斥器对象的线程ID设为0,递归计数设为0。系统接下来判断还有没有等待该互斥量的线程,如果有,会公平地选择正在等待该互斥量的线程,把对象内部的线程ID设为等待线程的ID,并把递归计数设为1。

信号量(Semaphore)

信号量也是内核模式下的一种线程同步机制,它可以很好地解决生产者/消费者问题。它可以被不同线程获得(锁住)n次,也可以由任意线程释放。互斥器可以草率理解为特殊的信号量:n为1的信号量,但是事实并非如此,因为信号量是不存在拥有者的概念的,而互斥器存在拥有者的概念。因为信号量不存在拥有者,所以任何线程都可以尝试获得(锁住)和释放信号量。

Win32使用CreateSemaphore函数来创建信号量:

1
2
3
4
5
6
HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTES pAttributes,
LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pName
);

Win32使用OpenSemaphore函数来得到一个已经存在的信号量的句柄:

1
2
3
4
5
HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pName
);

Win32使用ReleaseSemaphore函数来释放信号量:

1
2
3
4
5
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
PLONG pPreviousCount
);

C#封装了Semaphore类,通过新建Semaphore对象来初始化和创建信号量对象;通过调用Release方法来释放信号量对象;通过调用静态方法OpenExisting来获得一个已经存在的信号量对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public sealed class Semaphore : WaitHandle
{
// 内部调用Win32 CreateSemaphore函数
public Semaphore(int initialCount, int maximumCount);
public Semaphore (int initialCount, int maximumCount, string name);
public Semaphore (int initialCount, int maximumCount, string name, out bool createdNew);

// 内部调用Win32 OpenSemaphore函数
public static Semaphore OpenExisting (string name);

// 内部调用Win32 ReleaseSemaphore函数
public int Release();
public int Release(int releaseCount);
}

事件(Event)

事件也是内核模式下的一种线程同步机制,它实际上是内核维护的一个bool变量。事件为false(未激发),在事件上等待的线程就阻塞;事件为true(激发),就解除阻塞。事件分为两种:自动重置事件和手动重置事件。当一个自动重置事件为true时,它只唤醒一个阻塞的线程,然后内核将该事件自动重置(auto-reset)为false,其它等待线程继续阻塞。当一个手动重置事件为true时,它解除所有等待线程的阻塞,必须由你自己重新设置(reset)它为false。

Win32使用CreateEvent函数来创建事件对象:

1
2
3
4
5
6
HANDLE CreateEvent(
PSECURITY_ATTRIBUTES pEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
PCTSTR pName
);

Win32使用OpenEvent函数来获得一个已经创建的事件对象的句柄:

1
2
3
4
5
HANDLE OpenEvent(
DWORD dwDesiredAccess,
BOOL bInherit,
PCTSTR pName
);

Win32使用SetEvent来设置事件为true,使用ResetEvent来重新设置事件为false,使用PulseEvent来先设置事件为true,再立即将事件设置为false(SetEvent以后立即ResetEvent):

1
2
3
4
5
BOOL SetEvent(HANDLE hEvent);

BOOL ResetEvent(HANDLE hEvent);

BOOL PulseEvent(HANDLE hEvent);

自动重置事件通常不需要调用ResetEvent函数,因为当自动重置事件被Set为true(激发状态)以后,内核会自动重设该事件为false(未激发状态)。

C#封装了EventWaitHandle类型,并派生了AutoResetEvent类型和ManualResetEvent类型:

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
public class EventWaitHandle : WaitHandle
{
// 内部调用Win32 CreateEvent函数
public EventWaitHandle (bool initialState, EventResetMode mode);
public EventWaitHandle (bool initialState, EventResetMode mode, string name);
public EventWaitHandle (bool initialState, EventResetMode mode, string name, out bool createdNew);

// 内部调用Win32 SetEvent函数,将事件设置为true,总是返回true
public bool Set();

// 内部调用Win32 OpenEvent函数,打开一个已经存在的事件
public static EventWaitHandle OpenExisting (string name);

// 内部调用Win32 ResetEvent函数,将事件设为false,总是返回false
public bool Reset();
}

// 单纯只是帮你设置了EventResetMode为AutoReset
public sealed class AutoResetEvent : EventWaitHandle
{
public AutoResetEvent(bool initialState): base(initialState,EventResetMode.AutoReset){}
}

// 单纯只是帮你设置了EventResetMode为ManualReset
public sealed class ManualResetEvent : EventWaitHandle
{
public ManualResetEvent(bool initialState) : base(initialState,EventResetMode.ManualReset){}
}

由于PulseEvent仅仅相当于调用SetEvent后立即调用ResetEvent,用处并不大,所以C# EventWaitHandle类型中并没有提供与之对应的Pulse方法。

注意:

如果需要跨线程、进程获得事件对象,需要创建EventWaitHandle对象,需要在其构造方法中指定名称,否则是无法获得到的。

⬆︎TOP