第三章--Win32程序的执行单元(部分概念及代码讲解)(中-线程同步

学习《Windows程序设计》记录

概念贴士:

1.  同步可以保证在一个时间内只有一个线程对其共享资源有控制权。PS:共享资源包括全局变量、公共数据成员或者句柄等。

2.  临界区内核对象和时间内核对象可以很好地用于多线程同步和它们之间的通信。

3.  线程同步必要性:当多个线程在同一个进程中执行时,可能有不止一个线程同时执行同一段代码,访问同一段内存中的数据。多个线程同时读取共享数据没有问题,但是如果同时读写,情况就不同,也许会产生极大的错误。(如:程序CountErr)。解决同步问题,就是保证整个存取过程的独占性。

4.  临界区对象是定义在数据段中的一个CRITICAL_SECTION结构,Windows内部使用这个结构记录一些信息,确保在同一时间段只有一个线程访问该数据段中的数据。(临界区又叫关键代码段)。

5.  临界区的使用步骤:编程时,要把临界区对象定义在想保护的数据段中,然后在任何线程使用此临界区对象之前对它进行初始化。之后,线程访问临界区中的数据时,必须先调用EnterCriticalSection函数,申请进入临界区。当操作完成时,还要将临界区交还给Windows,以便其他线程可以申请使用。这一步由LeaveCriticalSection函数完成。当程序不再使用临界区对象时,必须使用DeleteCriticalSection函数将它删除。(如:程序CriticalSection

6.  同一时间内,Windows只允许一个线程进入临界区。所以在申请的时候,如果有另一个线程在临界区的话,EnterCriticalSection函数会一直等待下去,直到其他线程离开临界区才返回。

7.  临界区对象能够很好地保护共享数据,但是它不能够用于进程之间资源的锁定,因为它不是内核对象。如果要在进程之间维持线程的同步必须使用事件内核对象。

8.  互锁函数为同步访问多线程共享变量提供了一个简单的机制。如果变量在共享内存,不同进程的线程也可以使用此机制。

9.  多线程程序设计大多会涉及线程间相互通信。使用编程就要涉及线程的问题。

10.  事件对象是一种抽象的对象,它也有未受信和受信两种状态,编程人员也可以使用WaitForSingleObject函数等待其变成受信状态。不同于其他内核对象的是一些函数可以使事件对象在这两种状态之间转化。

11.  如果想使用事件对象,首先需要调用CreateEvent函数去创建它。

12.  当一个人工重置的事件对象受信之后,所有等待在这个事件上的线程都会变成可调度状态;可是当一个自动重置的时间对象受信以后,Windows仅允许一个等待在该事件上的线程变成可调度状态,然后就自动重置此事件对象为未守信状态。

13.  为事件对象命名是为了在其他地方(如:其他进程的线程中)使用OpenEvent或CreateEvent函数获取此内核对象的句柄。系统创建或打开一个事件内核对象后,会返回事件的句柄。当变成人员不适用此内核对象的时候,应该调用CloseHandle函数释放它占用的资源。

14.  事件对象主要用于线程间通信,因为它是一个内核对象,所以也可以跨进程使用。

15.  线程局部存储(Thread Local Storage,TLS)是一个使用方便的存储线程局部数据的系统。利用TLS机制可以为进程中所有的线程关于关联若干个数据,各个线程通过由TLS分配的全局索引来访问与自己关联的数据。

16.  Microsoft保证至少有TLS_MINIMUM_AVAILABLE(定义在WinNT.h文件中)个标志位可用。

17.  动态使用TLS典型步骤如下:(如:程序UseTLS

      1)主线程调用TlsAlloc函数为线程局部存储分配索引。TlsAlloc的返回值就是数组的一个下标(索引)。这个位数组的唯一用途就是记忆哪一个下标在使用中。成员初始值均为FREE。当调用TlsAlloc时,系统会检查数组中数据,直到找到一个值为FREE的成员。将其值改为INUSE后,TlsAlloc函数返回该成员的索引。

      2)每个线程调用TlsSetValue和TlsGetValue设置或读取线程数组中的值。调用TlsSetValu函数,一个线程只能改变自己线程数组中成员的值,不可以为其他线程设置TLS值。到现在为止,将数据从一个线程传到另一个线程的唯一办法是在创建线程时使用线程函数的参数。TlsSetValue和TlsSetValue分别用于设置和取得线程数组中的特定成员的值,而它们使用的索引就是TlsAlloc函数的返回值。

      3)主线程调用TlsFree释放局部存储索引。函数的唯一参数是TlsAlloc返回的索引。

