2004年12月03日

C++的各种new简介


1.new T


第一种new最简单,调用类的(如果重载了的话)或者全局的operator new分配空间,然后用
类型后面列的参数来调用构造函数,用法是
new TypeName(initial_args_list). 如果没有参数,括号一般可以省略.例如


int *p=new int;
int *p=new int(10);
int *p=new foo(“hello”);


通过调用delete来销毁:
delete p;


2. new T[]
这种new用来创建一个动态的对象数组,他会调用对象的operator new[]来分配内存(如果
没有则调用operator new,搜索顺序同上),然后调用对象的31m默认构造函数初始化每个对象
用法:
new TypeName[num_of_objects];
例如
int *p= new int[10];
销毁时使用operator delete31m[]


3.new()T 和new() T[]
这是个带参数的new,这种形式的new会调用operator new(size_t,OtherType)来分配内存
这里的OtherType要和new括号里的参数的类型兼容,


这种语法通常用来在某个特定的地址构件对象,称为placement new,前提是operator new
(size_t,void*)已经定义,通常编译器已经提供了一个实现,包含<new>头文件即可,这个
实现只是简单的把参数的指定的地址返回,因而new()运算符就会在括号里的地址上创建
对象


需要说明的是,第二个参数不是一定要是void*,可以识别的合法类型,这时候由C++的重载
机制来决定调用那个operator new


当然,我们可以提供自己的operator new(size_,Type),来决定new的行为,比如
char data[1000][sizeof(foo)];
inline void* operator new(size_t ,int n)
{
        return data[n];
}


就可以使用这样有趣的语法来创建对象:
foo *p=new(6) foo(); //把对象创建在data的第六个单元上
的确很有意思
标准库还提供了一个nothrow的实现:
void* operator new(std::size_t, const std::nothrow_t&) throw();
void* operator new[](std::size_t, const std::nothrow_t&) throw();


就可以实现调用new失败时不抛出异常
new(nothrow) int(10);
// nothrow 是std::nothrow_t的一个实例



placement new 创建的对象不能直接delete来销毁,而是要调用对象的析够函数来销毁对
象,至于对象所占的内存如何处理,要看这块内存的具体来源


4. operator new(size_t)
这个的运算符分配参数指定大小的内存并返回首地址,可以为自定义的类重载这个运算符,
方法就是在类里面声明加上
void *operator new(size_t size)
{
        //在这里分配内存并返回其地址
}
无论是否声明,类里面重载的各种operator new和operator delete都是具有static属性的


一般不需要直接调用operator new,除非直接分配原始内存(这一点类似于C的malloc)
在冲突的情况下要调用全局的operator加上::作用域运算符:
::operator new(1000); // 分配1000个31m字节


返回的内存需要回收的话,调用对应的operator delete


5.operator new[](size_t)


这个也是分配内存,,只不过是专门针对数组,也就是new T[]这种形式,当然,需要时可以
显式调用


6.operator new(size_t size, OtherType other_value)
和operator new[](size_t size, OtherType other_value)
参见上面的new()



需要强调的是,new用来创建对象并分配内存,它的行为是不可改变的,可以改变的是各种
operator new,我们就可以通过重载operator new来实现我们的内存分配方案.

2004年12月01日

写一个函数creat(),建立一个有5个学生的单向链表。

分析建立过程:


设:链表结构为:
    struct student
    {
     long num;
     float score;
     struct student *next;
    };


当输入的num等于零时,表示建立过程结束。


定义以下变量:
   struct student *head; /* 表头 */
   struct student *p1;  /* 新建结点 */
   struct student *p2;  /* 表尾结点 */
















通过以上分析,可以得到创建链表的算法,其中,n=1表示创建的是第一个结点。




程序:


#include “stdio.h”
#include “alloc.h”
#include “stdlib.h”


struct student
{
 long num;
 float score;
 struct student *next;
 };


#define LEN sizeof(struct student)


/* 注意,”#define NULL 0″不需要,因为,NULL已在stdio.h中定义 */


int n;


struct student * creat() /* 创建链表,并返回表头指针 */
{
 struct student *head; /* 表头 */
 struct student *p1;  /* 新建结点 */
 struct student *p2;  /* 表尾结点 */



 n = 0; /* 结点数为0 */
 head = NULL; /* 还没有任何结点,表头为指向空 */
 p1 = (struct student *)malloc(LEN); /* 创建一个新结点p1 */
 p2 = p1; /* 表尾p2也指向p1 */ 


 scanf(“%ld,%f”, &p1->num, &p1->score); /* 读入第一个结点的学生数据 */


 while(p1->num != 0) /* 假设num=0表示输入结束 */
 {
  n = n + 1;
  if (n == 1) head = p1;   /* 第一个新建结点是表头 */
  else    p2->next = p1; /* 原表尾的下一个结点是新建结点 */


  p2 = p1; /* 新建结点成为表尾 */


  p1 = (struct student *)malloc(LEN); /* 新建一个结点 */
  scanf(“%ld,%f”, &p1->num, &p1->score); /* 读入新建结点的学生数据 */
 }
 
 free(p1);   /* 对于num=0的结点,未加入链表,应删除其空间 */


 p2->next = NULL; /* 输入结束,表尾结点的下一个结点为空 */


 return (head); /* 返回表头指针 */

2004年11月30日

删除一个结点的算法:



应考虑以下情况:


1、找到需要删除的结点,用p1指向它。并用p2指向p1的前一个结点。


2、要删除的结点是头结点。




3、要删除的结点不是头结点



根据以上情况分析,得到删除一个结点的算法:



程序:


在链表head中删除学号为num的结点。以表头指针head和需要删除的结点的num(学号)为参数,返回删除操作后的链表表头。


struct student * del(struct student *head, long num)
{
 struct student *p1; /* 指向要删除的结点 */
 struct student *p2; /* 指向p1的前一个结点 */


 if (head == NULL) /* 空表 */
 {
  printf(“\n List is NULL\n”);
  return (head);
 }
 p1 = head;
 while(num != p1->num && p1->next != NULL) /* 查找要删除的结点 */
 {
  p2 = p1;
  p1 = p1->next;
 }
 if (num == p1->num) /* 找到了 */
 {
  if (p1 == head) /* 要删除的是头结点 */
   head = p1->next;
  else      /* 要删除的不是头结点 */
   p2->next = p1->next;
  free(p1); /* 释放被删除结点所占的内存空间 */
       /* 教材p235称:删除一个结点,并不是真正从内存把它抹掉 ?? */
  printf(“delete: %ld\n”, num);
  n = n – 1;
 }
 else /* 在表中未找到要删除的结点 */
  printf(“%ld not found\n”);


