首页 > 编程语言 >线程同步与异步套接字编程

线程同步与异步套接字编程

时间:2023-06-11 13:32:06浏览次数:27  
标签:异步 addr int 线程 DWORD 接字 NULL 指针


事件对象
时间对象也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是为通知状态的布尔值
有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件,当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。

HANDLE CreateEvent(
      LPSECURITY_ATTRIBUTES lpEventAttributes,
                          // pointer to security attributes,指定安全属性
      BOOL bManualReset,     // flag for manual-reset event
 //为TRUE表示手动,否则为自动,看上边文字说明
      BOOL bInitialState, // flag for initial state
 //为TRUE表示有信号
      LPCTSTR lpName         // pointer to event-object name
 );


事件对象代码如下(经本人修改,此代码与视频源码有一些差别):

#include "windows.h"
 #include "iostream.h"int ticket=100;
 HANDLE g_hEvent;DWORD WINAPI Fun1Proc(LPVOID lpParameter);
 DWORD WINAPI Fun2Proc(LPVOID lpParameter);void main()
 {
 HANDLE thread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
 HANDLE thread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);CloseHandle(thread1);
 CloseHandle(thread2);

g_hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);//第三个参数为TRUE,这时创建时需要在有信号状态下WaitForSingleObject才能获得信号进行工作,也可以调用SetEvent()来指定对象为有信号状态。当人工重置事件被调度时,所有线程都变成可调度线程。对象,一般采用自动重置。第二个参数为FALSE。但自动重置对象为等待该事件的线程中只有一个线程变为可调度线程。WaitForSingleObject就变成非信号状态了,完成处理后需要将事件量置成有信号状态SetEvent()。

Sleep(4000);
 CloseHandle(g_hEvent);//最后关闭事件对象句柄。}
 DWORD WINAPI Fun1Proc(LPVOID lpParameter)
 {
 WaitForSingleObject(g_hEvent,INFINITE);

 while(TRUE)
 {
      WaitForSingleObject(g_hEvent,INFINITE);
      if(tickets>0)
      {
       Sleep(1);
       cout<<"thread1 sell ticket : "<<tickets--<<endl;
      }
      else
       break;
      SetEvent(g_hEvent);
 }

 return 0;
 }
 DWORD WINAPI Fun2Proc(LPVOID lpParameter)
 {
 WaitForSingleObject(g_hEvent,INFINITE);
 while(TRUE)
 {
      WaitForSingleObject(g_hEvent,INFINITE);
      if(tickets>0)
      {
       Sleep(1);
       cout<<"thread2 sell ticket : "<<tickets--<<endl;
      }
      else
       break;
      SetEvent(g_hEvent);
 }

 return 0;
 }


人工重置的事件只有在调用ResetEvent()之后才变成无效状态,否则始终处于有信号状态。而自动重置信号在调用WaitForSingleObject之后就处于无信号状态了。

通过命名事件量来实现一个应用程序只有一个实例在运行。
只有当 g_hEvent=CreateEvent(NULL,FALSE,TRUE,"ticket");第四个参数有值时才能进行事件对象的判断:

if(g_hEvent)
 {
      if(ERROR_ALREADY_EXISTS==GetLastError())
      {
       cout<<"Only one instance can runs!"<<endl;
       return;
      }
 }

关键代码段
其他方式都工作在内核方式下中。
关键代码段(临界区)工作在用户方式下。
关键代码段(临界区)是指一个小代码段,在代码能够执行前,它必须独占对某资源的访问权。临界区有点像公用电话亭。

VOID InitializeCriticalSection(
      LPCRITICAL_SECTION lpCriticalSection      // address of critical 
                                             // section object
 );//初始化一个临界区对象,也就是创建了临界区。
 VOID DeleteCriticalSection(
      LPCRITICAL_SECTION lpCriticalSection      // pointer to critical 
                                             // section object
 );//删除临界区VOID EnterCriticalSection(
      LPCRITICAL_SECTION lpCriticalSection      // pointer to critical 
                                             // section object
 );如果得到了临界区的访问权,就返回,否则,一直等待下去。
 VOID LeaveCriticalSection(
      LPCRITICAL_SECTION lpCriticalSection      // address of critical 
                                             // section object
 );//离开临界区。

