注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

jasonyang9的博客

随便写写

 
 
 

日志

 
 

(怀旧系列)VC程序设计(孙鑫老师)听课笔记:15 多线程和聊天室程序  

2013-02-19 09:08:27|  分类: programming |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
(怀旧系列)VC程序设计(孙鑫老师)听课笔记:15 多线程和聊天室程序 - jasonyang9 - jasonyang9的博客

==================
多线程和聊天室程序
==================

程序、进程和线程的概念
* 程序是计算机指令的集合,以文件的形式存储在磁盘(存储介质)上
* 进程通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址空间中的一次执行活动(一个程序可以同时对应多个进程)
* 进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,不能作为独立运行的单位,因此,它不占用系统的运行资源(进程是运行中的程序)
* 进程由两个部分组成:
 1、操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方(内核对象是操作系统内部分配的内存块,是一种数据结构,成员负责维护该对象的各种信息,只能通过系统提供的一些函数对内核对象进行操作)
 2、地址空间。包含所有可执行模块或DLL模块的代码和数据,还包含动态内存分配的空间,如线程堆栈和堆分配空间
* 进程是不活泼的,进程从来不执行任何东西,是线程的容器,如果要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码
* 单个进程可能包含若干个线程,这些线程都“同时”执行进程地址空间中的代码
* 每个进程至少拥有一个线程,来执行进程的地址空间中的代码。当创建一个进程时,操作系统会自动创建这个进程的第一个线程,称为主线程(执行main或WinMain函数的线程)。此后,该线程可以创建其他的线程
* 系统赋予每个进程独立的虚拟地址空间。对于32位进程来说,这个地址空间是4GB
* 每个进程有它自己的私有地址空间。进程A可能有一个存放在它的地址空间中的数据结构,地址是0x12345678,而进程B则有一个完全不同的数据结构存放在它的地址空间中,地址是0x12345678。当进程A中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程A的数据结构。当进程B中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程B的数据结构。进程A中运行的线程不能访问进程B的地址空间中的数据结构,反之亦然。
* 4GB是虚拟的地址空间,只是内存地址的一个范围。在能够成功地访问数据而不会出现非法访问之前,必须赋予物理存储器,或者将物理存储器映射到各个部分的地址空间
* 4GB虚拟地址空间中,2GB是内核方式分区,供内核代码、设备驱动程序、设备I/O高速缓冲、非页面内存池的分配和进程页面表等使用,而用户方式分区使用的地址空间约为2GB,这个分区是进程的私有地址空间所在的地方。一个进程不能读取、写入或以任何方式访问驻留在该分区中的另一个进程的数据。对所有应用程序来说,该分区是维护进程的大部分数据的地方
* 线程由两个部分组成:
 1、线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方
 2、线程堆栈,用于维护线程在执行代码时需要的所有参数和局部变量
* 当创建线程时,系统创建一个线程内核对象,该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象看作是由关于线程的统计信息组成的一个小型数据结构
* 线程总是在某个进程环境中创建的,系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境和创建线程的环境相同,因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。这使得单个进程中的多个线程之间能够非常容易地互相通信。
* 线程只有一个内核对象和一个堆栈,保留的记录很少,因此所需要的内存也很少
* 因为线程需要的资源开销比进程少,因此,在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程
* 操作系统为每一个运行线程安排一定的CPU时间(时间片)。系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行。因时间片相当短,因此,给用户的感觉就好像几个线程是同时运行一样(单核多任务)
* 如果计算机拥有多个CPU(或多个处理核心),线程就能真正意义上同时运行了(多核多任务)

编写多线程程序,Win32 Console Application,MultiThread。

 

 #include <windows.h>
 #include <iostream.h>

 DWORD WINAPI fun1Proc(           // 声明线程入口函数fun1Proc
  LPVOID lpParameter // thread data
 );

 void main()            // main函数是主线程的入口函数
 {
  HANDLE hThread1           // 定义线程的句柄
  hThread1 = CreateThread(NULL,         // 创建线程,第一个参数NULL表示使用缺省的安全属性
    0,          // 默认的堆栈大小(和调用线程的一样)
    fun1Proc,         // 线程入口函数的地址(即函数名称)
    NULL,          // 传递给线程的参数
    0,          // 创建的flags,设为0表示一旦创建立即运行
    NULL);          // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
  CloseHandle(hThread1);          // 关闭线程的句柄(关闭句柄,并不是终止这个线程,而是表示在这里不会再引用这个线程了,其次,关闭句柄后,系统会递减新线程的内核对象的使用计数,当线程运行结束后,如果该内核对象的使用计数为0,系统就会释放资源,否则只有等到进程终止后,系统才能释放资源)

  cout << "main thread is running" << endl;       // 输出主线程正在运行的文字

  Sleep(10);           // 暂停主线程的执行(给子线程执行的机会,否则当主线程退出后,整个进程退出,子线程也就没有执行的机会了)
 }

 DWORD WINAPI fun1Proc(           // fun1Proc函数的实现
  LPVOID lpParameter // thread data
 )
 {
  cout << "thread1 is running" << endl;        // 输出线程1正在运行的文字
  return 0;
 }


