编程基础知识|网络安全编程:多线程编程基础知识(网络编程作业)

网友投稿 278 2022-06-14


线程是进程中的一个执行单位(每个进程都必须有一个主线程),一个进程可以有多个线程,而一个线程只存在于一个进程中。在数据关系上,进程与线程是一对多的关系。线程不拥有系统资源,线程所使用的资源全部由进程向系统申请,线程拥有的是CPU的时间片。读完本文如果觉得有帮助,欢迎前往 编程基础知识 了解更多详情。

在单处理器上(或单核处理器上),同一个进程中的不同线程交替得到CPU的时间片。在多处理器上(或多核处理器上),不同的线程可以同时运行在不同的CPU上,这样可以提高程序运行的效率。除此之外,在有些方面必须使用多线程。比如,如果在扫描磁盘并同时在程序界面上同步显示当前扫描的位置时,必须使用多线程。因为在程序界面上显示和磁盘的扫描工作在同一个线程中,而且界面也在不停进行重新显示,这样就会导致软件看起来像是卡死一样。在这种情况下,分为两个线程就可以解决该问题,界面的显示由主线程完成,而扫描磁盘的工作由另外一个线程完成,两个线程协同工作,这样就可以达到实时显示当前扫描状态的效果了。

首先了解一下线程的创建。线程的创建使用CreateThread()函数,该函数的原型如下:

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD

DWORD dwStackSize, // initial stack size

LPTHREAD_START_ROUTINE lpStartAddress, // thread function

LPVOID lpParameter, // thread argument

DWORD dwCreationFlags, // creation option

LPDWORD lpThreadId // thread identifier

);

参数说明如下。

lpThreadAttributes:指明创建线程的安全属性,为指向 SECURITY_ATTRIBUTES 结构的指针,该参数一般设置为 NULL。

dwStackSize:指定线程使用缺省的堆栈大小,如果为 NULL,则与进程主线程栈相同。

lpStartAddress:指定线程函数,线程即从该函数的入口处开始运行,函数返回时就意味着线程终止运行,该函数属于一个回调函数。线程函数的定义形式如下:

DWORD WINAPI ThreadProc(

LPVOID lpParameter // thread data

);

线程函数的返回值为DWORD类型,线程函数只有一个参数,该参数在CreateThread()函数中给出。该函数的函数名称可以任意给定。很多时候并不能保证执行了CreateThread()函数后线程就会立即启动,线程的启动需要等待CPU的调度,CPU将时间片给该线程时,该线程才会执行,当然这个时间短到可以忽略它。

lpParameter:该参数表示传递给线程函数的一个参数,可以是指向任意数据类型的指针。这里是一个指针,可以方便的将多个参数通过结构体等一次性传到线程函数中。

dwCreationFlags:该参数指明创建线程后的线程状态,在创建线程后可以让线程立刻执行(这里的立即执行的意思是不会受人为的去让它处于等待状态),也可以让线程处于暂停状态。如果需要立刻执行,该参数设置为 0;如果要让线程处于暂停状态,那么该参数设置为 CREATE_SUSPENDED,待需要线程执行时调用ResumeThread()函数让线程的状态调整为等待运行的状态,然后由 CPU 分配时间片后去执行。

lpThreadId:该参数用于返回新创建线程的线程 ID。

如果线程创建成功,该函数返回线程的句柄,否则返回NULL。创建新线程后,该线程就开始启动执行了。但如果在dwCreationFlags中使用了CREATE_SUSPENDED参数,那么线程并不马上执行,而是先挂起,等到调用ResumeThread后才开始启动线程。线程的句柄需要通过CloseHandle()进行关闭,以便释放资源。

写一个简单的多线程的例子,代码如下:

#include

#include

DWORD WINAPI ThreadProc(LPVOID lpParam)

{

printf("ThreadProc \r\n");

return 0;

}

int main()

{

HANDLE hThread = CreateThread(NULL,0,ThreadProc,NULL,0,NULL);

printf("main \r\n");

CloseHandle(hThread);

return 0;

}

代码在主线程中打印一行“main”,在创建的新线程中会打印一行“ThreadProc”。编译运行,查看其运行结果,如图1所示。

图1  多线程程序输出结果