18.  一般情况下,为各线程分配TLS索引的工作要在主线程中完成,而分配的索引值应该保存在全局变量中,以方便各线程访问。

19.  用于线程同步的内核对象还有互斥体和信号量。不作介绍。

代码解释:

1.CountErr

  PS:程序中多个线程执行任务,可以看出多个线程共享数据,产生的问题。

 1 #include <stdio.h>
 2 #include <windows.h>
 3 #include <process.h>
 4
 5 int g_nCount1 = 0;
 6 int g_nCount2 = 0;
 7 BOOL g_bContinue = TRUE;
 8
 9 UINT __stdcall ThreadFunc(LPVOID);
10
11 int main(int argc, char* argv[])
12 {
13     UINT uId;
14     HANDLE h[2];
15
16     h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
17     h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
18
19
20     // 等待1秒后通知两个计数线程结束,关闭句柄
21     Sleep(1000);
22     g_bContinue = FALSE;
23     ::WaitForMultipleObjects(2, h, TRUE, INFINITE);
24     ::CloseHandle(h[0]);
25     ::CloseHandle(h[1]);
26
27     printf("g_nCount1 = %d \n", g_nCount1);
28     printf("g_nCount2 = %d \n", g_nCount2);
29
30     return 0;
31 }
32
33 UINT __stdcall ThreadFunc(LPVOID)
34 {
35     while(g_bContinue)
36     {
37         g_nCount1++;
38         g_nCount2++;
39     }
40     return 0;
41 }

2.CriticalSection

  PS:通过使用临界区对象来改写之前存在同步问题的程序。

 1 #include <stdio.h>
 2 #include <windows.h>
 3 #include <process.h>
 4
 5 BOOL g_bContinue = TRUE;
 6 int g_nCount1 = 0;
 7 int g_nCount2 = 0;
 8 CRITICAL_SECTION g_cs; // 对存在同步问题的代码段使用临界区对象
 9
10 UINT __stdcall ThreadFunc(LPVOID);
11
12 int main(int argc, char* argv[])
13 {
14     UINT uId;
15     HANDLE h[2];
16
17     // 初始化临界区对象
18     ::InitializeCriticalSection(&g_cs);
19
20     h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
21     h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
22
23     // 等待1秒后通知两个计数线程结束,关闭句柄
24     Sleep(1000);
25     g_bContinue = FALSE;
26     ::WaitForMultipleObjects(2, h, TRUE, INFINITE);
27     ::CloseHandle(h[0]);
28     ::CloseHandle(h[1]);
29
30     // 删除临界区对象
31     ::DeleteCriticalSection(&g_cs);
32
33     printf("g_nCount1 = %d \n", g_nCount1);
34     printf("g_nCount2 = %d \n", g_nCount2);
35
36     return 0;
37 }
38
39 UINT __stdcall ThreadFunc(LPVOID)
40 {
41     while(g_bContinue)
42     {
43         ::EnterCriticalSection(&g_cs);
44         g_nCount1++;
45         g_nCount2++;
46         ::LeaveCriticalSection(&g_cs);
47     }
48     return 0;
49 }

3.EventDemo

  PS:主线程通过将事件状态设置为“受信”来通知它的子线程开始工作。

 1 #include <stdio.h>
 2 #include <windows.h>
 3 #include <process.h>
 4
 5 HANDLE g_hEvent;
 6 UINT __stdcall ChildFunc(LPVOID);
 7
 8 int main(int argc, char* argv[])
 9 {
10     HANDLE hChildThread;
11     UINT uId;
12
13     // 创建一个自动重置的(auto-reset events),未受信的(nonsignaled)事件内核对象
14     g_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
15
16     hChildThread = (HANDLE)::_beginthreadex(NULL, 0, ChildFunc, NULL, 0, &uId);
17
18     // 通知子线程开始工作
19     printf("Please input a char to tell the Child Thread to work: \n");
20     getchar();
21     ::SetEvent(g_hEvent);
22
23     // 等待子线程完成工作,释放资源
24     ::WaitForSingleObject(hChildThread, INFINITE);
25     printf("All the work has been finished. \n");
26     ::CloseHandle(hChildThread);
27     ::CloseHandle(g_hEvent);
28     return 0;
29 }
30
31 UINT __stdcall ChildFunc(LPVOID)
32 {
33     ::WaitForSingleObject(g_hEvent, INFINITE);
34     printf("  Child thread is working...... \n");
35     ::Sleep(5*1000); // 暂停5秒,模拟真正的工作
36     return 0;
37 }