输出:

main thread is running
thread1 is running


修改程序,观察时间片。

 

#include <windows.h>
 #include <iostream.h>

 DWORD WINAPI fun1Proc(           // 声明线程入口函数fun1Proc
  LPVOID lpParameter // thread data
 );

 int index = 0;            // 定义全局变量

 void main()            // main函数是主线程的入口函数
 {
  HANDLE hThread1           // 定义线程的句柄
  hThread1 = CreateThread(NULL,         // 创建线程,第一个参数NULL表示使用缺省的安全属性
    0,          // 默认的堆栈大小(和调用线程的一样)
    fun1Proc,         // 线程入口函数的地址(即函数名称)
    NULL,          // 传递给线程的参数
    0,          // 创建的flags,设为0表示一旦创建立即运行
    NULL);          // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
  CloseHandle(hThread1);          // 关闭线程的句柄(关闭句柄,并不是终止这个线程,而是表示在这里不会再引用这个线程了,其次,关闭句柄后,系统会递减新线程的内核对象的使用计数,当线程运行结束后,如果该内核对象的使用计数为0,系统就会释放资源,否则只有等到进程终止后,系统才能释放资源)

  while (index++ < 1000)          // 循环
   cout << "main thread is running" << endl;      // 输出主线程正在运行的文字
 }

 DWORD WINAPI fun1Proc(           // fun1Proc函数的实现
  LPVOID lpParameter // thread data
 )
 {
  while (index++ < 1000)          // 循环
   cout << "thread1 is running" << endl;       // 输出线程1正在运行的文字
  return 0;
 }


输出:

main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
...
thread1 is running    <------ 主线程时间片用完,切换到子线程,子线程在自己时间片中的输出
thread1 is running    <------ 子线程在自己时间片中的输出
thread1 is running    <------ 子线程在自己时间片中的输出
thread1 is running    <------ 子线程在自己时间片中的输出
thread1 is running    <------ 子线程在自己时间片中的输出
thread1 is running    <------ 子线程在自己时间片中的输出
thread1 is running    <------ 子线程在自己时间片中的输出
thread1 is running    <------ 子线程在自己时间片中的输出
thread1 is running    <------ 子线程在自己时间片中的输出
thread1 is running    <------ 子线程在自己时间片中的输出
...
main thread is running    <------ 子线程时间片用完,切换到主线程,主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
main thread is running    <------ 主线程在自己时间片中的输出
...


可以看到,主线程和子线程在自己的时间片中交替运行(单核多线程)。
如果是多核多线程,主线程和子线程应该能真正做到并发运行,多任务?

修改程序,模拟火车站售票系统。

 

 #include <windows.h>
 #include <iostream.h>

 DWORD WINAPI fun1Proc(           // 声明线程入口函数fun1Proc
  LPVOID lpParameter // thread data
 );

 DWORD WINAPI fun2Proc(           // 声明线程入口函数fun2Proc
  LPVOID lpParameter // thread data
 );

 int tickets = 100;           // 剩余的火车票数量

 void main()            // main函数是主线程的入口函数
 {
  HANDLE hThread1, hThread2;         // 定义线程的句柄
  hThread1 = CreateThread(NULL,         // 创建线程,第一个参数NULL表示使用缺省的安全属性
    0,          // 默认的堆栈大小(和调用线程的一样)
    fun1Proc,         // 线程入口函数的地址(即函数名称)
    NULL,          // 传递给线程的参数
    0,          // 创建的flags,设为0表示一旦创建立即运行
    NULL);          // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
  hThread2 = CreateThread(NULL,         // 创建线程,第一个参数NULL表示使用缺省的安全属性
    0,          // 默认的堆栈大小(和调用线程的一样)
    fun2Proc,         // 线程入口函数的地址(即函数名称)
    NULL,          // 传递给线程的参数
    0,          // 创建的flags,设为0表示一旦创建立即运行
    NULL);          // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
  CloseHandle(hThread1);          // 关闭线程的句柄(关闭句柄,并不是终止这个线程,而是表示在这里不会再引用这个线程了,其次,关闭句柄后,系统会递减新线程的内核对象的使用计数,当线程运行结束后,如果该内核对象的使用计数为0,系统就会释放资源,否则只有等到进程终止后,系统才能释放资源)
  CloseHandle(hThread2);

  Sleep(4000);           // 主线程放弃执行权,让2个子线程有机会运行
 }

 DWORD WINAPI fun1Proc(           // fun1Proc函数的实现
  LPVOID lpParameter // thread data
 )
 {
  while (TRUE)           // 循环
  {
   if (tickets > 0)         // 判断如果还有剩余的火车票
    cout << "thread1 sell ticket : " << tickets-- << endl;    // 输出,表示卖出了一张火车票(同时递减tickets)
   else
    break;          // 卖完了就终止线程
  }

  return 0;
 }

 DWORD WINAPI fun2Proc(           // fun2Proc函数的实现
  LPVOID lpParameter // thread data
 )
 {
  while (TRUE)           // 循环
  {
   if (tickets > 0)         // 判断如果还有剩余的火车票
    cout << "thread2 sell ticket : " << tickets-- << endl;    // 输出,表示卖出了一张火车票(同时递减tickets)
   else
    break;          // 卖完了就终止线程
  }

  return 0;
 }