原代码如下:

#include "windows.h"
 #include "iostream.h"int ticket=100;
 DWORD WINAPI Fun1Proc(LPVOID lpParameter);
 DWORD WINAPI Fun2Proc(LPVOID lpParameter);CRITICAL_SECTION g_cs;//全局临界区对象
 void main()
 {
 HANDLE thread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
 HANDLE thread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);CloseHandle(thread1);
 CloseHandle(thread2);
 InitializeCriticalSection(&g_cs);Sleep(4000);
DeleteCriticalSection(&g_cs);//释放资源
}
 DWORD WINAPI Fun1Proc(LPVOID lpParameter)
 { 
 while(TRUE)
 {
      EnterCriticalSection(&g_cs);//进入
      if(ticket>0)
      {
       cout<<"thread1 sells : "<<ticket--<<endl;
       Sleep(1);
      }
      else break;
      LeaveCriticalSection(&g_cs);//推出
 }
 return 0;
 }DWORD WINAPI Fun2Proc(LPVOID lpParameter)
 { 
 while(TRUE)
 {
      EnterCriticalSection(&g_cs);
      if(ticket>0)
      {
       cout<<"thread2 sells : "<<ticket--<<endl;
       Sleep(1);
      }
      else break;
      LeaveCriticalSection(&g_cs);
 }
 return 0;
 }

互斥对象、事件对象与关键代码段的比较
互斥对象和事件对象属于内核对象,利用内核对象进行线程同步,速度较慢,但利用互斥对象和事件对象这样的内河对象,可以在多个进程中的各个线程间进行同步。
关键代码段时工作在用户方式下,同步速度较快,但在使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值

在MFC中进行线程同步,首选临界区代码段,在一个类的构造函数中调用InitCriticalSection(),在析构函数中调用DeleteCriticalSection(),在我们要访问的代码前面加上EnterCriticalSection,在离开时调用LeaveCriticalSection,这两个一定要成对使用。

《window内核编程》——机械工业出版社

基于消息的异步套接字
Windows套接字在两种模式下执行I/O操作,阻塞和非阻塞。在阻塞模式下,在I/O操作完成前,执行操作的Winsock函数会一直等待下去,不会立即返回程序(将控制权交还给程序),所以在前面聊天程序中需要创建一个单独的线程接收消息。而在非阻塞模式下,Winsock函数无论如何都会立即返回。
Windows Sockets为了支持Windows消息驱动机制,使应用程序开发者能够方便地处理网络通信,它对网络事件采用了基于消息的异步存取策略。

Windows Sockets的异步选择函数WSAAsyncSelect()提供了消息机制的网络事件选择,当使用它登记的网络事件发生时,Windows应用程序相应的窗口函数将收到一个消息,消息中指示了发生的网络事件,以及与事件相关的一些信息。自定义消息。

Win32平台支持多种不同的网络协议,采用Winsock2就可以编写可直接使用任何一种协议的网络应用程序了。通过WSAEnumProtocols可以获得系统中安装的网络协议的的相关信息。

int WSAEnumProtocols (
      LPINT lpiProtocols,         //若为NULL,返回所有可用协议信息,
 //否则返回数组中所列协议信息。
      LPWSAPROTOCOL_INFO lpProtocolBuffer,     //WSAPROTOCOL_INFO用来存放或得              //到一个指定协议的完整信息
      ILPDWORD lpdwBufferLength        //指定传递给函数的缓冲区长度
 );SOCKET WSASocket (
      int af,                             
      int type,                           
      int protocol,                       
      LPWSAPROTOCOL_INFO lpProtocolInfo,  
      GROUP g,                            
      DWORD dwFlags                       
 );