4.UseTLS

  PS:通过TLS将每个线程的创建时间与线程关联起来,从而可以在线程终止时得到线程的生命周期。

 1 #include <stdio.h>
 2 #include <windows.h>
 3 #include <process.h>
 4
 5 // 利用TLS记录线程的运行时间
 6
 7 DWORD g_tlsUsedTime;
 8 void InitStartTime();
 9 DWORD GetUsedTime();
10
11
12 UINT __stdcall ThreadFunc(LPVOID)
13 {
14     int i;
15
16     // 初始化开始时间
17     InitStartTime();
18
19     // 模拟长时间工作
20     i = 10000*10000;
21     while(i--) { }
22
23     // 打印出本线程运行的时间
24     printf(" This thread is coming to end. Thread ID: %-5d, Used Time: %d \n",
25                         ::GetCurrentThreadId(), GetUsedTime());
26     return 0;
27 }
28
29 int main(int argc, char* argv[])
30 {
31     UINT uId;
32     int i;
33     HANDLE h[10];
34
35     // 通过在进程位数组中申请一个索引,初始化线程运行时间记录系统
36     g_tlsUsedTime = ::TlsAlloc();
37
38     // 令十个线程同时运行,并等待它们各自的输出结果
39     for(i=0; i<10; i++)
40     {
41         h[i] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
42     }
43     for(i=0; i<10; i++)
44     {
45         ::WaitForSingleObject(h[i], INFINITE);
46         ::CloseHandle(h[i]);
47     }
48
49     // 通过释放线程局部存储索引,释放时间记录系统占用的资源
50     ::TlsFree(g_tlsUsedTime);
51     return 0;
52 }
53
54 // 初始化线程的开始时间
55 void InitStartTime()
56 {
57     // 获得当前时间,将线程的创建时间与线程对象相关联
58     DWORD dwStart = ::GetTickCount();
59     ::TlsSetValue(g_tlsUsedTime, (LPVOID)dwStart);
60 }
61
62 // 取得一个线程已经运行的时间
63 DWORD GetUsedTime()
64 {
65     // 获得当前时间,返回当前时间和线程创建时间的差值
66     DWORD dwElapsed = ::GetTickCount();
67     dwElapsed = dwElapsed - (DWORD)::TlsGetValue(g_tlsUsedTime);
68     return dwElapsed;
69 }
时间: 2024-11-04 19:49:13