输出:

thread1 sell ticket : 100
thread1 sell ticket : 99
...
thread1 sell ticket : 67    <------ 视频中此行出现了2次!?
thread2 sell ticket : 66
thread2 sell ticket : 65
...
thread2 sell ticket : 36
thread1 sell ticket : 35
...
...
thread1 sell ticket : 1


程序的运行貌似完美,但实际却存在隐患。
问题在于,tickets等于1时,如果线程1在执行if (tickets > 0)后、执行cout<<"thread1 sell ticket : " << tickets-- << endl;前它的时间片结束了,切换到线程2执行(注意:此时tickets还没有递减为0),线程2打印出"thread2 sell ticket : 1"后,tickets被递减为0,于是线程2正常退出,线程1再次获得执行的机会,但此时tickets已经为0,且继续执行的语句是cout<<"thread1 sell ticket : " << tickets-- << endl;,于是就会打印出"thread1 sell ticket : 0"这个违反逻辑的错误内容。
虽然这种错误出现的概率很低,但恰恰是低概率导致了重现的困难,并增加排错的难度。
其实,最根本的原因是2个线程在访问同一个变量,而在其中1个还没有处理完相关的代码后,由于时间片的到期,切换到的另一个线程修改了变量的值。为此,应该有种办法,规定在1个线程访问该变量期间,让其他线程无法访问该变量(互斥)。
此外,在if (tickets > 0)后增加Sleep(1)可以极大的提高错误出现的概率。

 

#include <windows.h>
 #include <iostream.h>

 DWORD WINAPI fun1Proc(           // 声明线程入口函数fun1Proc
  LPVOID lpParameter // thread data
 );

 DWORD WINAPI fun2Proc(           // 声明线程入口函数fun2Proc
  LPVOID lpParameter // thread data
 );

 int tickets = 100;           // 剩余的火车票数量

 void main()            // main函数是主线程的入口函数
 {
  HANDLE hThread1, hThread2;         // 定义线程的句柄
  hThread1 = CreateThread(NULL,         // 创建线程,第一个参数NULL表示使用缺省的安全属性
    0,          // 默认的堆栈大小(和调用线程的一样)
    fun1Proc,         // 线程入口函数的地址(即函数名称)
    NULL,          // 传递给线程的参数
    0,          // 创建的flags,设为0表示一旦创建立即运行
    NULL);          // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
  hThread2 = CreateThread(NULL,         // 创建线程,第一个参数NULL表示使用缺省的安全属性
    0,          // 默认的堆栈大小(和调用线程的一样)
    fun2Proc,         // 线程入口函数的地址(即函数名称)
    NULL,          // 传递给线程的参数
    0,          // 创建的flags,设为0表示一旦创建立即运行
    NULL);          // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
  CloseHandle(hThread1);          // 关闭线程的句柄(关闭句柄,并不是终止这个线程,而是表示在这里不会再引用这个线程了,其次,关闭句柄后,系统会递减新线程的内核对象的使用计数,当线程运行结束后,如果该内核对象的使用计数为0,系统就会释放资源,否则只有等到进程终止后,系统才能释放资源)
  CloseHandle(hThread2);

  Sleep(4000);           // 主线程放弃执行权,让2个子线程有机会运行
 }

 DWORD WINAPI fun1Proc(           // fun1Proc函数的实现
  LPVOID lpParameter // thread data
 )
 {
  while (TRUE)           // 循环
  {
   if (tickets > 0)         // 判断如果还有剩余的火车票
   {
    Sleep(1);         // 放弃执行权1毫秒,能极大增加被其他线程修改tickets变量值的可能性
    cout << "thread1 sell ticket : " << tickets-- << endl;    // 输出,表示卖出了一张火车票(同时递减tickets)
   }
   else
    break;          // 卖完了就终止线程
  }

  return 0;
 }

 DWORD WINAPI fun2Proc(           // fun2Proc函数的实现
  LPVOID lpParameter // thread data
 )
 {
  while (TRUE)           // 循环
  {
   if (tickets > 0)         // 判断如果还有剩余的火车票
   {
    Sleep(1);         // 同fun1Proc
    cout << "thread2 sell ticket : " << tickets-- << endl;    // 输出,表示卖出了一张火车票(同时递减tickets)
   }
   else
    break;          // 卖完了就终止线程
  }

  return 0;
 }


关于互斥对象
* 互斥对象(mutex)属于内核对象,能够确保线程拥有对单个资源的互斥访问权
* 互斥对象包含一个使用数量、一个线程ID和一个计数器
* 线程ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数

修改代码,利用互斥对象保护关键代码。

 