lpProtocolInfo是指向WSAPROTOCOL_INFO结构体的指针,该结构定义了所创建的套接字的特性。如果为NULL,WinSock2.DLL使用前三个参数来决定使用哪一个服务提供者,他选择能够支持规定的抵制族、套接字类型和协议值的第一个传输提供者。如果不为NULL,则套接字绑定到与指定的结构WSAPROTOCOL_INFO相关的提供者

调用WSAAsyncSelect请求一个windows的基于消息的网络事件通知

int WSAAsyncSelect (
      SOCKET s,           
      HWND hWnd,          
      unsigned int wMsg,  
      long lEvent         
 );

接收数据:

int WSARecvFrom (
      SOCKET s,        //标识套接字的描述符
      LPWSABUF lpBuffers,      //指向WSABUF的指针,每一个包含WSABUF包含一个           //缓冲区的指针和缓冲区的长度
      DWORD dwBufferCount,     //lpBuffers数组中WSABUF结构体的长度
      LPDWORD lpNumberOfBytesRecvd,     //如果接收操作立即完成,则为指向本次调用接受            //字节数的指针
      LPDWORD lpFlags, 
      struct sockaddr FAR * lpFrom,     //指向重叠操作完成后存放原地址的缓冲区
      LPINT lpFromlen,      //指向From缓冲区大小的指针,制定了 lpFrom才需要
      LPWSAOVERLAPPED lpOverlapped, //指向WSAOVERLAPPED
      LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE  
 //指向接收操作完成时调用的完成例程的指针
 );

为了调用高版本的套接字,我们要调用WSAStartup来制定套接字版本:

在BOOL CChatApp::InitInstance()中添加代码如下:
WORD wVersionRequested;
 WSADATA wsaData;
 int err;

 wVersionRequested = MAKEWORD( 2, 2 );

 err = WSAStartup( wVersionRequested, &wsaData );
 if ( err != 0 ) 
 {
      return FALSE;
 }

 if ( LOBYTE( wsaData.wVersion ) != 2 ||
            HIBYTE( wsaData.wVersion ) != 2 ) 
 {
      WSACleanup( );
      return FALSE; 
 }


还需要在stdafx.h中添加头文件<winsock2.h>和链接库文件sc2_32.lib

在ChatApp类当中添加析构函数~CChatApp()中终止对套接字库的使用。

CChatApp::~CChatApp()
 {
 WSACleanup();
 }

在CChatDlg类中增加成员变量SOCKET::m_socke并在构造函数中初始化。

在CChatDlg中增加析构函数,关闭套接字:

CChatDlg::~CChatDlg()
 {
 if(m_socket)//判有没有断其存不存在
 closesocket(m_socket);
 }

增加一个初始化库的成员函数:

BOOL CChatDlg::InitSocket();
SOCKET WSASocket( int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags );


前三个参数和socket()函数的前三个参数含义一样。
lpProtocolInfo,一个指向WSAPROTOCOL_INFO结构体的指针,该结构定义了所创建的套接字的特性。如果lpProtocolInfo为NULL,则WinSock2 DLL使用前三个参数来决定使用哪一个服务提供者,它选择能够支持规定的地址族、套接字类型和协议值的第一个传输提供者。如果lpProtocolInfo不为NULL,则套接字绑定到与指定的结构WSAPROTOCOL_INFO相关的提供者。
g,保留的。
dwFlags,套接字属性的描述。

int WSARecvFrom( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, struct sockaddr FAR *lpFrom, LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
s,标识套接字的描述符。
lpBuffers,[in, out],一个指向WSABUF结构体的指针。每一个WSABUF结构体包含一个缓冲区的指针和缓冲区的长度。
dwBufferCount, lpBuffers数组中WSABUF结构体的数目。
lpNumberOfBytesRecvd,[out],如果接收操作立即完成,则为一个指向本次调用所接收的字节数的指针。
lpFlags,[in, out],一个指向标志位的指针。
lpFrom,[out],可选指针,指向重叠操作完成后存放源地址的缓冲区。
lpFromlen,[in, out],指向from缓冲区大小的指针,仅当指定了lpFrom才需要。
lpOverlapped,一个指向WSAOVERLAPPED结构体的指针(对于非重叠套接字则忽略)。
lpCompletionRoutine,一个指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)。