 return (head); /* 返回新的表头 */
}



#include “stdio.h”
#include “malloc.h”
#include “stdlib.h”

/*  单链表 示意图
——————-
| data   |   next |
——————-
data : 数据域,存放数据
next : 指针域,指向直接后继
*/

typedef char DataType;
typedef struct node
{
 DataType data; //以字符为演示
 struct node * next;
}ListNode;

typedef ListNode * LinkList;

LinkList CreateListF() //头插法建链表
{
 printf(“请输入要创建的链表数据(头插法):”);
 char ch;
 LinkList head=NULL;
 ListNode * s;
 ch=getchar();
 while (ch!=’n’)
 {
  s=(ListNode *)malloc(sizeof(ListNode));
  if (!s)
  {
   printf(“分配内存空间失败!”);
   return NULL;
  }
  s->data=ch;
  s->next=head;
  head=s;
  ch=getchar();
 }
 return head;
}

LinkList CreateListR()//尾插法建链表
{
 printf(“请输入要创建的链表数据(尾插法):”);
 char ch;
 LinkList head=NULL;
 ListNode * s=NULL, * r=NULL;
 while ((ch=getchar())!=’n’)
 {
  s=(ListNode *)malloc(sizeof(ListNode));
  if (!s)
  {
   printf(“分配内存空间失败!”);
   return NULL;
  }
  s->data=ch;
  if (head==NULL) //头为空,则说明没表,直接插入即可
   head=s;
  else
   r->next=s;
  r=s; //直接放尾部
 }
 if (r!=NULL)
  r->next=NULL;
 return head;
}

void DispList(ListNode * p)//显示出链表的数据
{
 printf(“n=======================n”);
 int ret=0;
 while (p!=NULL)
 {
  printf(“%d -> %cn”,ret,p->data);
  p=p->next;
  ret++;
 }
 printf(“=======================n”);
}

ListNode * GetNode(LinkList head,int pos)//按位置/序号查找
{
 int cur_pos=0; //当前位置
 ListNode * p=head;
 while (p->next && cur_pos<pos)
 {
  p=p->next;
  cur_pos++;
 }
 if (pos==cur_pos)
  return p;
 else
  return NULL;
}

ListNode * LocateNode(ListNode * head,DataType key)//按数据查找
{
 ListNode * p=head->next;
 while (p && p->data!=key)
  p=p->next;
 return p; 
}

void InsertNode(LinkList head,DataType x,int i)
{
 ListNode * p=GetNode(head,i-1);
 if (p==NULL)
 {
  printf(“插入位置错误n”);
  return;
 }
 ListNode * s=(ListNode *)malloc(sizeof(ListNode));
 s->data=x;
 s->next=p->next; // 插入新节点
 p->next=s;
}

void DeleteNode(LinkList head,int pos)
{
 ListNode * p, * r;
 p=GetNode(head,pos-1);
 if (p==NULL || p->next==NULL)
 {
  printf(“删除位置错误n”);
  return;
 }

 r=p->next;
 p->next=r->next;
 free(r);
}

//some demo
void main()
{
 printf(“=============== 链表演示程序 ================nn”);
 LinkList p=CreateListR();
 DispList(&(*p));
 ListNode * nod=GetNode(p,2);
 if (nod!=NULL)
  printf(“n查找到的第2个数据为:[%c] n”,nod->data);

 ListNode * nod2=LocateNode(&(*p),’c’);
 if (nod2!=NULL)
  printf(“n查找到的[’C’]数据为:[%c] n”,nod2->data);

 printf(“在位置[3]处插入数据[’@’]后的链表:”);
 InsertNode(p,’@’,3);
 DispList(&(*p));

 printf(“删除位置[4]后的链表:”);
 DeleteNode(p,4);
 DispList(&(*p));
}

 


发布者: soarlove
/* ======================================== */
/*    二叉树的中序遍历                      */
/* ======================================== */
#include <stdlib.h>


struct tree                       /* 树的结构宣告       */
{
   int data;                      /* 节点数据           */
   struct tree *left;             /* 指向左子树的指标   */
   struct tree *right;            /* 指向右子树的指标   */
};
typedef struct tree treenode;     /* 树的结构新型态     */
typedef treenode *btree;          /* 宣告树节点指标型态 */


/* —————————————- */
/*  插入二叉树的节点                        */
/* —————————————- */
btree insertnode(btree root,int value)
{


   btree newnode;                 /* 树根指标           */
   btree current;                 /* 目前树节点指标     */
   btree back;                    /* 父节点指标         */


   /* 建立新节点记忆体 */
   newnode = ( btree ) malloc(sizeof(treenode));
   newnode->data = value;         /* 建立节点内容       */
   newnode->left = NULL;          /* 设定指标初值       */
   newnode->right = NULL;         /* 设定指标初值       */
   if ( root == NULL )            /* 是否是根节点       */
   {
      return newnode;             /* 传回新节点位置     */
   }
   else
   {
      current = root;             /* 保留目前树指标     */
      while ( current != NULL )
      {
         back = current;          /* 保留父节点指标     */
         if ( current->data > value )    /* 比较节点值  */
            current = current->left;     /* 左子树      */
         else
            current = current->right;    /* 右子树      */
      }
      if ( back->data > value )   /* 接起父子的链结     */
         back->left = newnode;    /* 左子树             */
      else
         back->right = newnode;   /* 右子树             */
   }
   return root;                   /* 传回树根指标       */
}


/* —————————————- */
/*  建立二叉树                              */
/* —————————————- */
btree createbtree(int *data,int len)
{
   btree root = NULL;             /* 树根指标           */
   int i;


   for ( i = 0; i < len; i++ )    /* 用回路建立树状结构 */
      root = insertnode(root,data[i]);
   return root;
}


/* —————————————- */
/*  二叉树中序遍历                          */
/* —————————————- */
void inorder(btree ptr)
{
   if ( ptr != NULL )             /* 终止条件           */
   {
      inorder(ptr->left);         /* 左子树             */
      printf(“[%2d]n”,ptr->data);  /* 列印节点内容     */
      inorder(ptr->right);        /* 右子树             */
   }
}


/* —————————————- */
/*  主程式: 建立二叉树且用中序遍历列印出来. */
/* —————————————- */
void main()
{
   btree root = NULL;             /* 树根指标           */


   /* 二叉树节点数据 */
   int data[9] = { 5, 6, 4, 8, 2, 3, 7, 1, 9 };
   root = createbtree(data,9);    /* 建立二叉树         */
   printf(“树的节点内容 n”);
   inorder(root);                 /* 中序遍历二叉树     */
}



/* ======================================== */
/*    二叉树的前序遍历                      */
/* ======================================== */
#include <stdlib.h>


struct tree                       /* 树的结构宣告       */
{
   int data;                      /* 节点数据           */
   struct tree *left;             /* 指向左子树的指标   */
   struct tree *right;            /* 指向右子树的指标   */
};
typedef struct tree treenode;     /* 树的结构新型态     */
typedef treenode *btree;          /* 宣告树节点指标型态 */


/* —————————————- */
/*  插入二叉树的节点                        */
/* —————————————- */
btree insertnode(btree root,int value)
{


   btree newnode;                 /* 树根指标           */
   btree current;                 /* 目前树节点指标     */
   btree back;                    /* 父节点指标         */


   /* 建立新节点记忆体 */
   newnode = ( btree ) malloc(sizeof(treenode));
   newnode->data = value;         /* 建立节点内容       */
   newnode->left = NULL;          /* 设定指标初值       */
   newnode->right = NULL;         /* 设定指标初值       */
   if ( root == NULL )            /* 是否是根节点       */
   {
      return newnode;             /* 传回新节点位置     */
   }
   else
   {
      current = root;             /* 保留目前树指标     */
      while ( current != NULL )
      {
         back = current;          /* 保留父节点指标     */
         if ( current->data > value ) /* 比较节点值     */
            current = current->left;  /* 左子树         */
         else
            current = current->right; /* 右子树         */
      }
      if ( back->data > value )   /* 接起父子的链结     */
         back->left = newnode;    /* 左子树             */
      else
         back->right = newnode;   /* 右子树             */
   }
   return root;                   /* 传回树根指标       */
}


/* —————————————- */
/*  建立二叉树                              */
/* —————————————- */
btree createbtree(int *data,int len)
{
   btree root = NULL;             /* 树根指标           */
   int i;


   for ( i = 0; i < len; i++ )    /* 用回路建立树状结构 */
      root = insertnode(root,data[i]);
   return root;
}


/* —————————————- */
/*  二叉树前序遍历                          */
/* —————————————- */
void preorder(btree ptr)
{
   if ( ptr != NULL )             /* 终止条件           */
   {
      printf(“[%2d]n”,ptr->data);  /* 列印节点内容     */
      preorder(ptr->left);        /* 左子树             */
      preorder(ptr->right);       /* 右子树             */
   }
}


/* —————————————- */
/*  主程式: 建立二叉树且用前序遍历列印出来. */
/* —————————————- */
void main()
{
   btree root = NULL;             /* 树根指标           */


   /* 二叉树节点数据 */
   int data[9] = { 5, 6, 4, 8, 2, 3, 7, 1, 9 };
   root = createbtree(data,9);    /* 建立二叉树         */
   printf(“树的节点内容 n”);
   preorder(root);                /* 前序遍历二叉树     */
}




/* ======================================== */
/*    二叉树的后序遍历                      */
/* ======================================== */
#include <stdlib.h>


struct tree                       /* 树的结构宣告       */
{
   int data;                      /* 节点数据           */
   struct tree *left;             /* 指向左子树的指标   */
   struct tree *right;            /* 指向右子树的指标   */
};
typedef struct tree treenode;     /* 树的结构新型态     */
typedef treenode *btree;          /* 宣告树节点指标型态 */


/* —————————————- */
/*  插入二叉树的节点                        */
/* —————————————- */
btree insertnode(btree root,int value)
{


   btree newnode;                 /* 树根指标           */
   btree current;                 /* 目前树节点指标     */
   btree back;                    /* 父节点指标         */


   /* 建立新节点记忆体 */
   newnode = ( btree ) malloc(sizeof(treenode));
   newnode->data = value;         /* 建立节点内容       */
   newnode->left = NULL;          /* 设定指标初值       */
   newnode->right = NULL;         /* 设定指标初值       */
   if ( root == NULL )            /* 是否是根节点       */
   {
      return newnode;             /* 传回新节点位置     */
   }
   else
   {
      current = root;             /* 保留目前树指标     */
      while ( current != NULL )
      {
         back = current;          /* 保留父节点指标     */
         if ( current->data > value )    /* 比较节点值  */
            current = current->left;     /* 左子树      */
         else
            current = current->right;    /* 右子树      */
      }
      if ( back->data > value )   /* 接起父子的链结     */
         back->left = newnode;    /* 左子树             */
      else
         back->right = newnode;   /* 右子树             */
   }
   return root;                   /* 传回树根指标       */
}


/* —————————————- */
/*  建立二叉树                              */
/* —————————————- */
btree createbtree(int *data,int len)
{
   btree root = NULL;             /* 树根指标           */
   int i;


   for ( i = 0; i < len; i++ )    /* 用回路建立树状结构 */
      root = insertnode(root,data[i]);
   return root;
}


/* —————————————- */
/*  二叉树后序遍历                          */
/* —————————————- */
void postorder(btree ptr)
{
   if ( ptr != NULL )             /* 终止条件           */
   {
      postorder(ptr->left);       /* 左子树             */
      postorder(ptr->right);      /* 右子树             */
      printf(“[%2d]n”,ptr->data);  /* 列印节点内容     */
   }
}


/* —————————————- */
/*  主程式: 建立二叉树且用后序遍历列印出来. */
/* —————————————- */
void main()
{
   btree root = NULL;             /* 树根指标           */


   /* 二叉树节点数据 */
   int data[9] = { 5, 6, 4, 8, 2, 3, 7, 1, 9 };
   root = createbtree(data,9);    /* 建立二叉树         */
   printf(“树的节点内容 n”);
   postorder(root);               /* 后序遍历二叉树     */
}

2004年11月27日

在Win32下提供的进程间通信方式有以下几种:


  • 剪贴板Clipboard:在16位时代常使用的方式,CWnd类中提供了支持。
  • COM/DCOM:通过COM系统的代理存根方式进行进程间数据交换,但只能够表现在对接口函数的调用时传送数据,通过DCOM可以在不同主机间传送数据。
  • Dynamic Data Exchange (DDE):在16位时代常使用的方式。
  • File Mapping:文件映射,在32位系统中提供的新方法,可用来共享内存。
  • Mailslots:邮件槽,在32位系统中提供的新方法,可在不同主机间交换数据,分为服务器方和客户方,双方可以通过其进行数据交换,在Win9X下只支持邮件槽客户。
  • Pipes:管道,分为无名管道:在父子进程间交换数据;有名管道:可在不同主机间交换数据,分为服务器方和客户方,在Win9X下只支持有名管道客户。
  • RPC:远程过程调用,很少使用,原因有两个:复杂而且与UNIX系统的RCP并不完全兼容。但COM/DCOM的调用是建立在RPC的基础上的。
  • Windows Sockets:网络套接口,可在不同主机间交换数据,分为服务器方和客户方。(相关介绍见Visual C++/MFC入门教程 第六章 网络通信开发
  • WM_COPYDATA:通过发送WM_COPYDATA消息并将数据放在参数中来传递数据给其他进程。

下面主要介绍一下命名管道的用法,命名管道是一个有名字,单向或双向的通信管道。管道的名称有两部分组成:计算机名和管道名,例如\\[host_name]\pipe\[pipe_name]\(括号内为参数)。对于同一主机来讲允许有多个同一命名管道的实例并且可以由不同的进程打开,但是不同的管道都有属于自己的管道缓冲区而且有自己的通讯环境互不影响,并且命名管道可以支持多个客户端连接一个服务器端。命名管道客户端不但可以与本机上的服务器通讯也可以同其他主机上的服务器通讯。

命名管道的连接和通讯采用如下方式:


  • 在服务器端第一次创建命名管道后等待连接,当客户端连接成功后服务器端的命名管道就用作通讯用途。如果需要再次等待连接,服务器端就需要再次打开命名管道(创建一个命名管道的实例)并等待连接。
  • 对于客户端每次打开命名管道后建立与服务器间的连接,然后就可以利用命名管道进行通信,如果需要建立第二个连接则需要再次打开管道和再次建立连接。

创建命名管道时需要指定一个主机名和管道名,对于客户端来说可以是如下格式:\\[host_name]\pipe\[pipe_name]\也可以是\\.\pipe\pipe_name\其中.表示本机。而服务器端只能够在指定本机作为主机名,即只能使用下面的格式:\\.\pipe_name\。此外需要记住,在同一主机上管道名称是唯一的,一个命名管道一旦被创建就不允许相同名称的管道再被创建。

服务器方通过:

HANDLE CreateNamedPipe(
LPCTSTR lpName, // pipe name
DWORD dwOpenMode, // pipe open mode
DWORD dwPipeMode, // pipe-specific modes
DWORD nMaxInstances, // maximum number of instances
DWORD nOutBufferSize, // output buffer size
DWORD nInBufferSize, // input buffer size
DWORD nDefaultTimeOut, // time-out interval
LPSECURITY_ATTRIBUTES lpSecurityAttributes // SD
);
创建命名管道和打开已经存在的命名管道,其中lpName为管道名称,dwOpenMode为创建方式,可以是下面值的组合:

  • PIPE_ACCESS_INBOUND:管道只能用作接收数据。
  • PIPE_ACCESS_OUTBOUND:管道只能用作发送数据。
  • PIPE_ACCESS_DUPLEX:管道既可以发送也可以接收数据。(上面这三个值只能够取其中一个)
  • FILE_FLAG_WRITE_THROUGH:管道用于同步发送和接收数据,只有在数据被发送到目标地址时发送函数才会返回,如果不设置这个参数那么在系统内部对于命名管道的处理上可能会因为减少网络附和而在数据积累到一定量时才发送,并且对于发送函数的调用会马上返回。
  • FILE_FLAG_OVERLAPPED:管道可以用于异步输入和输出,异步读写的有关方法和文件异步读写是相同的。
dwPipeMode指定管道类型,可以是下面值的组合:

  • PIPE_TYPE_BYTE:数据在通过管道发送时作为字节流发送,不能与PIPE_READMODE_MESSAGE共用。
  • PIPE_TYPE_MESSAGE:数据在通过管道发送时作为消息发送,不能与PIPE_READMODE_BYTE共用。
  • PIPE_READMODE_BYTE:在接收数据时接收字节流。
  • PIPE_READMODE_MESSAGE:在接收数据时接收消息。
  • PIPE_WAIT:使用等待模式,在读,写和建立连接时都需要管道的另一方完成相应动作后才会返回。
  • PIPE_NOWAIT:使用非等待模式,在读,写和建立连接时不需要管道的另一方完成相应动作后就会立即返回。
nMaxInstances为管道的的最大数量,在第一次建立服务器方管道时这个参数表明该管道可以同时存在的数量。PIPE_UNLIMITED_INSTANCES表明不对数量进行限制。nOutBufferSize和nInBufferSize表示缓冲区的大小。nDefaultTimeOut表示在等待连接时最长的等待时间(以毫秒为单位),如果在创建时设置为NMPWAIT_USE_DEFAULT_WAIT表明无限制的等待,而以后服务器方的其他管道实例也需要设置相同的值。lpSecurityAttributes为安全属性,一般设置为NULL。如果创建或打开失败则返回INVALID_HANDLE_VALUE。可以通过GetLastError得到错误。

客户方通过:

HANDLE CreateFile(
LPCTSTR lpFileName, // file name
DWORD dwDesiredAccess, // access mode
DWORD dwShareMode, // share mode
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // SD
DWORD dwCreationDisposition, // how to create
DWORD dwFlagsAndAttributes, // file attributes
HANDLE hTemplateFile // handle to template file
);
创建客户端命名管道,CreateFile可以有很多用途,可以用来创建文件,管道,邮件槽,目录等,这里介绍用CreateFile来打开客户端命名管道。lpFileName用来指明管道名称。dwDesiredAccess用来表明使用方式,可以使用下面的值:

  • GENERIC_READ:打开一个只用于读的管道。
  • GENERIC_WRITE:打开一个只用于写的管道。
  • GENERIC_READ | GENERIC_WRITE:打开一个用于读和写的管道。
dwShareMode指定共享方式,一般指定为0,lpSecurityAttributes为安全属性,一般设置为NULL,dwCreationDisposition设置为OPEN_EXISTING,dwFlagsAndAttributes设置为FILE_ATTRIBUTE_NORMAL,此外可以还设置为FILE_FLAG_OVERLAPPED来进行异步通讯,hTemplateFile设置为NULL。如果打开失败则返回INVALID_HANDLE_VALUE。可以通过GetLastError得到错误。

此外客户方可以利用:

BOOL CallNamedPipe(
LPCTSTR lpNamedPipeName, // pipe name
LPVOID lpInBuffer, // write buffer
DWORD nInBufferSize, // size of write buffer
LPVOID lpOutBuffer, // read buffer
DWORD nOutBufferSize, // size of read buffer
LPDWORD lpBytesRead, // number of bytes read
DWORD nTimeOut // time-out value
);
来创建一个发送消息的管道。

管道的连接管理,客户方在调用CreateFile后立即就能够建立服务器的连接,而服务器方一旦管道打开或创建后可以用

BOOL ConnectNamedPipe(
HANDLE hNamedPipe, // handle to named pipe
LPOVERLAPPED lpOverlapped // overlapped structure
);
来等待客户端的连接建立。如果希望在服务器方检测是否有连接到达,可以调用
BOOL WaitNamedPipe(
LPCTSTR lpNamedPipeName, // pipe name
DWORD nTimeOut // time-out interval
);
这里的lpNamePipeName直接使用创建管道时的名称,如果在服务器方希望关闭连接则调用
BOOL DisconnectNamedPipe(
HANDLE hNamedPipe // handle to named pipe
);

一旦连接被关闭,服务器方可以再次调用ConnectNamedPipe来建立连接。如果要关闭管道则直接调用CloseHandle。请注意这里提到的关闭管道和关闭连接是不同的意思,在同一个管道上可以依次反复建立连接,而且可以减小系统的负荷。而且如果指定了管道最大数量限制那么在打开的管道达到最大限制后如果不关闭旧管道就无法打开新管道。

对于客户方则无法关闭连接,而只能直接调用CloseHandle关闭管道。

数据的发送,不论是服务器还是客户方都可以通过ReadFile和WriteFile进行管道读写来达到通讯的目的。

下面是一个例子,服务器方创建或打开一个管道并读入对方发送的数据,将小写字母转换成大写字母后返回,而客户发创建一个到服务器的连接并发送一个字符串并读回经过转换的数据:

在使用这个例子时,运行三个服务端进程,而运行第四个时会因为达到管道数量限制而打开管道失败。

//服务方
void CNamed_pipeDlg::OnCreateP()
{
DWORD dwTO = NMPWAIT_USE_DEFAULT_WAIT;//设置连接等待时间
HANDLE hSvr = CreateNamedPipe(“\\\\.\\pipe\\test_pipe\\”,PIPE_ACCESS_DUPLEX,PIPE_TYPE_BYTE,3,256,256,dwTO,NULL);
if( INVALID_HANDLE_VALUE == hSvr)
{
AfxMessageBox(“Error create/open pipe”);
}
else
{
if (ConnectNamedPipe(hSvr,NULL))
{
BYTE bRead;
DWORD dwRead,dwWritten;
while (ReadFile(hSvr,&bRead,1,&dwRead,NULL))
{
if(bRead >= ‘a’ && bRead $lt;=’z')
bRead = ‘A’+ (bRead-’a');
WriteFile(hSvr,&bRead,1,&dwWritten,NULL);
}
}
else
{
AfxMessageBox(“error when waiting connected”);
}
CloseHandle(hSvr);
}
}
//客户端
void CNamed_pipe_cDlg::OnConn()
{
HANDLE hClient = CreateFile(“\\\\.\\pipe\\test_pipe\\”,GENERIC_WRITE |GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(hClient == INVALID_HANDLE_VALUE)
{
AfxMessageBox(“Error open pipe”);
}
else
{
DWORD dwRead,dwWritten;
char szSend[10]=”send…”;
char szRecv[10];
for(int i=0;i<strlen(szSend)+1;i++)
{
WriteFile(hClient,szSend+i,1,&dwWritten,NULL);
ReadFile(hClient,szRecv+i,1,&dwRead,NULL);
}
CloseHandle(hClient);//close pipe
AfxMessageBox(szRecv);
}
}

这一节的内容比较多请你耐心的看完,因为进程/线程间同步的方法比较多,每种方法都有不同的用途:这节中会讲通过临界区互斥量信号灯事件来进行同步。

由于进程/线程间的操作是并行进行的,所以就产生了一个数据的问题同步,我们先看一段代码:

int iCounter=0;//全局变量
DOWRD threadA(void* pD)
{
for(int i=0;i<100;i++)
{
int iCopy=iCounter;
//Sleep(1000);
iCopy++;
//Sleep(1000);
iCounter=iCopy;
}
}
现在假设有两个线程threadA1和threadA2在同时运行那么运行结束后iCounter的值会是多少,是200吗?不是的,如果我们将Sleep(1000)前的注释去掉后我们会很容易明白这个问题,因为在iCounter的值被正确修改前它可能已经被其他的线程修改了。这个例子是一个将机器代码操作放大的例子,因为在CPU内部也会经历数据读/写的过程,而在线程执行的过程中线程可能被中断而让其他线程执行。变量iCounter在被第一个线程修改后,写回内存前如果它又被第二个线程读取,然后才被第一个线程写回,那么第二个线程读取的其实是错误的数据,这种情况就称为脏读(dirty read)。这个例子同样可以推广到对文件,资源的使用上。

那么要如何才能避免这一问题呢,假设我们在使用iCounter前向其他线程询问一下:有谁在用吗?如果没被使用则可以立即对该变量进行操作,否则等其他线程使用完后再使用,而且在自己得到该变量的控制权后其他线程将不能使用这一变量,直到自己也使用完并释放为止。经过修改的伪代码如下:

int iCounter=0;//全局变量
DOWRD threadA(void* pD)
{
for(int i=0;i<100;i++)
{
ask to lock iCounter
wait other thread release the lock
lock successful

{
int iCopy=iCounter;
//Sleep(1000);
iCopy++;
}
iCounter=iCopy;

release lock of iCounter
}
}


幸运的是OS提供了多种同步对象供我们使用,并且可以替我们管理同步对象的加锁和解锁。我们需要做的就是对每个需要同步使用的资源产生一个同步对象,在使用该资源前申请加锁,在使用完成后解锁。接下来我们介绍一些同步对象:

临界区:临界区是一种最简单的同步对象,它只可以在同一进程内部使用。它的作用是保证只有一个线程可以申请到该对象,例如上面的例子我们就可以使用临界区来进行同步处理。几个相关的API函数为:


  • VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection );产生临界区
  • VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection );删除临界区
  • VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection );进入临界区,相当于申请加锁,如果该临界区正被其他线程使用则该函数会等待到其他线程释放
  • BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection );进入临界区,相当于申请加锁,和EnterCriticalSection不同如果该临界区正被其他线程使用则该函数会立即返回FALSE,而不会等待
  • VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection );退出临界区,相当于申请解锁