#include <windows.h>
 #include <iostream.h>

 DWORD WINAPI fun1Proc(           // 声明线程入口函数fun1Proc
  LPVOID lpParameter // thread data
 );

 DWORD WINAPI fun2Proc(           // 声明线程入口函数fun2Proc
  LPVOID lpParameter // thread data
 );

 int tickets = 100;           // 剩余的火车票数量
 HANDLE hMutex;            // 定义互斥对象句柄

 void main()            // main函数是主线程的入口函数
 {
  HANDLE hThread1, hThread2;         // 定义线程的句柄
  hThread1 = CreateThread(NULL,         // 创建线程,第一个参数NULL表示使用缺省的安全属性
    0,          // 默认的堆栈大小(和调用线程的一样)
    fun1Proc,         // 线程入口函数的地址(即函数名称)
    NULL,          // 传递给线程的参数
    0,          // 创建的flags,设为0表示一旦创建立即运行
    NULL);          // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
  hThread2 = CreateThread(NULL,         // 创建线程,第一个参数NULL表示使用缺省的安全属性
    0,          // 默认的堆栈大小(和调用线程的一样)
    fun2Proc,         // 线程入口函数的地址(即函数名称)
    NULL,          // 传递给线程的参数
    0,          // 创建的flags,设为0表示一旦创建立即运行
    NULL);          // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
  CloseHandle(hThread1);          // 关闭线程的句柄(关闭句柄,并不是终止这个线程,而是表示在这里不会再引用这个线程了,其次,关闭句柄后,系统会递减新线程的内核对象的使用计数,当线程运行结束后,如果该内核对象的使用计数为0,系统就会释放资源,否则只有等到进程终止后,系统才能释放资源)
  CloseHandle(hThread2);

  hMutex = CreateMutex(NULL,         // 创建互斥对象,第一个参数NULL表示没有安全性
    FALSE,          // 第二个参数FALSE表示当前线程不是互斥对象的初始拥有者(操作系统会将此互斥对象初始化为有信号状态)
    NULL);          // 第三个参数NULL表示互斥对象没有名字(匿名)

  Sleep(4000);           // 主线程放弃执行权,让2个子线程有机会运行
 }

 DWORD WINAPI fun1Proc(           // fun1Proc函数的实现
  LPVOID lpParameter // thread data
 )
 {
  while (TRUE)           // 循环
  {
   WaitForSingleObject(hMutex, INFINITE);       // WaitForSingleObject等待某个对象(参数1)的状态变为有信号或超时(参数2)才返回,否则等待(放弃执行权),INFINITE表示永远等待,直到对象变成有信号状态
   if (tickets > 0)         // 判断如果还有剩余的火车票
   {
    Sleep(1);         // 放弃执行权1毫秒,能极大增加被其他线程修改tickets变量值的可能性
    cout << "thread1 sell ticket : " << tickets-- << endl;    // 输出,表示卖出了一张火车票(同时递减tickets)
   }
   else
    break;          // 卖完了就终止线程
   ReleaseMutex(hMutex);         // 释放互斥对象(将互斥对象的线程ID设为0、互斥对象为有信号状态)
  }

  return 0;
 }

 DWORD WINAPI fun2Proc(           // fun2Proc函数的实现
  LPVOID lpParameter // thread data
 )
 {
  while (TRUE)           // 循环
  {
   WaitForSingleObject(hMutex, INFINITE);       // 同fun1Proc
   if (tickets > 0)         // 判断如果还有剩余的火车票
   {
    Sleep(1);         // 同fun1Proc
    cout << "thread2 sell ticket : " << tickets-- << endl;    // 输出,表示卖出了一张火车票(同时递减tickets)
   }
   else
    break;          // 卖完了就终止线程
   ReleaseMutex(hMutex);         // 同fun1Proc
  }

  return 0;
 }


由于在创建互斥对象时给出的第二个参数FALSE表示该互斥对象没有初始的拥有者,操作系统会将该互斥对象设为有信号状态。线程1执行到WaitForSingleObject时,会判断到互斥对象为有信号状态,将互斥对象的线程ID(拥有者)会设置为线程1,同时将互斥对象设置为无信号状态。虽然接下来的Sleep(1)会暂停线程1的执行,但在线程2中,当执行到WaitForSingleObject时,互斥对象已经是无信号状态,且被线程1拥有,也就无限期等待。线程1经过Sleep(1)后恢复执行,运行到ReleaseMutex时,释放了互斥对象的所有权,互斥对象也变为有信号状态。如果此时线程1仍具有时间片,那么它可继续通过WaitForSingleObject来拥有互斥对象的所有权,反之,如果在释放了互斥对象后线程1的时间片用完了,线程2就会取得互斥对象的所有权,并在它拥有的时间片期间不让线程1夺得互斥对象,这样重复切换,也就保护了变量不被其他线程修改。
小结:可以将互斥对象理解为钥匙,谁拥有这把钥匙就能进入房间,而其他人会被挡在房间外,直到他能获得钥匙。用WaitForSingleObject和ReleaseMutex将关键代码保护起来,不被其他线程干扰。总之,互斥对象在没有拥有者时为有信号状态,在有拥有者时为无信号状态。在有信号状态时,线程调用WaitForSingleObject能够返回,互斥对象成无信号状态,拥有者变为本线程;拥有者线程ReleaseMutex后,互斥对象成有信号状态,无拥有者;在互斥对象无信号状态时,其他线程调用WaitForSingleObject不返回(除非超时)。