第三章--Win32程序的执行单元(部分概念及代码讲解)(中-线程同步的相关文章

第二章--Win32程序运行原理 (部分概念及代码讲解)

学习<Windows程序设计>记录 概念贴士: 1. 每个进程都有赋予它自己的私有地址空间.当进程内的线程运行时,该线程仅仅能够访问属于它的进程的内存,而属于其他进程的内存被屏蔽了起来,不能被该线程访问. PS:进程A在其地址空间的0x12345678地址处能够有一个数据结构,而进程B能够在其地址空间的0x12345678处存储一个完全不同的数据.彼此不能访问. 2. 在大多数系统中,Windows将地址空间的一半(4GB的前一半,0x00000000-0x7FFFFFFF)留给进程作为私有存

第三章—Windows程序

这一章我都不知道该如何写了,呵呵~~ 毕竟,Win32是一个非常深奥的系统,目前还容不得我这种 小辈在这儿说三道四,不过,我既然是要写给那些入门阶段的朋友们看的,又不是写给那些搞程序设计老鸟看的,所以,我也犯不着怕被人背后指着骂 本章的名字就叫<Windows程序>而不是<Windows程序设计>所以,我只是讲一些关于Windows程序运作的原理: Windows 为什么叫Windows,相信所有用过的朋友都可以明白,那桌面上一个一个的窗口,就是它名字的由来.也就是这一个又一个窗口

Python编程入门-第三章 编写程序 -学习笔记

第三章 编写程序 1.编辑源程序.运行程序 可通过IDLE中File>New File新建一个文本以编辑源程序,编辑完成可通过Run>Run Module(或者F5快捷键)来运行程序.Python源文件都以.py格式存储. 2.从命令行运行程序 除了上述利用IDLE的集成功能运行程序的方式外,当然也可以通过命令行运行程序,命令格式为:python ‘源文件名称.py’. 3.编译源代码 当运行py格式文件时,Python会自动创建相应的.pyc文件,该文件包含编译后的代码即目标代码,目标代码基

《算法》第三章部分程序 part 5

? 书中第三章部分程序,加上自己补充的代码,包含公共符号表.集合类型 ● 公共符号表,用于普通查找表的基本类 1 package package01; 2 3 import java.util.NoSuchElementException; 4 import java.util.TreeMap; 5 import edu.princeton.cs.algs4.StdIn; 6 import edu.princeton.cs.algs4.StdOut; 7 8 public class class

《算法》第三章部分程序 part 6

? 书中第三章部分程序,加上自己补充的代码,包含双向索引表.文建索引.稀疏向量类型 ● 双向索引表 1 package package01; 2 3 import edu.princeton.cs.algs4.ST; 4 import edu.princeton.cs.algs4.Queue; 5 import edu.princeton.cs.algs4.In; 6 import edu.princeton.cs.algs4.StdIn; 7 import edu.princeton.cs.a

《算法》第三章部分程序 part 4

? 书中第三章部分程序,加上自己补充的代码,包括散列表.线性探查表 ● 散列表 1 package package01; 2 3 import edu.princeton.cs.algs4.Queue; 4 import edu.princeton.cs.algs4.SequentialSearchST; 5 import edu.princeton.cs.algs4.StdIn; 6 import edu.princeton.cs.algs4.StdOut; 7 8 public class

【知识强化】第三章 存储系统 3.1 存储器的基本概念

前面我们已经把第二章数据的表示和计算都已经讲完了,那么从这一章开始我们将进入存储系统的学习.那么本章要讲什么呢? 本章是历年考查的一个重点.特别呢是我用红框框框出来的这部分,是考试当中特别容易考到的重中之重.那么我们来看一下我们这一章要学什么?最重要的呢是要学习存储器是和如何和CPU协同工作的,那么我们将要介绍存储器的一个简单的模型以及它地址信号如何找到我们的存储单元的,也就是寻址的一个概念.接下来我们将要讲解主存和CPU的连接.那么我们将要解决两个问题,一个问题呢就是我们的主存和我们的CPU它

【好程序员训练营】-Java多线程与并发(二)之线程同步

android培训--我的java笔记,期待与您交流! 线程同步 1 . 多线程共享数据 在多线程操作中, 多个线程有可能同时处理同一个资源, 这就是多线程中的共享数据. 举个不太恰当的例子简单理解一下,图中是小新家的厕所茅坑,但是家里只有一个那这就是一个"资源",那么家里的所有人都共享这同一个"资源",也就是所谓的多线程共享数据 可以明显的看出多线程共享数据带来的问题,就是会造成数据的不确定性!就好比小新正在上着厕所,此时小新爸爸来了, 此时相当于一个资源两个人在

第三章 JVM内存回收区域+对象存活的判断+引用类型+垃圾回收线程

注意:本文主要参考自<深入理解Java虚拟机(第二版)> 说明:查看本文之前,推荐先知道JVM内存结构,见<第一章 JVM内存结构> 1.内存回收的区域 堆:这是GC的主要区域 方法区:回收两样东西 无用的类 废弃的常量 栈和PC寄存器是线程私有区域,不发生GC 2.怎样判断对象是否存活 垃圾回收:回收掉死亡对象所占的内存.判断对象是否死亡,有两种方式: 引用计数法 原理:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值+1:引用失效时,计数器值-1 实际中不用,不用的两