int WSASendTo( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const struct sockaddr FAR *lpTo, int iToLen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
s,标识一个套接字(可能已连接)的描述符。
lpBuffers,一个指向WSABUF结构体的指针。每一个WSABUF结构体包含一个缓冲区的指针和缓冲区的长度。
dwBufferCount, lpBuffers数组中WSABUF结构体的数目。
lpNumberOfBytesSent,[out],如果发送操作立即完成,则为一个指向本次调用所发送的字节数的指针。
dwFlags,指示影响操作行为的标志位。
lpTo,可选指针,指向目标套接字的地址。
iToLen,lpTo中地址的长度。
lpOverlapped,一个指向WSAOVERLAPPED结构的指针(对于非重叠套接字则忽略)。
lpCompletionRoutine,一个指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)。

接下来编写函数初始化套接字,步骤如下:
1。新建套接字
2。新建地址。
3。绑定
4。请求一个windows的基于消息的网络事件通知
5。在BOOL CChatDlg::OnInitDialog()中调用BOOL CChatDlg::InitSocket()
代码如下:

BOOL CChatDlg::InitSocket()
 {
 m_socket=WSASocket(AF_INET,SOCK_DGRAM,0,NULL,0,0);
 if(INVALID_SOCKET=m_socket)
 {
      MessageBox("套接字创建失败");
      return FALSE;
 }//Socket增强版
 SOCKADDR_IN addrSock;
 addrSock.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
 addrSock.sin_family=AF_INET;
 addrSock.sin_port=htons(6000);if(SOCKET_ERROR==bind(m_socket,(SOCKADDR*)&addrSock,sizeof(SOCKADDR)))
 {
      MessageBox("绑定失败!");
      return FALSE;
 }if(SOCKET_ERROR==WSAAsyncSelect(m_socket,m_hWnd,UM_SOCK,FD_READ))
 {
      MessageBox("注册网络读取事件失败!");
      return FALSE;
 }
 }//该函数InitSocket()在InitInstance中被调用。

自定义消息响应函数步骤:
1。在ChatDlg.h中定义#define UM_SOCK     WM_USER+1
2。在

//{{AFX_MSG(CChatDlg)
 virtual BOOL OnInitDialog();
 afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
 afx_msg void OnPaint();
 afx_msg HCURSOR OnQueryDragIcon();
 //}}AFX_MSG
 DECLARE_MESSAGE_MAP()
 中添加
 afx_msg LRESULT OnSock(WPARAM wParam,LPARAM lParam);
 //注意返回值类型
 3。在
 BEGIN_MESSAGE_MAP(CChatDlg, CDialog)
 //{{AFX_MSG_MAP(CChatDlg)
 ON_WM_SYSCOMMAND()
 ON_WM_PAINT()
 ON_WM_QUERYDRAGICON()
 //}}AFX_MSG_MAP
 END_MESSAGE_MAP()
 中添加消息映射
 ON_MESSAGE(UM_SOCK,OnSock)     //此处不加标点


4。实现消息响应函数