注意1:如果将WaitForSingleObject放在while循环前,ReleaseMutex放在while循环后,则线程2没有运行机会。(自己分析一下)

 

 #include <windows.h>
 #include <iostream.h>

 DWORD WINAPI fun1Proc(
  LPVOID lpParameter // thread data
 );

 DWORD WINAPI fun2Proc(
  LPVOID lpParameter // thread data
 );

 int tickets = 100;
 HANDLE hMutex;

 void main()
 {
  HANDLE hThread1, hThread2;
  hThread1 = CreateThread(NULL,
    0,
    fun1Proc,
    NULL,
    0,
    NULL);
  hThread2 = CreateThread(NULL,
    0,
    fun2Proc,
    NULL,
    0,
    NULL);
  CloseHandle(hThread1);
  CloseHandle(hThread2);

  hMutex = CreateMutex(NULL,
    FALSE,
    NULL);

  Sleep(4000);
 }

 DWORD WINAPI fun1Proc(
  LPVOID lpParameter // thread data
 )
 {
  WaitForSingleObject(hMutex, INFINITE);        // 将WaitForSingleObject放到while循环前
  while (TRUE)
  {
   if (tickets > 0)
   {
    Sleep(1);
    cout << "thread1 sell ticket : " << tickets-- << endl;
   }
   else
    break;
  }
  ReleaseMutex(hMutex);          // ReleaseMutex放在while循环后

  return 0;
 }

 DWORD WINAPI fun2Proc(
  LPVOID lpParameter // thread data
 )
 {
  WaitForSingleObject(hMutex, INFINITE);        // 将WaitForSingleObject放到while循环前
  while (TRUE)
  {
   if (tickets > 0)
   {
    Sleep(1);
    cout << "thread2 sell ticket : " << tickets-- << endl;
   }
   else
    break;
  }
  ReleaseMutex(hMutex);          // ReleaseMutex放在while循环后

  return 0;
 }


注意2:如果在main中CreateMutex时,将第二个参数设为TRUE,则由于互斥对象的拥有者为主线程,则线程1和线程2都没有运行机会。即使耍个小聪明,在线程1和线程2中调用ReleaseMutex(hMutex)来尝试释放互斥对象也会失败,因为只有互斥对象的拥有者(ReleaseMutex函数通过互斥对象的线程ID和调用线程ID比较判断)线程才能成功释放它。所以,只能通过互斥对象的拥有者(主线程)自己释放。

 

 #include <windows.h>
 #include <iostream.h>

 DWORD WINAPI fun1Proc(
  LPVOID lpParameter // thread data
 );

 DWORD WINAPI fun2Proc(
  LPVOID lpParameter // thread data
 );

 int tickets = 100;
 HANDLE hMutex;

 void main()
 {
  HANDLE hThread1, hThread2;
  hThread1 = CreateThread(NULL,
    0,
    fun1Proc,
    NULL,
    0,
    NULL);
  hThread2 = CreateThread(NULL,
    0,
    fun2Proc,
    NULL,
    0,
    NULL);
  CloseHandle(hThread1);
  CloseHandle(hThread2);

  hMutex = CreateMutex(NULL,
    TRUE,          // 将第二个参数改为TRUE
    NULL);
  ReleaseMutex(hMutex);          // 除非在主线程中调用ReleaseMutex,否则是无法解开互斥对象的(解铃还需系铃人)

  Sleep(4000);
 }

 DWORD WINAPI fun1Proc(
  LPVOID lpParameter // thread data
 )
 {
  while (TRUE)
  {
   ReleaseMutex(hMutex);         // 自己加上ReleaseMutex不能解开互斥对象
   WaitForSingleObject(hMutex, INFINITE);
   if (tickets > 0)
   {
    Sleep(1);
    cout << "thread1 sell ticket : " << tickets-- << endl;
   }
   else
    break;
   ReleaseMutex(hMutex);
  }

  return 0;
 }

 DWORD WINAPI fun2Proc(
  LPVOID lpParameter // thread data
 )
 {
  while (TRUE)
  {
   ReleaseMutex(hMutex);         // 自己加上ReleaseMutex不能解开互斥对象
   WaitForSingleObject(hMutex, INFINITE);
   if (tickets > 0)
   {
    Sleep(1);
    cout << "thread2 sell ticket : " << tickets-- << endl;
   }
   else
    break;
   ReleaseMutex(hMutex);
  }

  return 0;
 }