从图1中看出,程序的输出跟预期的结果并不相同。程序的问题出在了哪里呢?每个线程都有属于自己的CPU时间片,当主线程创建新线程后,主线程的CPU时间片并未结束,它会向下继续执行。由于主线程的代码非常少,因此主线程在CPU分配的时间片中就执行完成并退出了。由于主线程的结束,意味着进程也就结束并退出了。因此,在代码中创建的线程虽然被创建了,但是根本就没有执行的机会。那么在这么短的代码中,如何保证新创建的线程在主线程结束前就能得到执行呢?或者说,主线程的运行需要等待新线程的完成才得以执行。这里需要使用WaitForSingleObject()函数,该函数的原型如下:

DWORD WaitForSingleObject(

HANDLE hHandle, // handle to object

DWORD dwMilliseconds // time-out interval

);

参数说明如下。

hHandle:该参数为要等待的对象句柄。

dwMilliseconds:该参数指定等待超时的毫秒数,如果设为 0,则立即返回,如果设为 INFINITE,则表示一直等待线程函数的返回。INFINITE 是系统定义的一个宏,其定义如下。

#define INFINITE 0xFFFFFFFF

如果该函数失败,则返回WAIT_FAILED;如果等待的对象编程激发状态,则返回WAIT_ OBJECT_0;如果等待对象变成激发状态之前,等待时间结束了,将返回WAIT_TIMEOUT。

修改上面的代码,在CreateThread()函数后面加入如下代码:

WaitForSingleObject(hThread, INFINITE);

添加WaitForSingleObject()函数以后,主线程会等待新创建的线程结束再继续向下执行主线程后续的代码。这样在控制台上的输出如图2所示。

图2  主线程等待子线程的执行

WaitForSingleObject()只能等待一个线程,可是在程序中往往要创建多个线程来执行,那么如果需要等待若干个线程的完成状态的话,WaitForSingleObject()函数就无能为力了。不过,系统除了提供WaitForSingleObject()函数外,还提供了另外一个可以等待多个线程的完成状态的函数WaitForMultipleObjects(),该函数的定义如下:

DWORD WaitForMultipleObjects(

DWORD nCount, // number of handles in array

CONST HANDLE *lpHandles, // object-handle array

BOOL fWaitAll, // wait option

DWORD dwMilliseconds // time-out interval

);

该函数的参数比WaitForSingleObject()函数多2个参数,下面介绍这些参数。

nCount:该参数用于指明想要让函数等待的线程的数量。该参数的取值范围在 1 到 MAXIMUM_WAIT _OBJECTS 之间。

lpHandles:该参数是指向等待线程句柄的数组指针。

fWaitAll:该参数表示是否等待全部线程的状态完成,如果设置为 TRUE,则等待全部。

dwMilliseconds:该参数与 WaitForSingleObject()函数中的 dwMilliseconds 用法相同。

WaitForSingleObject()和WaitForMultipleObjects()两个函数除了可以等待线程外,还可以等待用于多线程同步和互斥的内核对象。

在使用多线程的时候常常需要考虑和注意的问题很多。比如多线程同时对一个共享资源进行操作,通过线程需要按照一定的顺序执行等。看一个简单的多线程例子:

int g_Num_One = 0;

DWORD WINAPI ThreadProc(LPVOID lpParam)

{

int nTmp = 0;

for ( int i = 0; i < 10; i ++ )

{

nTmp = g_Num_One;

nTmp ++;

// Sleep(1)的作用是让出 CPU

// 使其他线程被调度运行

Sleep(1);

g_Num_One = nTmp;

}

return 0;

}

每个线程都有一个CPU时间片,当自己的时间片运行完成后,CPU会停止该线程的运行,并切换到其他线程去运行。当多线程同时操作一个共享资源时,这样的切换会带来隐形的问题。这里的代码比较短,在一个CPU时间片内肯定会完成,无法体现出因线程切换而产生的错误。为了达到能够因线程切换导致的错误,在代码中加入了Sleep(1),使得线程主动让出CPU,让CPU进行线程切换。在代码中,线程处理的共享资源是全局变量g_Num_One变量。主函数创建线程的代码如下:

int main()

{

HANDLE hThread[10] = { 0 };

int i;

for ( i = 0; i < 10; i ++ )

{

hThread[i] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);

}

WaitForMultipleObjects(10, hThread, TRUE, INFINITE);

for ( i = 0; i < 10; i ++ )

{

CloseHandle(hThread[i]);

}

printf("g_Num_One = %d \r\n", g_Num_One);

return 0;

}