LRESULT CChatDlg::OnSock(WPARAM wParam,LPARAM lParam)
 {


switch(LOWORD(lParam))//在消息响应函数中判断消息的类型。LOWORD(lParam)表明网络事件HIWORD(lParam)标明错误代码。

{
 case FD_READ:
      WSABUF wsabuf;
      wsabuf.buf=new char[200];
      wsabuf.len=200;
      DWORD dwRead;
      DWORD dwFlag=0;
      SOCKADDR_IN addrFrom;
      int len=sizeof(SOCKADDR);     CString strTemp;
      CString str;
   
      if(SOCKET_ERROR==WSARecvFrom(m_socket,&wsabuf,1,&dwRead,&dwFlag,
          (SOCKADDR*)&addrFrom,&len,NULL,NULL))
      {
       MessageBox("接收数据失败!");
       return FALSE;
      }
      str.Format("%s speak : %s",inet_ntoa(addrFrom.sin_addr),wsabuf.buf);
      str+="\r\n";
      GetDlgItemText(IDC_EDIT_RECV,strTemp);
      str+=strTemp;
      SetDlgItemText(IDC_EDIT_RECV,str);     break;
 }
 return 0;
 }

接收按钮消息:

void CChatDlg::OnBtnSend() 
 {
 DWORD dwIP;
 CString strSend;
 WSABUF wsabuf;
 DWORD dwSend;
 int len;
 CString strHostName;
 SOCKADDR_IN addrTo;
 HOSTENT* pHost;
 if(GetDlgItemText(IDC_EDIT_HOSTNAME,strHostName),strHostName=="")
 {
      ((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);
      addrTo.sin_addr.S_un.S_addr=htonl(dwIP);
 }
 else
 {
      pHost=gethostbyname(strHostName);
      addrTo.sin_addr.S_un.S_addr=*((DWORD*)pHost->h_addr_list[0]);
 }

 addrTo.sin_family=AF_INET;
 addrTo.sin_port=htons(6000);GetDlgItemText(IDC_EDIT_SEND,strSend);
 len=strSend.GetLength();
 wsabuf.buf=strSend.GetBuffer(len);//将cstring对象作为一个char*返回
 wsabuf.len=len+1;SetDlgItemText(IDC_EDIT_SEND,"");
if(SOCKET_ERROR==WSASendTo(m_socket,&wsabuf,1,&dwSend,0,
       (SOCKADDR*)&addrTo,sizeof(SOCKADDR),NULL,NULL))
 {
      MessageBox("发送数据失败");
      return;
 }

 }

注意,此程序的接收端和发送端是在同一个线程下完成的,如果我们采用阻塞套接字会因为接收函数的调用而使主线程暂停运行。这样我们采用异步选择的机制完成了主线程的接收端和发送端

编写网络程序采用异步选择机制可以提高系能。

如果我们不想总是输入IP地址而是想输入主机名,可以调用函数

struct hostent* FAR gethostbyname(
      const char* name
 );//将主机名转化为IP地址。


在对话框上新建一个文本框
代码:

HOSTENT * pHost;
GetDlgItemText(IDC_EDIT_HOSTNAME,strHostName);
 if(strHostName=="")
 {
      ((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS))->GetAddress(dwIP);
      addrTo.sin_addr.S_un.S_addr=htonl(dwIP);
 }
 else
 {
      pHost=gethostbyname(strHostName);
      addrTo.sin_addr.S_un.S_addr=*((DWORD*)pHost->h_addr_list[0]);
 }


用DWORD取出的四个字节,正好是网络字节序表示的ULONG类型的地址。
这段代码在教学视频上在文本框中输入的是“sunxin”,我认为那是那台电脑的在网络上的主机名,而在我的电脑上只能输入“localhost”,或者我的主机名,否则程序异常中止。

如果要在接收数据框中显示主机名,在LRESULT CChatDlg::OnSock(WPARAM wParam,LPARAM lParam)中添加如下代码:
HOSTENT *pHost; pHost=gethostbyaddr((char*)&addrFrom.sin_addr.S_un.S_addr,4,AF_INET);
str.Format("%s speak : %s",pHost->h_name,wsabuf.buf);
注意:指针之间可以任意转换,以上代码把一个ulong类型转换为char*,要先取地址再进行转换。

记住两个函数
The inet_ntoa function converts an (Ipv4) Internet network address into a string in Internet standard dotted format.

The inet_addr function converts a string containing an (Ipv4) Internet Protocol dotted address into a proper address for the IN_ADDR structure.


标签:异步,addr,int,线程,DWORD,接字,NULL,指针
From: https://blog.51cto.com/u_130277/6457577

相关文章

  • 5.28学习总结thread多线程理解
    多线程早在大二刚来的时候就听王建民老师提到过,但是当时觉得多线程肯定很难,而且现在也用不到,就没有接触。现在看来多线程的学习还是比较简单的。下面演示代码均为PythonfromthreadingimportThreadth=thread(target=,args=())#target指向新线程执行的目标函数,args中......
  • 面试官:在项目中,你是如何使用线程池的?
    大家好,我是田哥前两天,有位星友(知识星球里的朋友简称)私信我,问在项目中如何使用线程池,关于线程池的原理和八股文相关的都可以背,但是要是问到你们项目中是怎么用的,心里总是有点慌。公众号里回复77,获取面试小抄和面试相关资源:话不多说,我们直接步入正题。创建线程池的方式我在这篇文章......
  • Java基础语法(二十):创建线程
    前言在计算机科学中,多线程是指在单个程序中同时执行多个线程。Java是一种支持多线程编程的语言,Java中的线程可以通过继承Thread类或实现Runnable接口来创建。本文将介绍Java多线程的基本概念和如何创建线程。介绍在Java中,线程是一种轻量级的进程,它可以与其他线程共享同一个进程的内......
  • (2023.6.10)线程绑定到指定核上
    pthread_setaffinity_np与sched_setaffinity的区别:sched_setaffinity可在进程的线程中去修改亲和性写在启动脚本中是使用pthread_setaffinity_np、sched_setaffinity、还是tasklet?(https://www.cnblogs.com/x_wukong/p/5924298.html)c语言如何调用到系统命令reboot? 同时在......
  • QT多线程(线程互斥)
    (文章目录)前言线程互斥是指在多线程并发执行时,为避免多个线程访问共享资源时发生冲突而采取的一种机制。本篇文章我们就这个问题来了解一下什么叫线程互斥,又如何解决线程互斥的问题。一、导致问题产生的原因和解决方法如果多个线程同时访问同一共享资源,可能会导致数据不一致......
  • QT多线程基础
    (文章目录)前言本篇文章来讲解一下QT中的多线程使用方法。其实线程这个概念对于我们来说并不陌生,main函数在多线程中一般就被称为主线程。在QT中,使用QThread类可以方便地创建新的线程并在其中执行任务。以下介绍一些常用的QT多线程的技术和方法。一、多线程概念介绍多线程是......
  • 网站加速,AdapterMan 是基于 Workerman 的高性能 PHP 异步网络编程框架,可以用于加速任
    AdapterMan是基于Workerman的高性能PHP异步网络编程框架,可以用于加速任意项目。下面是使用AdapterMan进行加速的详细步骤:1.安装AdapterMan:composerrequireadapterman/adapterman 2.创建一个PHP文件,例如`index.php`,并编写以下代码:require_once__DIR__.......
  • Python多线程编程的一个掉进去不太容易爬出来的坑
    原文复制过来很多图片不能显示,发个链接吧。是使用Python+Socket编程模拟FTP工作原理的代码,多线程会引入一个坑,使用多进程不存在这个问题。原文地址 ......
  • 详解Python线程对象daemon属性对线程退出的影响
    进程、线程的概念以及多线程编程的基础知识请参考文末给出的方式在公众号历史文章中查找相关文章进行阅读。本文重点介绍线程对象daemon属性在线程退出时产生的作用和影响。首先,我们来看一下官方文档对守护线程(daemonthread)的描述:再来看一下官方文档对线程对象daemon属性的描述:可......
  • C++的多线程编程(练习一下condition_variable)
        嗯,高考结束了,那就编写一个阅卷和查成绩的多线程吧。一个线程老师阅卷,其他三个线程查成绩。代码如下:    1#include<iostream>2#include<thread>3#include<mutex>4#include<condition_variable>5#include<chrono>6#include<futu......