注意3:接上面,如果在主线程创建互斥对象后调用WaitForSingleObject,再调用ReleaseMutex,会出现什么情况?

 

 #include <windows.h>
 #include <iostream.h>

 DWORD WINAPI fun1Proc(
  LPVOID lpParameter // thread data
 );

 DWORD WINAPI fun2Proc(
  LPVOID lpParameter // thread data
 );

 int tickets = 100;
 HANDLE hMutex;

 void main()
 {
  HANDLE hThread1, hThread2;
  hThread1 = CreateThread(NULL,
    0,
    fun1Proc,
    NULL,
    0,
    NULL);
  hThread2 = CreateThread(NULL,
    0,
    fun2Proc,
    NULL,
    0,
    NULL);
  CloseHandle(hThread1);
  CloseHandle(hThread2);

  hMutex = CreateMutex(NULL,
    TRUE,          // 第二个参数为TRUE
    NULL);
  WaitForSingleObject(hMutex, INFINITE);        // 主线程自己调用WaitForSingleObject
  ReleaseMutex(hMutex);

  Sleep(4000);
 }

 DWORD WINAPI fun1Proc(
  LPVOID lpParameter // thread data
 )
 {
  while (TRUE)
  {
   WaitForSingleObject(hMutex, INFINITE);
   if (tickets > 0)
   {
    Sleep(1);
    cout << "thread1 sell ticket : " << tickets-- << endl;
   }
   else
    break;
   ReleaseMutex(hMutex);
  }

  return 0;
 }

 DWORD WINAPI fun2Proc(
  LPVOID lpParameter // thread data
 )
 {
  while (TRUE)
  {
   WaitForSingleObject(hMutex, INFINITE);
   if (tickets > 0)
   {
    Sleep(1);
    cout << "thread2 sell ticket : " << tickets-- << endl;
   }
   else
    break;
   ReleaseMutex(hMutex);
  }

  return 0;
 }


分析和解决:

 

  hMutex = CreateMutex(NULL,
    TRUE,          // 第二个参数为TRUE,表示此互斥对象的拥有者是本线程,那么所建立的互斥对象就是没有信号的状态,且拥有者的拥有次数计数器从0递增为1
    NULL);
  WaitForSingleObject(hMutex, INFINITE);        // 主线程自己调用WaitForSingleObject,一般来说,如果互斥对象处于无信号状态,WaitForSingleObject是不会返回的,但由于WaitForSingleObject还会判断该互斥对象的拥有者线程和调用函数的线程是否一致,如果一致,则函数还是能够返回,同时将互斥对象中的拥有者的拥有次数计数器递增(从1递增为2)
  ReleaseMutex(hMutex);          // ReleaseMutex函数执行将互斥对象中的拥有者的拥有次数计数器递减(从2递减为1),由于还没有递减为0,无法将其设为有信号状态(也就是继续被此线程拥有)
  ReleaseMutex(hMutex);          // 再次调用ReleaseMutex,这次将互斥对象中的拥有者的拥有次数计数器递减(从1递减为0),并成功将其设置为有信号状态


MSDN文档中的说明:While a thread has ownership of a mutex, it can specify the same mutex in additional wait-function calls without blocking its execution. This prevents a thread from deadlocking itself while waiting for a mutex that it already owns. However, to release its ownership, the thread must call ReleaseMutex once for each time that the mutex satisfied a wait.

注意4:如果某个线程在用WaitForSingleObject后没有ReleaseMutex,线程终止后其他线程有机会获得互斥对象吗?

 

 #include <windows.h>
 #include <iostream.h>

 DWORD WINAPI fun1Proc(
  LPVOID lpParameter // thread data
 );

 DWORD WINAPI fun2Proc(
  LPVOID lpParameter // thread data
 );

 int tickets = 100;
 HANDLE hMutex;

 void main()
 {
  HANDLE hThread1, hThread2;
  hThread1 = CreateThread(NULL,
    0,
    fun1Proc,
    NULL,
    0,
    NULL);
  hThread2 = CreateThread(NULL,
    0,
    fun2Proc,
    NULL,
    0,
    NULL);
  CloseHandle(hThread1);
  CloseHandle(hThread2);

  hMutex = CreateMutex(NULL,
    FALSE,
    NULL);

  Sleep(4000);
 }

 DWORD WINAPI fun1Proc(
  LPVOID lpParameter // thread data
 )
 {
  WaitForSingleObject(hMutex, INFINITE);
  cout << "thread1 is running" << endl;        // 获得互斥对象后不释放
  return 0;
 }

 DWORD WINAPI fun2Proc(
  LPVOID lpParameter // thread data
 )
 {
  WaitForSingleObject(hMutex, INFINITE);
  cout << "thread2 is running" << endl;        // 获得互斥对象后不释放
  return 0;
 }


运行结果是:

 

thread1 is running
thread2 is running