下面的示范代码演示了如何使用临界区来进行数据同步处理:

//全局变量
int iCounter=0;
CRITICAL_SECTION criCounter;

DWORD threadA(void* pD)
{
int iID=(int)pD;
for(int i=0;i<8;i++)
{
EnterCriticalSection(&criCounter);
int iCopy=iCounter;
Sleep(100);
iCounter=iCopy+1;
printf(“thread %d : %d\n”,iID,iCounter);
LeaveCriticalSection(&criCounter);
}
return 0;
}
//in main function
{
//创建临界区
InitializeCriticalSection(&criCounter);
//创建线程
HANDLE hThread[3];
CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待线程结束
//至于WaitForMultipleObjects的用法后面会讲到。
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
//删除临界区
DeleteCriticalSection(&criCounter);
printf(“\nover\n”);

}


接下来要讲互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。下面介绍可以用在互斥量上的API函数:

创建互斥量:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,// 安全信息
BOOL bInitialOwner, // 最初状态,
//如果设置为真,则表示创建它的线程直接拥有了该互斥量,而不需要再申请
LPCTSTR lpName // 名字,可以为NULL,但这样一来就不能被其他线程/进程打开
);
打开一个存在的互斥量:
HANDLE OpenMutex(
DWORD dwDesiredAccess, // 存取方式
BOOL bInheritHandle, // 是否可以被继承
LPCTSTR lpName // 名字
);
释放互斥量的使用权,但要求调用该函数的线程拥有该互斥量的使用权:
BOOL ReleaseMutex(//作用如同LeaveCriticalSection
HANDLE hMutex // 句柄
);
关闭互斥量:
BOOL CloseHandle(
HANDLE hObject // 句柄
);