在主函数中,通过CreateThread()创建了10个线程,每个线程都让g_Num_One自增10次,每次的增量为1。那么10个线程会使得g_Num_One的结果变成100。编译运行上面的代码,查看输出结果,如图3所示。

图3  多线程操作共享资源的错误结果

这个结果和预测的结果并不相同。为什么会产生这种不同呢?这里进行一次模拟分析。为了方便分析,把线程的数量缩小为两个线程,分别是A线程和B线程。

① g_Num_One的初始值为0。

② 当A线程中执行nTmp = g_Num_One和nTmp++后(此时nTmp的值为1),因为Sleep(1)的原因发生了线程切换,此时g_Num_One的初始值仍然为0。

③ 当B线程中执行nTmp = g_Num_One和nTmp++后(此时nTmp的值也为1),因为Sleep(1)的原因又发生了线程切换。

④ A线程执行g_Num_One = nTmp,此时g_Num_One的值为1,接着执行下一次循环中的nTmp = g_Num_One和nTmp++的操作,又进行切换。

⑤ B线程执行g_Num_One = nTmp,此时g_Num_One的值为1。

到第⑤步时,不继续往下分析了,已经可以看出原因。g_Num_One的值是最后一次nTmp进行赋值后的值(线程中的局部变量属于线程内私有的,虽然是同一个线程函数,但是nTmp在每个线程中是私有的)。

解决该问题,这里使用的是临界区。临界区对象是一个CRITICAL_SECTION的数据结构,Windows操作系统使用该数据结构对关键代码进行保护,以确保多线程下的共享资源。在同一时间内,Windows只允许一个线程进入临界区。

临界区的函数有4个,分别是初始化临界区对象(InitializeCriticalSection())、进入临界区(EnterCriticalSection())、离开临界区(LeaveCriticalSection())和删除临界区对象(DeleteCriticalSection())。临界区很好的保护了共享资源,临界区在现实生活中有很多类似的例子。比如,在进行体检的时候,一个体检室内只有一个体检医生,体检医生会叫一个患者进去体检,这时其他人是不能进入的,当这个患者离开后,下一个患者才可以进入。这里体检医生就是一个共享的资源,而每个体检的患者是多个不同的线程。临界区就是以类似的方式保护了共享资源不被破坏的。下面依次来看一下这四个函数关于临界区的函数的定义,分别如下:

VOID InitializeCriticalSection(

LPCRITICAL_SECTION lpCriticalSection // critical section

);

VOID EnterCriticalSection(

LPCRITICAL_SECTION lpCriticalSection // critical section

);

VOID LeaveCriticalSection(

LPCRITICAL_SECTION lpCriticalSection // critical section

);

VOID DeleteCriticalSection(

LPCRITICAL_SECTION lpCriticalSection // critical section

);

这4个API函数的参数都是指向CRITICAL_SECTION结构体的指针。修改上面有问题的代码,修改后的代码如下:

#include

#include

int g_Num_One = 0;

CRITICAL_SECTION g_cs;

DWORD WINAPI ThreadProc(LPVOID lpParam)

{

int nTmp = 0;

for ( int i = 0; i < 10; i ++ )

{

// 进入临界区

EnterCriticalSection(&g_cs);

nTmp = g_Num_One;

nTmp ++;

Sleep(1);

g_Num_One = nTmp;

// 离开临界区

LeaveCriticalSection(&g_cs);

}

return 0;

}int main()

{

InitializeCriticalSection(&g_cs);

HANDLE hThread[10] = { 0 };

int i;

for ( i = 0; i < 10; i ++ )

{

hThread[i] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);

}

WaitForMultipleObjects(10, hThread, TRUE, INFINITE);

printf("g_Num_One = %d \r\n", g_Num_One);

for ( i = 0; i < 10; i ++ )

{

CloseHandle(hThread[i]);

}

DeleteCriticalSection(&g_cs);

return 0;

}

编译以上代码并运行,输出结果为想要的正确结果,即g_Num_One的值为100。除了使用临界区以外,对于线程的同步与互斥还有其他方法,这里就不一一进行介绍了。在开发多线程程序时,要注意多线程的同步与互斥问题。临界区对象只能用于多线程的互斥。这篇编程基础知识就先讲到这儿,更多资讯欢迎前往并关注:。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Java简短表白代码:你是我生命的唯一(java编程表白代码)
下一篇:Win10新添实用功能,你发现了吗?
相关文章

 发表评论

暂时没有评论,来抢沙发吧~