|
when I used the CThreadPool class, I encapsulate it in a dll.But i used it ina another all,there has be a error when the CThreadPool object was released.I traked it,the error occur in ~CCriticalSection().That is it:
User breakpoint called from code at 0x7c921230
HEAP[****.exe]: Invalid Address specified to RtlValidateHeap( 00F00000, 5F4D0AE6 )
I know the first error whie a invalid pointer was deleted.Maybe the Desconstrcut of CThreadPool was used twice or more.But i can make sure the Desconstrcut of CThreadPool was used once.And strangely,after when i change "CCriticalSection m_arrayCs" into "CRITICAL_SECTION m_arrayCs",the progamme runs with no error.Of course I add "InitializeCriticalSection(&m_arrayCs);" in CThreadPool() and dd "DeleteCriticalSection(&m_arrayCs);" in ~CThreadPool().
|
|
|
|
|
In some case,the menber function "Stop" may be used twice and more,the first using "Stop" has close all I/0 Completion Portand and teminated all thread,so when using "Stop" agin,"::PostQueuedCompletionStatus(m_hWorkerIoPort, 0, 0, (OVERLAPPED*)0xFFFFFFFF);" will be not handled,and "DWORD rc=WaitForMultipleObjects(nCount, pThread, TRUE, 120000);" will be waited for 2 minute,it's not appropriate.I think the "CThreadPool::Stop()" like this:
void CThreadPool::Stop(bool bHash)
{
CSingleLock singleLock(&m_arrayCs);
singleLock.Lock(); // Attempt to lock the shared resource
if(m_hMgrIoPort != NULL)
{
EnterCriticalSection(&m_arrayCs);
::PostQueuedCompletionStatus(m_hMgrIoPort, 0, 0, (OVERLAPPED*)0xFFFFFFFF);
WaitForSingleObject(m_hMgrThread, INFINITE);
CloseHandle(m_hMgrIoPort);
m_hMgrIoPort = NULL;
LeaveCriticalSection(&m_arrayCs);
}
if(m_hWorkerIoPort != NULL)
{
EnterCriticalSection(&m_arrayCs);
//shut down all the worker threads
UINT nCount=m_threadMap.GetCount();
HANDLE* pThread = new HANDLE[nCount];
long n=0;
POSITION pos=m_threadMap.GetStartPosition();
DWORD threadId; ThreadInfo info;
while(pos!=NULL)
{
::PostQueuedCompletionStatus(m_hWorkerIoPort, 0, 0, (OVERLAPPED*)0xFFFFFFFF);
m_threadMap.GetNextAssoc(pos, threadId, info);
pThread[n++]=info.m_hThread;
}
DWORD rc=WaitForMultipleObjects(nCount, pThread, TRUE, 120000);//wait for 2 minutes, then start to kill threads
CloseHandle(m_hWorkerIoPort);
m_hWorkerIoPort = NULL;
if(rc==WAIT_TIMEOUT&&bHash)
{
//some threads not terminated, we have to stop them.
DWORD exitCode;
for(int i=0; i<ncount;> if (::GetExitCodeThread(pThread[i], &exitCode)==STILL_ACTIVE)
TerminateThread(pThread[i], 99);
}
delete[] pThread;
}
singleLock.Unlock();
}
|
|
|
|
|
I have read many articles on thread-pools on this site. Some use IOCP - some don't. However none of them discuss WHY you might want to use IOCP. Yes IOCP is quite easy to use but there are many other ways to implement a thread pool.
There is a very good reason for using IOCP and it should be explained!
|
|
|
|
|
|
when i use the code,have some memory leaks
|
|
|
|
|
From VS7 and up, you can use the internal class ATL::CThreadPool<t>. The worker archetype that is passed as a template argument has only to provide three public methods and one typedef for the worker items. It is a very versatile, easy and fast class. When programming for Windows, use this class instead.
Cheers
|
|
|
|
|
Hi,
First, many thanks for sharing your code.
I think there is a little bug when WorkerProc process RemoveThread message (0xFFFFFFFE)
Currently, when the thread receives this messages, the While boucle breaks and the thread is terminated. We dont knows if ProcessJob has finished or not.
So, I've fixed this like that :
unsigned int CThreadPool::WorkerProc(void* p)
{
{
...
bool bBusy = false;
...
while(::GetQueuedCompletionStatus(IoPort, &pN1, &pN2,
&pOverLapped, INFINITE ))
{
if(pOverLapped == (OVERLAPPED*)0xFFFFFFFE)
{
if(bBusy == false)
{
TRACE1("Server remove thread ID %d\n", threadId);
pServer->RemoveThread(threadId);
break;
}
else
TRACE1("Server remove thread ID %d BUSY\n", threadId);
}
else if(pOverLapped == (OVERLAPPED*)0xFFFFFFFF)
{
break;
}
else
{
...
bBusy = true;
pIWorker->ProcessJob(pIJob);
bBusy = false;
....
}
}
}
Best regards
Always look on the bright side of life.
|
|
|
|
|
Sorry,
Spaces seem to be wiped out in my previous message so it's quite difficult to read.
unsigned int CThreadPool::WorkerProc(void* p)
{
{
...
bool bBusy = false;
...
while(::GetQueuedCompletionStatus(IoPort, &pN1, &pN2, &pOverLapped, INFINITE ))
{
if(pOverLapped == (OVERLAPPED*)0xFFFFFFFE)
{
if(bBusy == false)
{
TRACE1("Server remove thread ID %d\n", threadId);
pServer->RemoveThread(threadId);
break;
}
else
TRACE1("Server remove thread ID %d BUSY\n", threadId);
}
else if(pOverLapped == (OVERLAPPED*)0xFFFFFFFF)
{
break;
}
else
{
...
bBusy = true;
pIWorker->ProcessJob(pIJob);
bBusy = false;
....
}
}
}
Always look on the bright side of life.
|
|
|
|
|
Thanks for what you have done!!
And thanks to the author for sharing his code with us!!
William
Fortes in fide et opere!
|
|
|
|
|
The program couldn't link due to the error, "cannot open file, 'pthreadVC.lib'"
As part of the download, I did receive a 'pthreadVC.dll'. If this is the file that should be 'pthreadVC.lib', do I just rename it, or how do I resolve what the linker is complaining about?
Thanks.
William
|
|
|
|
|
see:
http://www.codeproject.com/useritems/error_about_worker_thread.asp
I am seeking...
For what?
Why did you ask me for what? I don't know!
|
|
|
|
|
I have created some similar code. I did not use IOCompletionPort, however. I do use WaitForSingleObject and SetEvent to control the threads in the pool. Also my code is not as generic as yours.
I did do something you might want to consider. My pool manager recognizes a maximum number of threads, but it is read from the registry on each request. This allows it to be set dynamically, the pool will resize itself accordingly. A second parameter is used as a throttle. While threads are already existing, if the CPU load is greate than a (read parameter from registry) value, the thread will be assigned, but not started until the CPU usage drops below the threshold. Each time a job finshes, my worker class determines whether the thread is still needed (if more than some percentage of threads are not busy or if we exceed the maximum).
In any case thanks for the article I found some interesting ideas in it. I wish I'd seen it BEFORE I completed my project!
Thanks,
Bill
|
|
|
|
|
Here is my worker thread class:
#include <afxmt.h>
#if !defined(AFX_WORKER_H__EC52A7FA_9663_11D5_9FC7_00B0D081C96F__INCLUDED_)
#define AFX_WORKER_H__EC52A7FA_9663_11D5_9FC7_00B0D081C96F__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#include "errorlog.h"
class CWorker : public CWinThread
{
public:
CWorker();
virtual ~CWorker();
BOOL m_bStopNow;
CEvent m_Event;
DWORD m_dwThreadID;
BOOL Doit();
Run();
void Start();
void SetStop();
BOOL IsBusy();
void SetBusy(BOOL bIsBusy);
void SetRequest(BSTR bstrRequest);
BSTR GetRequest();
void SetID(long id);
long GetID();
void SetIndex(int n);
GetIndex();
DWORD GetThreadID();
BOOL NotNeeded();
BOOL InitInstance();
BOOL ExitInstance();
protected:
BSTR m_bstrRequest;
BOOL m_bIsBusy;
DWORD m_dwFunctionThread;
long m_lRowID;
int m_nThreadIndex;
};
#endif // !defined(AFX_WORKER_H__EC52A7FA_9663_11D5_9FC7_00B0D081C96F__INCLUDED_)
#include "stdafx.h"
#if defined(_APM_)
#include "APMObject.h"
#endif
#if defined(_OTCS_)
#include "otcsobject.h"
#endif
#if defined(_ODCS_)
#include "ODCSObject.h"
#endif
#if defined(_ODPS_)
#include "ODPSObject.h"
#endif
#include "Worker.h"
#include "errorinfo.h"
#include "errorlog.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
int MessageArrived(BSTR bstrRequest,CWorker *parent);
extern int GetMax();
#define TRACEW(msg) \
{ \
CString strTraceW; \
strTraceW.Format("Request: %d, %s",m_lRowID, msg); \
TRACEX(strTraceW); \
}
#define DIAGW(sev, msg, sender) \
TRACEW(msg); \
g_pErrorLog->LogError(sev, msg, sender);
#define DIAGXW(msg) \
DIAGW(2,msg,g_strSender);
CWorker::CWorker()
{
g_strSender = OBJECTNAME + "CWorker::CWorker()";
SetBusy( FALSE);
m_bStopNow = FALSE;
m_lRowID = 0;
DIAGX("Thread Created");
}
void CWorker::SetIndex(int index)
{
m_nThreadIndex = index;
}
int CWorker::GetIndex()
{
return m_nThreadIndex;
}
DWORD CWorker::GetThreadID()
{
return m_dwThreadID;
}
CWorker::~CWorker()
{
g_strSender = OBJECTNAME + "CWorker::~CWorker()";
DIAGX("Thread destroyed");
}
BOOL CWorker::InitInstance()
{
g_strSender = OBJECTNAME + "CWorker::InitInstance()";
CoInitializeEx(NULL,COINIT_MULTITHREADED );
m_dwThreadID = GetCurrentThreadId();
m_bStopNow = FALSE;
return TRUE;
}
BOOL CWorker::ExitInstance()
{
g_CS.Lock();
g_ThreadsArray.RemoveAt(m_nThreadIndex);
for (int i = m_nThreadIndex; i < g_ThreadsArray.GetSize(); i++)
g_ThreadsArray[i]->m_nThreadIndex--;
g_CS.Unlock();
CoUninitialize();
return TRUE;
}
BOOL CWorker::IsBusy()
{
return m_bIsBusy || m_bStopNow;
}
void CWorker::SetBusy(BOOL bIsBusy)
{
m_bIsBusy = bIsBusy;
}
long CWorker::GetID()
{
long lRow = m_lRowID;
return lRow;
}
void CWorker::SetID(long lID)
{
m_lRowID = lID;
}
BSTR CWorker::GetRequest()
{
BSTR bstr = m_bstrRequest;
return bstr;
}
void CWorker::SetRequest(BSTR bstrRequest)
{
m_bstrRequest = bstrRequest;
}
void CWorker::Start()
{
g_strSender = OBJECTNAME + "CWorker::Start()";
g_CS.Lock();
m_Event.SetEvent();
DIAGX("CWorker started");
}
int CWorker::Run()
{
g_strSender = OBJECTNAME + "CWorker::Run()";
while (TRUE)
{
DIAGXW("Waiting");
DWORD dwWaitResult = WaitForSingleObject(m_Event,INFINITE);
if (m_lRowID == 0)
{
Sleep(10);
CString szErr;
szErr.Format("WaitForSingleObject returned %d",dwWaitResult);
DIAGW(2,szErr,"APMObject::Worker::Run()");
} else {
SetBusy(TRUE);
m_Event.ResetEvent();
g_CS.Unlock();
if (m_bStopNow) break;
DIAGXW("Ready to execute");
int iResult = Doit();
CString szTemp;
szTemp.Format("Finished function execution Request: %d, thread %x, Result: %d",
m_lRowID, GetCurrentThreadId(), iResult);
DIAGXW(szTemp);
::PostThreadMessage(_Module.dwThreadID, WM_MSG_WORKERCOMPLETE,GetID(), iResult);
if (GetID() == 0)
m_lRowID = 0;
szTemp.Format("Posted message Request: %d, thread %x, Result: %d, Stop = %d",
m_lRowID, GetCurrentThreadId(), iResult, m_bStopNow);
DIAGXW(szTemp);
g_CS.Lock();
if (NotNeeded())
{
SetBusy(TRUE);
m_bStopNow = TRUE;
DIAG(2,"NotNeeded return TRUE","APMObject::CWorker::Run()");
g_CS.Unlock();
break;
}
g_CS.Unlock();
m_lRowID = 0;
SetBusy(FALSE);
}
}
DIAGXW("Thread terminated");
return 0;
}
BOOL CWorker::NotNeeded()
{
return FALSE;
CString strDiag;
strDiag.Format(_T("GetMax()=%d, g_ThreadsArray.GetSize()=%d"),GetMax(),g_ThreadsArray.GetSize());
DIAG(2,strDiag,"CWorker::NotNeeded");
long lMax = GetMax();
if(lMax != 0 && lMax < g_ThreadsArray.GetSize())
{
return TRUE;
}
if (g_ThreadsArray.GetSize() <= g_nInitial) return FALSE;
int iTotalUnused = 0;
for (int i=0; i<g_ThreadsArray.GetSize();i++)
{
if (!g_ThreadsArray[i]->IsBusy()) iTotalUnused++;
}
strDiag.Format("Total unused = %d",iTotalUnused);
DIAG(2,strDiag,"CWorker::NotNeeded");
strDiag.Format("(iTotalUnused * 100) / g_ThreadsArray.GetSize() = %d",
(iTotalUnused * 100) / g_ThreadsArray.GetSize());
DIAG(2,strDiag,"CWorker::NotNeeded");
if ((iTotalUnused * 100) / g_ThreadsArray.GetSize() > 20)
{
DIAG(2,"RETURN TRUE","CWorker::NotNeeded");
return TRUE;
}
DIAG(2,"RETURN FALSE","CWorker::NotNeeded");
return FALSE;
}
void CWorker::SetStop()
{
m_bStopNow = TRUE;
if (!IsBusy())
m_Event.SetEvent();
}
BOOL CWorker::Doit()
{
g_strSender = OBJECTNAME + "CWorker::Doit";
CString strMsg;
strMsg.Format("Starting Request: %d, on thread %x", GetID(), m_dwThreadID);
DIAGXW(strMsg);
return MessageArrived(m_bstrRequest, this);
}
WARNING: There may be something wrong with the way a thread is removed. This is still a work in progress. Sorry, I didn't have time to clean it up.
Bill
|
|
|
|
|
Here is my worker thread class:
#include <afxmt.h>
#if !defined(AFX_WORKER_H__EC52A7FA_9663_11D5_9FC7_00B0D081C96F__INCLUDED_)
#define AFX_WORKER_H__EC52A7FA_9663_11D5_9FC7_00B0D081C96F__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#include "errorlog.h"
class CWorker : public CWinThread
{
public:
CWorker();
virtual ~CWorker();
BOOL m_bStopNow;
CEvent m_Event;
DWORD m_dwThreadID;
BOOL Doit();
Run();
void Start();
void SetStop();
BOOL IsBusy();
void SetBusy(BOOL bIsBusy);
void SetRequest(BSTR bstrRequest);
BSTR GetRequest();
void SetID(long id);
long GetID();
void SetIndex(int n);
GetIndex();
DWORD GetThreadID();
BOOL NotNeeded();
BOOL InitInstance();
BOOL ExitInstance();
protected:
BSTR m_bstrRequest;
BOOL m_bIsBusy;
DWORD m_dwFunctionThread;
long m_lRowID;
int m_nThreadIndex;
};
#endif // !defined(AFX_WORKER_H__EC52A7FA_9663_11D5_9FC7_00B0D081C96F__INCLUDED_)
#include "stdafx.h"
#if defined(_APM_)
#include "APMObject.h"
#endif
#if defined(_OTCS_)
#include "otcsobject.h"
#endif
#if defined(_ODCS_)
#include "ODCSObject.h"
#endif
#if defined(_ODPS_)
#include "ODPSObject.h"
#endif
#include "Worker.h"
#include "errorinfo.h"
#include "errorlog.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
int MessageArrived(BSTR bstrRequest,CWorker *parent);
extern int GetMax();
#define TRACEW(msg) \
{ \
CString strTraceW; \
strTraceW.Format("Request: %d, %s",m_lRowID, msg); \
TRACEX(strTraceW); \
}
#define DIAGW(sev, msg, sender) \
TRACEW(msg); \
g_pErrorLog->LogError(sev, msg, sender);
#define DIAGXW(msg) \
DIAGW(2,msg,g_strSender);
CWorker::CWorker()
{
g_strSender = OBJECTNAME + "CWorker::CWorker()";
SetBusy( FALSE);
m_bStopNow = FALSE;
m_lRowID = 0;
DIAGX("Thread Created");
}
void CWorker::SetIndex(int index)
{
m_nThreadIndex = index;
}
int CWorker::GetIndex()
{
return m_nThreadIndex;
}
DWORD CWorker::GetThreadID()
{
return m_dwThreadID;
}
CWorker::~CWorker()
{
g_strSender = OBJECTNAME + "CWorker::~CWorker()";
DIAGX("Thread destroyed");
}
BOOL CWorker::InitInstance()
{
g_strSender = OBJECTNAME + "CWorker::InitInstance()";
CoInitializeEx(NULL,COINIT_MULTITHREADED );
m_dwThreadID = GetCurrentThreadId();
m_bStopNow = FALSE;
return TRUE;
}
BOOL CWorker::ExitInstance()
{
g_CS.Lock();
g_ThreadsArray.RemoveAt(m_nThreadIndex);
for (int i = m_nThreadIndex; i < g_ThreadsArray.GetSize(); i++)
g_ThreadsArray[i]->m_nThreadIndex--;
g_CS.Unlock();
CoUninitialize();
return TRUE;
}
BOOL CWorker::IsBusy()
{
return m_bIsBusy || m_bStopNow;
}
void CWorker::SetBusy(BOOL bIsBusy)
{
m_bIsBusy = bIsBusy;
}
long CWorker::GetID()
{
long lRow = m_lRowID;
return lRow;
}
void CWorker::SetID(long lID)
{
m_lRowID = lID;
}
BSTR CWorker::GetRequest()
{
BSTR bstr = m_bstrRequest;
return bstr;
}
void CWorker::SetRequest(BSTR bstrRequest)
{
m_bstrRequest = bstrRequest;
}
void CWorker::Start()
{
g_strSender = OBJECTNAME + "CWorker::Start()";
g_CS.Lock();
m_Event.SetEvent();
DIAGX("CWorker started");
}
int CWorker::Run()
{
g_strSender = OBJECTNAME + "CWorker::Run()";
while (TRUE)
{
DIAGXW("Waiting");
DWORD dwWaitResult = WaitForSingleObject(m_Event,INFINITE);
if (m_lRowID == 0)
{
Sleep(10);
CString szErr;
szErr.Format("WaitForSingleObject returned %d",dwWaitResult);
DIAGW(2,szErr,"APMObject::Worker::Run()");
} else {
SetBusy(TRUE);
m_Event.ResetEvent();
g_CS.Unlock();
if (m_bStopNow) break;
DIAGXW("Ready to execute");
int iResult = Doit();
CString szTemp;
szTemp.Format("Finished function execution Request: %d, thread %x, Result: %d",
m_lRowID, GetCurrentThreadId(), iResult);
DIAGXW(szTemp);
::PostThreadMessage(_Module.dwThreadID, WM_MSG_WORKERCOMPLETE,GetID(), iResult);
if (GetID() == 0)
m_lRowID = 0;
szTemp.Format("Posted message Request: %d, thread %x, Result: %d, Stop = %d",
m_lRowID, GetCurrentThreadId(), iResult, m_bStopNow);
DIAGXW(szTemp);
g_CS.Lock();
if (NotNeeded())
{
SetBusy(TRUE);
m_bStopNow = TRUE;
DIAG(2,"NotNeeded return TRUE","APMObject::CWorker::Run()");
g_CS.Unlock();
break;
}
g_CS.Unlock();
m_lRowID = 0;
SetBusy(FALSE);
}
}
DIAGXW("Thread terminated");
return 0;
}
BOOL CWorker::NotNeeded()
{
return FALSE;
CString strDiag;
strDiag.Format(_T("GetMax()=%d, g_ThreadsArray.GetSize()=%d"),GetMax(),g_ThreadsArray.GetSize());
DIAG(2,strDiag,"CWorker::NotNeeded");
long lMax = GetMax();
if(lMax != 0 && lMax < g_ThreadsArray.GetSize())
{
return TRUE;
}
if (g_ThreadsArray.GetSize() <= g_nInitial) return FALSE;
int iTotalUnused = 0;
for (int i=0; i<g_ThreadsArray.GetSize();i++)
{
if (!g_ThreadsArray[i]->IsBusy()) iTotalUnused++;
}
strDiag.Format("Total unused = %d",iTotalUnused);
DIAG(2,strDiag,"CWorker::NotNeeded");
strDiag.Format("(iTotalUnused * 100) / g_ThreadsArray.GetSize() = %d",
(iTotalUnused * 100) / g_ThreadsArray.GetSize());
DIAG(2,strDiag,"CWorker::NotNeeded");
if ((iTotalUnused * 100) / g_ThreadsArray.GetSize() > 20)
{
DIAG(2,"RETURN TRUE","CWorker::NotNeeded");
return TRUE;
}
DIAG(2,"RETURN FALSE","CWorker::NotNeeded");
return FALSE;
}
void CWorker::SetStop()
{
m_bStopNow = TRUE;
if (!IsBusy())
m_Event.SetEvent();
}
BOOL CWorker::Doit()
{
g_strSender = OBJECTNAME + "CWorker::Doit";
CString strMsg;
strMsg.Format("Starting Request: %d, on thread %x", GetID(), m_dwThreadID);
DIAGXW(strMsg);
return MessageArrived(m_bstrRequest, this);
}
WARNING: There may be something wrong with the way a thread is removed. This is still a work in progress. Sorry, I didn't have time to clean it up.
Bill
|
|
|
|
|
This code will not work on win64.
The solution is not clean, it is more like a hack.
The missuse of the followng function:
BOOL PostQueuedCompletionStatus(
HANDLE CompletionPort, // handle to an I/O completion port
DWORD dwNumberOfBytesTransferred, // bytes transferred
ULONG_PTR dwCompletionKey, // completion key
LPOVERLAPPED lpOverlapped // overlapped buffer
);
::PostQueuedCompletionStatus(m_hWorkerIoPort, \
reinterpret_cast<dword>(pWorker), \
reinterpret_cast<dword>(pJob),\
NULL);
What about the semantics ???
I have found a more acceptable solution (that J Richter calls a trick, not a hack)
I recomend you to read the article from MSJ : New Windows 2000 Pooling Functions Greatly Simplify Thread Management from J Richter, he explains some nem threadpooling api from win2k, and how it is implemented.
Win2k implementation uses similar tricks, but he solved the previus part more accptable :
The CompletionKey associated with this device will be the address of the overlapped completion routine
and
Also, because the address of the completion routine is the completion key, to get additional context information into the OverlappedCompletionRoutine function you should use the traditional trick of placing the context information at the end of the OVERLAPPED structure.
So this way you can avoid assuming a pointer is a DWORD, and your code will survive win64.
The article explains the win2k thread pool and it's flexibility, and performance enhancements it can bring. This article is the best description of win2k thraedpools that i've found and it is definitely better than the one from MSDN help.
regards
Anyway it is interesting .
|
|
|
|
|
Very cool that your thread pool class uses an IO Completion port to queue work items. This is such a powerful and under utilized construct of NT! Beware: Windows 95/98 don't support IOCP (booo hooo). It would be cool to templatize the thread class to take a mixin strategy class that wraps IOCP and non-IOCP implementations.
|
|
|
|
|
If your are interested i ported the Class to use events instead of IO COmpletion ports, to work on Win95/98 as well.
Doen't make really a diffrence in speed.
|
|
|
|
|
like IJobDesc, IWorker, et
|
|
|
|
|
the difference between struct and class is that the default access attribute of members of struct is public. I use these two classese to define the interface so all the member functions are virtual functions, i.e, all the workers must implement the IWorker interface and all jobs must derive from the IJobDesc interface. Just it
|
|
|
|
|
There is no benefit but as you now, the all the interface members are public and the struct members by default are also public (class members by default are private)
|
|
|
|
|