你会说为什么没有名称如同EnterMutex,功能如同EnterCriticalSection一样的函数来获得互斥量的使用权呢?的确没有!获取互斥量的使用权需要使用函数:

DWORD WaitForSingleObject(
HANDLE hHandle, // 等待的对象的句柄
DWORD dwMilliseconds // 等待的时间,以ms为单位,如果为INFINITE表示无限期的等待
);
返回:
WAIT_ABANDONED 在等待的对象为互斥量时表明因为互斥量被关闭而变为有信号状态
WAIT_OBJECT_0 得到使用权
WAIT_TIMEOUT 超过(dwMilliseconds)规定时间

在线程调用WaitForSingleObject后,如果一直无法得到控制权线程讲被挂起,直到超过时间或是获得控制权。

讲到这里我们必须更深入的讲一下WaitForSingleObject函数中的对象(Object)的含义,这里的对象是一个具有信号状态的对象,对象有两种状态:有信号/无信号。而等待的含义就在于等待对象变为有信号的状态,对于互斥量来讲如果正在被使用则为无信号状态,被释放后变为有信号状态。当等待成功后WaitForSingleObject函数会将互斥量置为无信号状态,这样其他的线程就不能获得使用权而需要继续等待。WaitForSingleObject函数还进行排队功能,保证先提出等待请求的线程先获得对象的使用权,下面的代码演示了如何使用互斥量来进行同步,代码的功能还是进行全局变量递增,通过输出结果可以看出,先提出请求的线程先获得了控制权:

int iCounter=0;

DWORD threadA(void* pD)
{
int iID=(int)pD;
//在内部重新打开
HANDLE hCounterIn=OpenMutex(MUTEX_ALL_ACCESS,FALSE,”sam sp 44″);

for(int i=0;i<8;i++)
{
printf(“%d wait for object\n”,iID);
WaitForSingleObject(hCounterIn,INFINITE);
int iCopy=iCounter;
Sleep(100);
iCounter=iCopy+1;
printf(“\t\tthread %d : %d\n”,iID,iCounter);
ReleaseMutex(hCounterIn);
}
CloseHandle(hCounterIn);
return 0;
}

//in main function
{
//创建互斥量
HANDLE hCounter=NULL;
if( (hCounter=OpenMutex(MUTEX_ALL_ACCESS,FALSE,”sam sp 44″))==NULL)
{
//如果没有其他进程创建这个互斥量,则重新创建
hCounter = CreateMutex(NULL,FALSE,”sam sp 44″);
}

//创建线程
HANDLE hThread[3];
CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待线程结束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);

//关闭句柄
CloseHandle(hCounter);
}
}


在这里我没有使用全局变量来保存互斥量句柄,这并不是因为不能这样做,而是为演示如何在其他的代码段中通过名字来打开已经创建的互斥量。其实这个例子在逻辑上是有一点错误的,因为iCounter这个变量没有跨进程使用,所以没有必要使用互斥量,只需要使用临界区就可以了。假设有一组进程在同时使用一个文件那么我们可以使用互斥量来保证该文件只同时被一个进程使用(如果只是利用OS的文件存取控制功能则需要添加更多的错误处理代码),此外在调度程序中也可以使用互斥量来对资源的使用进行同步化。

现在我们回过头来讲WaitForSingleObject这个函数,从前面的例子中我们看到WaitForSingleObject这个函数将等待一个对象变为有信号状态,那么具有信号状态的对象有哪些呢?下面是一部分:


  • Mutex
  • Event
  • Semaphore
  • Job
  • Process
  • Thread
  • Waitable timer
  • Console input

互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以我们可以使用WaitForSingleObject来等待进程和线程退出。(至于信号灯,事件的用法我们接下来会讲)我们在前面的例子中使用了WaitForMultipleObjects函数,这个函数的作用与WaitForSingleObject类似但从名字上我们可以看出,WaitForMultipleObjects将用于等待多个对象变为有信号状态,函数原型如下:

DWORD WaitForMultipleObjects(
DWORD nCount, // 等待的对象数量
CONST HANDLE *lpHandles, // 对象句柄数组指针
BOOL fWaitAll, // 等待方式,
//为TRUE表示等待全部对象都变为有信号状态才返回,为FALSE表示任何一个对象变为有信号状态则返回
DWORD dwMilliseconds // 超时设置,以ms为单位,如果为INFINITE表示无限期的等待
);

返回值意义:
WAIT_OBJECT_0 到 (WAIT_OBJECT_0 + nCount – 1):当fWaitAll为TRUE时表示所有对象变为有信号状态,当fWaitAll为FALSE时使用返回值减去WAIT_OBJECT_0得到变为有信号状态的对象在数组中的下标。
WAIT_ABANDONED_0 到 (WAIT_ABANDONED_0 + nCount – 1):当fWaitAll为TRUE时表示所有对象变为有信号状态,当fWaitAll为FALSE时表示对象中有一个对象为互斥量,该互斥量因为被关闭而成为有信号状态,使用返回值减去WAIT_OBJECT_0得到变为有信号状态的对象在数组中的下标。
WAIT_TIMEOUT:表示超过规定时间。

前面的例子中的如下代码表示等待三个线程都变为有信号状态,也就是说三个线程都结束。
HANDLE hThread[3];
CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待线程结束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
此外,在启动和等待进程结束一文中就利用这个功能等待进程结束。

通过互斥量我们可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,你的老板会要求你根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。对信号灯的操作伪代码大致如下:

Semaphore sem=3;

dword threadA(void*)
{
while(sem <= 0)
{// 相当于 WaitForSingleObject
wait …
}
// sem > 0
// lock the Semaphore
sem — ;
do functions …
// release Semaphore
sem ++ ;
return 0;
}


这里信号灯有一个初始值,表示有多少进程/线程可以进入,当信号灯的值大于0时为有信号状态,小于等于0时为无信号状态,所以可以利用WaitForSingleObject进行等待,当WaitForSingleObject等待成功后信号灯的值会被减少1,直到释放时信号灯会被增加1。用于信号灯操作的API函数有下面这些:

创建信号灯:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,// 安全属性,NULL表示使用默认的安全描述
LONG lInitialCount, // 初始值
LONG lMaximumCount, // 最大值
LPCTSTR lpName // 名字
);
打开信号灯:
HANDLE OpenSemaphore(
DWORD dwDesiredAccess, // 存取方式
BOOL bInheritHandle, // 是否能被继承
LPCTSTR lpName // 名字
);
释放信号灯:
BOOL ReleaseSemaphore(
HANDLE hSemaphore, // 句柄
LONG lReleaseCount, // 释放数,让信号灯值增加数
LPLONG lpPreviousCount // 用来得到释放前信号灯的值,可以为NULL
);
关闭信号灯:
BOOL CloseHandle(
HANDLE hObject // 句柄
);