因为,当线程终止时,系统发现到有未释放的互斥对象,就会自动将其所拥有的互斥对象的所有者线程ID设为0,计数器也设为0,这样,其他的线程就能够成功获得该互斥对象。
进一步,可以通过WaitForSingleObject的返回值来判断是正常释放了对象还是被系统自动释放。如果WaitForSingleObject返回WAIT_ABANDONED表示互斥对象是由系统自动释放的。
出现WAIT_ABANDONED后要警惕,因为系统自动释放的原因可能是线程真的忘记调用ReleaseMutex,还有可能是因为线程异常终止,来不及调用ReleaseMutex,那么对于在已终止线程中WaitForSingleObject所保护的代码,其访问的资源的当前状态是不清楚的(是否正常操作完成),那么在之后获得互斥对象的线程中对其再次操作的结果也是未知的,必须做一些处理。

限制只能运行一个进程(利用命名互斥对象实现)
--------------------------------------------

 

 #include <windows.h>
 #include <iostream.h>

 HANDLE hMutex;

 void main()
 {
  hMutex = CreateMutex(NULL, FALSE, "tickets");       // 创建命名互斥对象

  if (hMutex)           // 如果创建成功
  {
   if (ERROR_ALREADY_EXISTS == GetLastError())      // 判断GetLastError是否是ERROR_ALREADY_EXIST,如果是则表明已有一个进程在运行(互斥对象的名字重复)
   {
    cout << "only one instance can run!" << endl;
    return;
   }
  }
  Sleep(4000);
 }


多线程聊天程序
--------------
新建一个基于对话框的工程Chat,删除对话框上的按钮等控件,增加组框(“接收数据”),编辑框(IDC_EDIT_RECV,属性中勾选“Multiline”),组框(“发送数据”),IP地址控件(IDC_IPADDRESS1),编辑框(IDC_EDIT_SEND),按钮(IDC_BTN_SEND,“发送”)。
在InitInstance中调用AfxSocketInit初始化套接字,这个函数的调用会使得在程序终止前自动::WSACleanup,释放资源。

 

 // 在StdAfx.h中包含 <Afxsock.h>(StdAfx.h是一个预编译头文件,在每个MFC源文件中都会包含它)
 #include <Afxsock.h>

 // Chat.cpp
 BOOL CCharApp::InitInstance()
 {
  if(!AfxSocketInit())          // 用AfxSocketInit加载套接字库的另一个好处是不需要链接库文件
  {
   AfxMessageBox("加载套接字库失败!");
   return FALSE;
  }
  ...
 }

在CCharDlg中增加成员函数,初始化套接字。增加私有成员变量SOCKET m_socket。

 BOOL CChatDlg::InitSocket()
 {
  m_socket = socket(AF_INET, SOCK_DGRAM, 0);       // 创建套接字

  if (INVALID_SOCKET == m_socket)         // 如果套接字创建失败
  {
   MessageBox("套接字创建失败!");        // 提示
   return FALSE;
  }

  SOCKADDR_IN addrSock;
  addrSock.sin_family = AF_INET;         // sin_family总是AF_INET
  addrSock.sin_port = htons(6000);        // 端口为6000
  addrSock.sin_addr.S_un.S_addr = htonl(INADDR_ANY);      // INADDR_ANY允许套接字向任何分配给本地计算机的IP地址发送或接收数据(注意是网络字节序)

  int retval;
  retval = bind(m_socket, (SOCKADDR*)&addrSock, sizeof(SOCKADDR));    // 绑定套接字和地址
  if (SOCKET_ERROR == retval)         // 如果绑定失败
  {
   closesocket(m_socket);         // 关闭套接字
   MessageBox("绑定失败!");
   return FALSE;
  }
  return TRUE;
 }

在OnInitDialog中调用InitSocket。

 BOOL CChatDlg::OnInitDialog()
 {
  ...
  InitSocket();

  return TRUE;
 }