可以看出来信号灯的使用方式和互斥量的使用方式非常相似,下面的代码使用初始值为2的信号灯来保证只有两个线程可以同时进行数据库调用:

DWORD threadA(void* pD)
{
int iID=(int)pD;
//在内部重新打开
HANDLE hCounterIn=OpenSemaphore(SEMAPHORE_ALL_ACCESS,FALSE,”sam sp 44″);

for(int i=0;i<3;i++)
{
printf(“%d wait for object\n”,iID);
WaitForSingleObject(hCounterIn,INFINITE);
printf(“\t\tthread %d : do database access call\n”,iID);
Sleep(100);
printf(“\t\tthread %d : do database access call end\n”,iID);
ReleaseSemaphore(hCounterIn,1,NULL);
}
CloseHandle(hCounterIn);
return 0;
}
//in main function
{
//创建信号灯
HANDLE hCounter=NULL;
if( (hCounter=OpenSemaphore(SEMAPHORE_ALL_ACCESS,FALSE,”sam sp 44″))==NULL)
{
//如果没有其他进程创建这个信号灯,则重新创建
hCounter = CreateSemaphore(NULL,2,2,”sam sp 44″);
}

//创建线程
HANDLE hThread[3];
CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待线程结束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);

//关闭句柄
CloseHandle(hCounter);
}


信号灯有时用来作为计数器使用,一般来讲将其初始值设置为0,先调用ReleaseSemaphore来增加其计数,然后使用WaitForSingleObject来减小其计数,遗憾的是通常我们都不能得到信号灯的当前值,但是可以通过设置WaitForSingleObject的等待时间为0来检查信号灯当前是否为0。

接下来我们讲最后一种同步对象:事件,前面讲的信号灯和互斥量可以保证资源被正常的分配和使用,而事件是用来通知其他进程/线程某件操作已经完成。例如:现在有三个线程:threadA,threadB,threadC,现在要求他们中的部分功能要顺序执行,也就是说threadA执行完一部分后threadB执行,threadB执行完一部分后threadC开始执行。也许你觉得下面的代码可以满足要求:

要求:A1执行完后执行B2然后执行C3,再假设每个任务的执行时间都为1,而且允许并发操作。
方案一:
dword threadA(void*)
{
do something A1;
create threadB;
do something A2;
do something A3;
}

dword threadB(void*)
{
do something B1;
do something B2;
create threadC;
do something B3;
}

dword threadC(void*)
{
do something C1;
do something C2;
do something C3;
}

方案二:
dword threadA(void*)
{
do something A1;
do something A2;
do something A3;
}

dword threadB(void*)
{
do something B1;
wait for threadA end
do something B2;
do something B3;
}

dword threadC(void*)
{
do something C1;
do something C2;
wait for threadB end
do something C3;
}

main()
{
create threadA;
create threadB;
create threadC;
}

方案三:
dword threadA(void*)
{
do something A1;
release event1;
do something A2;
do something A3;
}

dword threadB(void*)
{
do something B1;
wait for envet1 be released
do something B2;
release event2;
do something B3;
}

dword threadC(void*)
{
do something C1;
do something C2;
wait for event2 be released
do something C3;
}

main()
{
create threadA;
create threadB;
create threadC;
}
比较一下三种方案的执行时间:
方案一 方案二 方案三
1 threadA threadB threadC threadA threadB threadC threadA threadB threadC
2 A1 A1 B1 C1 A1 B1 C1
3 A2 B1 A2 C2 A2 B2 C2
4 A1 B2 A3 A3 B3 C3
5 B3 C1 B2
6 C2 B3
7 C3 C3
8


可以看出来方案三的执行时间是最短的,当然这个例子有些极端,但我们可以看出事件对象用于通知其他进程/线程某件操作已经完成方面的作用是很大的,而且如果有的任务要在进程尖进行协调采用等待其他进程中线程结束的方式是不可能实现的。此外我也希望通过这个例子讲一点关于分析线程执行效率的方法。

事件对象可以一两种方式创建,一种为自动重置,在其他线程使用WaitForSingleObject等待到事件对象变为有信号后该事件对象自动又变为无信号状态,一种为人工重置在其他线程使用WaitForSingleObject等待到事件对象变为有信号后该事件对象状态不变。例如有多个线程都在等待一个线程运行结束,我们就可以使用人工重置事件,在被等待的线程结束时设置该事件为有信号状态,这样其他的多个线程对该事件的等待都会成功(因为该事件的状态不会被自动重置)。事件相关的API如下:

创建事件对象:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,// 安全属性,NULL表示使用默认的安全描述
BOOL bManualReset, // 是否为人工重置
BOOL bInitialState, // 初始状态是否为有信号状态
LPCTSTR lpName // 名字
);
打开事件对象:
HANDLE OpenEvent(
DWORD dwDesiredAccess, // 存取方式
BOOL bInheritHandle, // 是否能够被继承
LPCTSTR lpName // 名字
);
设置事件为无信号状态:
BOOL ResetEvent(
HANDLE hEvent // 句柄
);
设置事件有无信号状态:
BOOL SetEvent(
HANDLE hEvent // 句柄
);
关闭事件对象:
BOOL CloseHandle(
HANDLE hObject // 句柄
);

下面的代码演示了自动重置和人工重置事件在使用中的不同效果:

DWORD threadA(void* pD)
{
int iID=(int)pD;
//在内部重新打开
HANDLE hCounterIn=OpenEvent(EVENT_ALL_ACCESS,FALSE,”sam sp 44″);

printf(“\tthread %d begin\n”,iID);
//设置成为有信号状态
Sleep(1000);
SetEvent(hCounterIn);
Sleep(1000);
printf(“\tthread %d end\n”,iID);
CloseHandle(hCounterIn);
return 0;
}

DWORD threadB(void* pD)
{//等待threadA结束后在继续执行
int iID=(int)pD;
//在内部重新打开
HANDLE hCounterIn=OpenEvent(EVENT_ALL_ACCESS,FALSE,”sam sp 44″);

if(WAIT_TIMEOUT == WaitForSingleObject(hCounterIn,10*1000))
{
printf(“\t\tthread %d wait time out\n”,iID);
}
else
{
printf(“\t\tthread %d wait ok\n”,iID);
}
CloseHandle(hCounterIn);
return 0;
}

//in main function
{
HANDLE hCounter=NULL;
if( (hCounter=OpenEvent(EVENT_ALL_ACCESS,FALSE,”sam sp 44″))==NULL)
{
//如果没有其他进程创建这个事件,则重新创建,该事件为人工重置事件
hCounter = CreateEvent(NULL,TRUE,FALSE,”sam sp 44″);
}

//创建线程
HANDLE hThread[3];
printf(“test of manual rest event\n”);
CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadB,(void*)2);
CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadB,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待线程结束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
//关闭句柄
CloseHandle(hCounter);

if( (hCounter=OpenEvent(EVENT_ALL_ACCESS,FALSE,”sam sp 44″))==NULL)
{
//如果没有其他进程创建这个事件,则重新创建,该事件为自动重置事件
hCounter = CreateEvent(NULL,FALSE,FALSE,”sam sp 44″);
}
//创建线程
printf(“test of auto rest event\n”);
pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
pT2=AfxBeginThread((AFX_THREADPROC)threadB,(void*)2);
pT3=AfxBeginThread((AFX_THREADPROC)threadB,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待线程结束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
//关闭句柄
CloseHandle(hCounter);
}


从执行结果中我们可以看到在第二次执行时由于使用了自动重置事件threadB中只有一个线程能够等待到threadA中释放的事件对象。

在处理多进程/线程的同步问题时必须要小心避免发生死锁问题,比如说现在有两个互斥量A和B,两个线程tA和tB,他们在执行前都需要得到这两个互斥量,但现在这种情况发生了,tA拥有了互斥量A,tB拥有了互斥量B,但它们同时都在等待拥有另一个互斥量,这时候显然谁也不可能得到自己希望的资源。这种互相拥有对方所拥有的资源而且都在等待对方拥有的资源的情况就称为死锁。关于这个问题更详细的介绍请参考其他参考书。