现在编写接收部分的程序。在接收数据时,如果没有数据到来,recvfrom函数会阻塞,程序不响应,将这部分代码放在单独的线程中完成就可以使主程序保持响应。
在线程函数中要传递2个参数,分别是套接字和对话框(或编辑框)的句柄,那么就可以从套接字中接收数据并将内容显示在编辑框内。由于CreateThread函数只提供了1个可供传递的参数(LPVOID lpParameter),这是一个指针,那么就需要自定义一个结构体(其中包括2个参数),将这个结构体的指针传递给线程函数。
注意:线程函数不能为类的成员函数,因为要调用类的成员函数前必须生成类对象,而CreateThread函数无从知晓如何生成类对象。变通的方法是将类的成员函数声明为static静态函数,类的静态成员函数不属于某个类对象,即在类对象产生之前就可被调用,那么CreateThread就能够正常执行了。

 

 // CCharDlg.h
 #define WM_RECVDATA WM_USER + 1;         // 自定义消息WM_RECVDATA

 struct RECVPARAM           // 一个用于传递参数的结构体
 {
  SOCKET sock;           // 套接字变量
  HWND hwnd;           // 窗口句柄变量
 };

 class CChatDlg : public CDialog
 {
 public:
  static DWORD WINAPI RecvProc(LPVOID lpParameter);      // 将线程函数定义为类的静态成员函数
  ...
  afx_msg void OnRecvData(WPARAM wParam, LPARAM lParam);      // 定义消息响应函数(注意需要加上参数,让数据传递进来)
  ...
 };

 // CCharDlg.cpp
 BEGIN_MESSAGE_MAP(CChatDlg, CDialog)
  //{{AFX_MSG_MAP(CChatDlg)
  ON_WM_SYSCOMMAND()
  ON_WM_PAINT()
  ON_WM_QUERYDRAGICON()
  //}}AFX_MSG_MAP
  ON_MESSAGE(WM_RECVDATA, OnRecvData)        // 增加WM_RECVDATA的消息映射
 END_MESSAGE_MAP()

 DWORD WINAPI CChatDlg::RecvProc(LPVOID lpParameter)       // 接收数据的线程函数
 {
  SOCKET sock = ((RECVPARAM*)lpParameter)->sock;       // 将传递进来的参数取出(注意需要强制类型转换)
  HWND hwnd = ((RECVPARAM*)lpParameter)->hwnd;

  SOCKADDR_IN addrFrom;          // 接收发送端的地址
  int len = sizeof(SOCKADDR);         // 接收发送端的地址的长度

  char recvBuf[200];          // 定义接收数据的字符数组
  char tempBuf[300];          // 定义存放中间数据的字符数组
  int retval;

  while (TRUE)
  {
   retval = recvfrom(sock, recvBuf, 200, 0, (SOCKADDR*)&addrFrom, &len);   // 接收数据,保存返回值到retval
   if (SOCKET_ERROR == retval)        // 如果recvfrom出错
    return;
   sprintf(tempBuf, "%s说:%s", inet_ntoa(addrFrom.sin_addr), recvBuf);   // 将对方的地址和收到的数据格式化
   ::PostMessage(hwnd, WM_RECVDATA, 0, (LPARAM)tempBuf);     // 用自定义消息将数据发送给对话框
  }

  return 0;
 }

 BOOL CChatDlg::OnInitDialog()
 {
  ...
  InitSocket();           // 调用InitSocket创建套接字并绑定地址

  RECVPARAM *pRecvParam = new RECVPARAM;        // 传参结构体,在堆上分配空间,并将其地址赋值给一个指针类型变量
  pRecvParam->sock = m_socket;         // 对结构体中的变量初始化(第一个是套接字)
  pRecvParam->hwnd = m_hWnd;         // 第二个是对话框的句柄

  HANDLE hThread = CreateThread(NULL,        // 创建线程,NULL表示使用缺省的安全属性
     0,         // 默认的堆栈大小(和调用线程的一样)
     RecvProc,        // 线程函数
     (LPVOID)pRecvParam,       // 将结构体指针传递给线程函数
     0,         // flags为0,一旦创建立即运行
     NULL);         // 返回线程的ID,不需要的话可以设置为NULL(Win9x系统不能设为NULL)
  CloseHandle(hThread);          // 关闭线程的句柄(关闭句柄,并不是终止这个线程,而是表示在这里不会再引用这个线程了,其次,关闭句柄后,系统会递减新线程的内核对象的使用计数,当线程运行结束后,如果该内核对象的使用计数为0,系统就会释放资源,否则只有等到进程终止后,系统才能释放资源)

  return TRUE;
 }

 void CChatDlg::OnRecvData(WPARAM wParam, LPARAM lParam)       // WM_RECVDATA消息响应函数的实现
 {
  CString str = (char*)lParam;         // 取出数据
  CString strTemp;          // 接收编辑框中已有的数据

  GetDlgItemText(IDC_EDIT_RECV, strTemp);        // 取出编辑框中已有的数据
  str += "\r\n";           // 增加换行
  str += strTemp;           // 将新收到的数据放在原数据之前
  SetDlgItemText(IDC_EDIT_RECV, str);        // 将数据放到编辑框中
 }

 // 增加IDC_BTN_SEND的命令响应函数
 void CChatDlg::OnBtnSend()
 {
  DWORD dwIP;
  ((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);    // 从IP地址控件获得用户输入的IP地址

  SOCKADDR_IN addrTo;
  addrTo.sin_family = AF_INET;         // sin_family总是AF_INET
  addrTo.sin_port = htons(6000);         // 端口为6000
  addrTo.sin_addr.S_un.S_addr = htonl(dwIP);       // 用户指定的IP

  CString strSend;
  GetDlgItemText(IDC_EDIT_SEND, strSend);
  sendto(m_socket, strSend, strSent.GetLength() + 1, 0, (SOCKADDR*)&addrTo, sizeof(SOCKADDR)); // 发送数据
  SetDlgItemText(IDC_EDIT_SEND, "");
 }


编译运行后,在IP地址控件中输入127.0.0.1,发送编辑框中输入内容后点击发送按钮就可以在接收编辑框中看到内容了。还可以将发送按钮设为Default button并隐藏,这样直接在发送编辑框中输入内容并按回车就可以直接将内容发送。
以上是在一台主机上的实验,如果在2台互联的主机上分别运行此程序,只需输入对方的IP地址,就可以实现聊天功能。

  评论这张
 
阅读(428)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017