在MFC中对于各种同步对象都提供了相对应的类

在这些类中封装了上面介绍的对象创建,打开,控制,删除功能。但是如果要使用等待功能则需要使用另外两个类:CSingleLock和CMultiLock。这两个类中封装了WaitForSingleObject和WaitForMultipleObjects函数。如果大家觉的需要可以看看这些类的定义,我想通过上面的介绍可以很容易理解,但是在对象同步问题上我觉得使用API函数比使用MFC类更为直观和方便。


 


版权所有 闻怡洋

线程(Thread)的概念在一些以前的操作系统中是不存在的例如以前的UNIX和Windows3.X,线程与进程的区别在于子线程与父线程序运行在同一进程空间内,而子进程和父进程则运行在不同的空间。这样一来同一进程内的不同线程间可以直接通过内存交换数据(出于数据同步原因最好不要这样做)。

此外在Win32的定义中一个进程至少拥有一个线程,所以进程也被叫做主线程。在上一节中创建进程时大家也看见了可以在获得进程句柄时也可以获得一个线程句柄。在Win32中线程有两种,窗口线程(GUI Thread)和工作线程(Worker Thread)。窗口线程将可以创建窗口,因为创建窗口后系统会为该窗口分配消息队列,而不会为工作线程分配消息队列,所以工作线程将消耗更少的系统资源。

我们先看看如何创建线程。

在MFC中提供了对线程功能的封装类,CWinThread我们常使用的CWinApp类就是从这个类派生的。通常我们使用CWinThread来创建窗口线程,过程如下:


  • 从CWinThread中派生新类。
  • 重载CWinThread::InitInstance()函数,在其中创建窗口并将窗口指针赋给m_pMainWnd。
  • 如果需要可以重载CWinThread::ExitInstance(),在窗口被销毁时该函数将会被调用。也就是说窗口线程的生命期是于窗口的生命器联系起来的。
下面是个简单的例子:
//线程类定义
class CGUIThread:public CWinThread
{
public:
CGUIThread();
virtual BOOL InitInstance(void);
virtual int ExitInstance(void);
};
CGUIThread::CGUIThread()
{
//设置自动删除
m_bAutoDelete = TRUE;
}
BOOL CGUIThread::InitInstance(void)
{
CWnd* pWnd= new
CWnd();pWnd->CreateEx(0,
AfxRegisterWndClass( CS_HREDRAW|CS_VREDRAW) ,
“gui thread window”,
WS_OVERLAPPEDWINDOW|WS_VISIBLE,
CRect(0,0,100,100),
NULL,
0);
m_pMainWnd=pWnd;
return (m_pMainWnd!=FALSE);
}
int CGUIThread::ExitInstance(void)
{
TRACE(“gui thread exit\n”);

return CWinThread::ExitInstance();
}
//创建GUI线程过程
void CSam_sp_43Dlg::OnGuiT()
{
CGUIThread *p_thread1= new
CGUIThread;p_thread1->CreateThread();
//使用默认参数,由于CGUIThread自动删除,所以不需要保存该指针
}


对于工作线程来讲只需要提供一个线程入口,也就是一个函数地址就可以了。在工作线程被启动后会转入该函数,并且函数退出时线程就会结束。在MFC中我们可以通过
CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );参数的含义如下:


  • pfunThreadProc:是函数入口地址,函数的原形应该如同:UINT MyControllingFunction( LPVOID pParam );
  • pParam:是传递给线程的参数。
  • nPriority:表明线程的优先级。常用的有THREAD_PRIORITY_IDLE,THREAD_PRIORITY_NORMAL,THREAD_PRIORITY_TIME_CRITICAL,THREAD_PRIORITY_ABOVE_NORMAL。
  • nStackSize:为栈大小,如果为0表示使用系统默认值。
  • dwCreateFlags:为创建线程时的标记,为CREATE_SUSPENDED表示线程被创建后被挂起。
  • lpSecurityAttrs:为安全属性。
函数的返回值是CWinThread类的指针,可以通过它实现对线程的控制。在线程函数返回时线程将被结束,在线程内部可以利用void AfxEndThread( UINT nExitCode );结束线程,nExitCode为退出码。
下面是个简单的例子:
//工作线程
UINT WorkerThread( LPVOID pParam )
{
//接收一个窗口类指针,然后设置窗口标题
CWnd *pstaTimer=(CWnd*)pParam;
for(int i=0;i<100;i++)
{
//TRACE(“thread %d\n”,i);
char szT[100];
sprintf(szT,”worker thread : %d”,i);
pstaTimer->SetWindowText(szT);
Sleep(10);
}
return 0; // 返回并退出线程
//或者调用void AfxEndThread( UINT nExitCode );来退出
}

//创建线程
void CSam_sp_43Dlg::OnWorkT()
{
//m_staTimer为CStatic 变量。传递窗口类指针
AfxBeginThread(WorkerThread,&m_staTimer);
}


在CWinThread类中通过DWORD CWinThread::SuspendThread( )和DWORD CWinThread::ResumeThread( )来挂起和恢复线程运行。通过int CWinThread::GetThreadPriority( )和BOOL CWinThread::SetThreadPriority( int nPriority )来获取和设置线程优先级。

此外CWinThread类中的成员变量:m_hThread和m_nThreadID保存了线程句柄和线程ID号。也可以通过其他API函数对线程进行操作。

API函数BOOL TerminateThread( HANDLE hThread,DWORD dwExitCode );可以在线程外部强制结束一个线程,但这样做是有危险的,因为线程申请某些资源可能没法释放,而且也有可能引起进程的崩溃。所以推荐的方法为设置一个标记当线程检测到后自己退出,而不是采用从外部强制结束线程的方法。


版权所有 闻怡洋

 

进程控制的意义在于可以创建一个进程,并可以通过进程句柄结束进程。

创建进程的函数为CreateProcess,该函数比较复杂共有十个参数。

BOOL CreateProcess(
LPCTSTR
lpApplicationName, // 执行程序文件名
LPTSTR lpCommandLine, // 参数行
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 进程安全参数
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程安全参数
BOOL bInheritHandles, // 继承标记
DWORD dwCreationFlags, // 创建标记
LPVOID lpEnvironment, // 环境变量
LPCTSTR lpCurrentDirectory, // 运行该子进程的初始目录
LPSTARTUPINFO lpStartupInfo, // 创建该子进程的相关参数
LPPROCESS_INFORMATION lpProcessInformation // 创建后用于被创建子进程的信息
);

lpApplicationName:为执行程序的文件名,如果在创建进程时要使用参数,则该参数可以为NULL。


lpCommandLine:为参数行,如果无参数可以为NULL,在有参数传递给进程时如下设置:lpApplicationName=NULL;lpCommandLine=para,例如lpCommandLine=”c:\\windows\\notepad.exe c:\\autoexec.bat”。


lpProcessAttributes,lpThreadAttributes分别描述了创建的进程和线程安全属性,如果使用NULL表示使用默认的安全描述。


bInheritHandles:表示当前进程中的打开的句柄是否能够被创建的子进程所继承。


dwCreationFlags:表示创建标记,通过该标记可以设置进程的创建状态和优先级别。常用的有下面的标记:



  • CREATE_NEW_CONSOLE:为子进程创建一个新的控制台。
  • CREATE_SUSPENDED:子进程在创建时为挂起状态。
  • HIGH_PRIORITY_CLASS/NORMAL_PRIORITY_CLASS:高/普通优先级别。

lpEnvironment:表示子进程所使用的环境变量,如果为NULL,则表示与当前进程使用相同的环境变量。


lpCurrentDirectory:表示子进程运行的初始目录。


lpStartupInfo:用于在创建子进程时设置各种属性。该结构定义如下:

typedef struct _STARTUPINFO { // si
DWORD cb; //结构长度
LPTSTR lpReserved; //保留
LPTSTR lpDesktop; //保留
LPTSTR lpTitle; //如果为控制台进程则为显示的标题
DWORD dwX; //窗口位置
DWORD dwY; //窗口位置
DWORD dwXSize; //窗口大小
DWORD dwYSize; //窗口大小
DWORD dwXCountChars; //控制台窗口字符号宽度
DWORD dwYCountChars; //控制台窗口字符号高度
DWORD dwFillAttribute; //控制台窗口填充模式
DWORD dwFlags; //创建标记
WORD wShowWindow; //窗口显示标记如同ShowWindow中的标记
WORD cbReserved2; //
LPBYTE lpReserved2; //
HANDLE hStdInput; //标准输入句柄
HANDLE hStdOutput; //标准输出句柄
HANDLE hStdError; //标准错误句柄
} STARTUPINFO, *LPSTARTUPINFO;

如果要使结构中相关的分量起作用,必须正确的设置dwFlags。例如:dwFlags包含STARTF_USESIZE表示dwXSize和dwYSize有效,包含STARTF_USEPOSITION表示dwX和dwY有效。


lpProcessInformation:用来在进程创建后接收相关信息,该结构由系统填写。

typedef struct _PROCESS_INFORMATION { // pi
HANDLE hProcess; //进程句柄
HANDLE hThread; //进程的主线程句柄
DWORD dwProcessId; //进程ID
DWORD dwThreadId; //进程的主线程ID
} PROCESS_INFORMATION;

在本节提供的例子中使用下面的代码创建新进程:

	PROCESS_INFORMATION pi;
STARTUPINFO si;
si.cb=sizeof(si);
si.wShowWindow=SW_SHOW;
si.dwFlags=STARTF_USESHOWWINDOW;
BOOL fRet=CreateProcess(NULL,
“c:\\windows\\notepad.exe c:\\autoexec.bat”,
NULL,
NULL,
FALSE,
NORMAL_PRIORITY_CLASS|CREATE_NEW_CONSOLE,
NULL,
NULL,
&si,
&pi);
if(success)
{
m_hPro=pi.hProcess;//保存当前进程句柄,在强制结束进程时使用。
}

如果要结束进程需要知道进程的句柄,在上面的例子中我们已经保存了pi.hProcess。


结束一个进程所使用的函数为:

BOOL TerminateProcess(
HANDLE
hProcess, // 进程句柄
UINT uExitCode // 退出代码
);

本节的例子中使用下面的代码来结束进程。

	if(m_hPro)
{
if(!TerminateProcess(m_hPro,0)) //结束代码为0
{
reportError(…);
}
else
{
AfxMessageBox(“TerminateProcess成功”);
}
m_hPro=NULL;
}
else
{
AfxMessageBox(“m_hPro为空”);
}

在进程内结束进程的方法为调用:VOID ExitProcess( UINT uExitCode )建议在进程内部进行退出,因为进程被强制结束时可能一些DLL不能被正确卸载。


退出代码可以在其他进程中通过调用GetExitCodeProcess获得。

BOOL GetExitCodeProcess(
HANDLE
hProcess, // handle to the process
LPDWORD lpExitCode // address to receive termination status
);

如果进程尚未退出,函数将会返回STILL_ACTIVE。


在以前的Windows3.X时代,我们使用

UINT WinExec(
LPCSTR
lpCmdLine, // 命令行
UINT uCmdShow // 窗口显示方式
);

现在仍然可以使用这个函数但我们可以看到我们在运行程序后无法得到该程序的各种句柄。所以建议使用CreateProcess创建进程。

 

还记得DOS时代有一个程序被大家奉为后台操作的经典,那就是Print.EXE(由M$提供),这个程序用于后台打印。可以从一定程度上实现了多任务,但是DOS并不是一个多任务的环境所以勉强实现多任务时限制太多。随后有了Windows 3.X,虽然OS有了多任务的支持但是严格的说来对多进程的支持并不够,这主要表现在进程间通信方面提供的支持非常少。一些传统的IPC方式都没有提供。后来在WinNT上完全实现了多进程/多线程支持,当然现在的Windows9X/2K都完全提供了这方面的支持。


什么是进程(Process):普通的解释就是,进程是程序的一次执行,而什么是线程(Thread),线程可以理解为进程中的执行的一段程序片段。在一个多任务环境中下面的概念可以帮助我们理解两者间的差别:


  • 进程间是独立的,这表现在内存空间,上下文环境;线程运行在进程空间内。
  • 一般来讲(不使用特殊技术)进程是无法突破进程边界存取其他进程内的存储空间;而线程由于处于进程空间内,所以同一进程所产生的线程共享同一内存空间。(图一)
  • 同一进程中的两段代码不能够同时执行,除非引入线程。
  • 线程是属于进程的,当进程退出时该进程所产生的线程都会被强制退出并清除。
  • 线程占用的资源要少于进程所占用的资源。
  • 进程和线程都可以有优先级。
  • 在线程系统中进程也是一个线程。可以将进程理解为一个程序的第一个线程。


图一

一个最简单的例子就是在屏幕上画多个跳动的小球,我们对每个球的绘制都可以采用一个线程来完成。但是象这样的线程间完全独立没有影响没有数据交换的情况是很少的。

下面我们看一个例子,一个应用要完成两个任务:每次产生1000个随机数写入文件并从文件中读出数据并以该随机数为圆心画圆,对该操做进行100次,并使用100个不同的文件保存文件。传统做法如下:

void do_this(void)
{
for(int i=0;i<100;i++)
{
/// step 1
generate 1000 randam number;
write to file;
/// step 2
read from file;
draw circle;
}
}

如果引入多进程的概念,则实现方法可以改为:

void do_this(void)
{
CreateProcess(“do_rand.exe”,…);
CreateProcess(“draw_circle.exe”,…);
}
//do_rand.exe
void do_rand(void)
{
for(int i=0;i<100;i++)
{
/// step 1
generate 1000 randam number;
write to file;
wait draw_circle finish last task
tell draw_cricle data ready

}
}
//draw_circle.exe
void draw_circle(void)
{
for(int i=0;i<100;i++)
{
/// step 2
set flag of last task finish
wait data ready

read from file;
draw circle;
}
}

在多进程中我们引入了更多的控制手段,首先do_rand在准备好数据后必须等待draw_circle处于空闲状态,这样做的原因是只有一个进程在进行画圆操作,所以必须保证当前提交的data ready请求能够被接收。在图二中我们可以看到用红框内的代码会在同时执行,由于使用了不同的文件所以不需要对文件的使用情况也进行判断。

图二

如果使用线程,我们可以进一步的改造程序,我们取消使用文件来保存数据,而是全局变量来保存数据:

void do_this(void)
{
CreateThread(“do_rand”,…);//参数为线程入口而不是执行程序
CreateThread(“draw_circle”,…);
}

global int giRandNum[1000];

void do_rand(void)
{
for(int i=0;i<100;i++)
{
/// step 1
local int iRandNum[1000];
generate 1000 randam number;

get access of giRandNum;
memcpy(giRandNum,iRandNum,…);
release access of giRandNum;

wait draw_circle finish last task
tell draw_cricle data ready
}
}

void draw_circle(void)
{
for(int i=0;i<100;i++)
{
/// step 2
set flag of last task finish
wait data ready
local int iRandNum[1000];

get access of giRandNum;
memcpy(iRandNum,giRandNum,…);
release access of giRandNum;

draw circle;
}
}


在这里我们使用全局变量来保存数据,而且程序使用的资源要小于前面使用进程的情况,而且效率是相同的。在这里我们由引入了对全局数据使用情况的判断,这是因为保证全局数据在被draw_circle读取的时候不会被do_rand修改。这就是一个数据同步的概念,实现数据同步的方法在4 进程/线程间同步会详细讲解。

通过上面的例子可以看出使用多线程时可以提高效率又能够节省资源。

最后一点线程在单CPU主机上与多进程相比是没有的效率上的提高,而在多CPU的主机上不同的线程代码可以分配到不同的主机上执行。但多进程/线程与单进程相比的在效率和速度上的优点很很明显的。

随着多线进程/程序的采用同时也会产生很多其他的问题比如数据如何交换(在上面的例子中我们使用文件来保存中间数据,当然还有很多的方法来在进程间交换数据),数据如何同步(保证某些数据在同时只被一段代码进行写操作),如何协调进程/线程间的操作(一个进程的继续执行是否要等待其他进程完成某些操作)。