c語言線程庫
① c語言如何實現多線程同時運行
C語言可以通過使用多線程庫實現多線程同時運行。以下是實現步驟:
引入pthreads庫:
- 要使用多線程,首先需要安裝並引入pthreads庫。這個庫提供了創建和管理線程所需的函數和機制。在代碼中,通常通過#include <pthread.h>來引入該庫。
創建線程:
- 使用pthread_create函數來創建新的線程。這個函數需要指定新線程的屬性和要執行的函數。例如:cpthread_t thread;pthread_createargument);其中thread_function是線程將要執行的函數,argument可以傳遞給該函數的參數。3. 線程函數的編寫: 線程函數包含了線程應該執行的代碼。這個函數必須是以下形式:cvoid* thread_function { // 線程執行的代碼 return NULL;}
線程函數通常執行特定的任務,然後返回,表示該線程的任務已完成。
線程同步和互斥:
- 在多線程環境中,線程之間的同步和互斥非常重要,以避免數據競爭和其他並發問題。
- 可以使用互斥鎖來保護共享資源,確保一次只有一個線程可以訪問它們。例如:cpthread_mutex_t lock;pthread_mutex_init;pthread_mutex_lock;// 訪問共享資源pthread_mutex_unlock; 條件變數則用於線程之間的通信,允許一個線程等待另一個線程完成某項任務。5. 線程的結束和等待: 每個線程執行完畢後需要被正確地結束。 主線程可能需要等待其他線程完成其任務,這時可以使用pthread_join函數。例如:cpthread_join;
這個函數會阻塞調用線程,直到指定的線程結束為止。
通過以上步驟,你可以在C語言中實現多線程編程,使不同的任務可以同時執行,從而提高程序的效率。但需要注意的是,多線程編程也帶來了復雜性,如數據同步和互斥問題,需要謹慎處理以避免潛在的問題。
② win32程序創建線程用c語言庫的_beginthread還是API的CreateThread哪種用的多
讓我們簡單回顧一下歷史。很早以前,是一個庫用於單線程應用程序,另一個庫用於多線程應
用程序。之所以採用這個設計,是由於標准C運行庫是在1970年左右發明的。要在很久很久之
後,才會在操作系統上出現線程的概念。標准C運行庫的發明者根本沒有考慮到為多線程應用
程序使用C運行庫的問題。讓我們用一個例子來了解可能遇到的問題。
以標准C運行庫的全局變數errno為例。有的函數會在出錯時設置該變數。假定現在有這樣的一
個代碼段:
BOOL fFailure = (system("NOTEPAD.EXE README.TXT") == -1);
if (fFailure) {
switch (errno) {
case E2BIG: // Argument list or environment too big
break;
case ENOENT: // Command interpreter cannot be found
break;
case ENOEXEC: // Command interpreter has bad format
break;
case ENOMEM: // Insufficient memory to run command
break;
}
}
假設在調用了system函數之後,並在執行if語句之前,執行上述代碼的線程被中斷了。另外還假
設,這個線程被中斷後,同一個進程中的另一個線程開始執行,而且這個新線程將執行另一個
C運行庫函數,後者設置了全局變數errno。當CPU後來被分配回第一個線程時,對於上述代碼
中的system函數調用,errno反映的就不再是正確的錯誤碼。為了解決這個問題,每個線程都需
要它自己的errno變數。此外,必須有某種機制能夠讓一個線程引用它自己的errno變數,同時
不能讓它去碰另一個線程的errno變數。
這僅僅是證明了「標准C/C++運行庫最初不是為多線程應用程序而設計」的眾多例子中的一個。
在多線程環境中會出問題的C/C++運行庫變數和函數有errno,_doserrno,strtok,_wcstok,
strerror,_strerror,tmpnam,tmpfile,asctime,_wasctime,gmtime,_ecvt和_fcvt等等。
為了保證C和C++多線程應用程序正常運行,必須創建一個數據結構,並使之與使用了C/C++運
行庫函數的每個線程關聯。然後,在調用C/C++運行庫函數時,那些函數必須知道去查找主調
線程的數據塊,從而避免影響到其他線程。
那麼,系統在創建新的線程時,是如何知道要分配這個數據塊的呢?答案是它並不知道。系統
並不知道應用程序是用C/C++來寫的,不知道你調用的函數並非天生就是線程安全的。保證線
程安全是程序員的責任。創建新線程時,一定不要調用操作系統的CreateThread函數。相反,
必須調用C/C++運行庫函數_beginthreadex:
unsigned long _beginthreadex(
void *security,
unsigned stack_size,
unsigned (*start_address)(void *),
void *arglist,
unsigned initflag,
unsigned *thrdaddr);
_beginthreadex函數的參數列表與CreateThread函數的一樣,但是參數名稱和類型並不完全一
樣。這是因為Microsoft的C/C++運行庫開發組認為,C/C++運行庫函數不應該對Windows數據類
型有任何依賴。_beginthreadex函數也會返回新建線程的句柄,就像CreateThread那樣。所以,
如果已經在自己的源代碼中調用了CreateThread函數,可以非常方便地用_beginthreadex來全
局替換所有CreateThread。但是,由於數據類型並不完相同,所以可能還必須執行一些類型轉
換,以便順利地通過編譯。為了簡化這個工作,我創建了一個名為chBEGINTHREADEX的宏,
並在自己的源代碼中使用:
typedef unsigned (__stdcall *PTHREAD_START) (void *);
#define chBEGINTHREADEX(psa, cbStack, pfnStartAddr, \
pvParam, fdwCreate, pdwThreadID) \
((HANDLE) _beginthreadex( \
(void *) (psa), \
(unsigned) (cbStackSize), \
(PTHREAD_START) (pfnStartAddr), \
(void *) (pvParam), \
(unsigned) (dwCreateFlags), \
(unsigned *) (pdwThreadID)))
根據Microsoft為C/C++運行庫提供的源代碼,很容易看出_beginthreadex能而CreateThread不能
做的事情。事實上,在搜索了Visual Studio安裝文件夾後,我在<Program Files>\Microsoft Visual
Studio 8\VC\crt\src\Threadex.c中找到了_beginthreadex的源代碼。為節省篇幅,這里沒有全部照
抄一遍。相反,我在這里提供了該函數的偽代碼版本,強調了其中最有意思的地方:
uintptr_t __cdecl _beginthreadex (
void *psa,
unsigned cbStackSize,
unsigned (__stdcall * pfnStartAddr) (void *),
void * pvParam,
unsigned dwCreateFlags,
unsigned *pdwThreadID) {
_ptiddata ptd; // Pointer to thread's data block
uintptr_t thdl; // Thread's handle
// Allocate data block for the new thread.
if ((ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL)
goto error_return;
// Initialize the data block.
initptd(ptd);
// Save the desired thread function and the parameter
// we want it to get in the data block.
ptd->_initaddr = (void *) pfnStartAddr;
ptd->_initarg = pvParam;
ptd->_thandle = (uintptr_t)(-1);
// Create the new thread.
thdl = (uintptr_t) CreateThread((LPSECURITY_ATTRIBUTES)psa, cbStackSize,
_threadstartex, (PVOID) ptd, dwCreateFlags, pdwThreadID);
if (thdl == 0) {
// Thread couldn't be created, cleanup and return failure.
goto error_return;
}
// Thread created OK, return the handle as unsigned long.
return(thdl);
error_return:
// Error: data block or thread couldn't be created.
// GetLastError() is mapped into errno corresponding values
// if something wrong happened in CreateThread.
_free_crt(ptd);
return((uintptr_t)0L);
}
對於_beginthreadex函數,以下幾點需要重點關注。
每個線程都有自己的專用_tiddata內存塊,它們是從C/C++運行庫的堆(heap)上分配
的。
傳給_beginthreadex的線程函數的地址保存在_tiddata內存塊中。(_tiddata結構在
Mtdll.h文件的C++源代碼中。)純粹是為了增加趣味性,我在下面重現了這個結構。要
傳入_beginthreadex函數的參數也保存在這個數據塊中。
_beginthreadex確實會在內部調用CreateThread,因為操作系統只知道用這種方式來
創建一個新線程。
CreateThread函數被調用時,傳給它的函數地址是_threadstartex(而非
pfnStartAddr)。另外,參數地址是_tiddata結構的地址,而非pvParam。
如果一切順利,會返回線程的句柄,就像CreateThread那樣。任何操作失敗,會返回0。
struct _tiddata {
unsigned long _tid; /* thread ID */
unsigned long _thandle; /* thread handle */
int _terrno; /* errno value */
unsigned long _tdoserrno; /* _doserrno value */
unsigned int _fpds; /* Floating Point data segment */
unsigned long _holdrand; /* rand() seed value */
char* _token; /* ptr to strtok() token */
wchar_t* _wtoken; /* ptr to wcstok() token */
unsigned char* _mtoken; /* ptr to _mbstok() token */
/* following pointers get malloc'd at runtime */
char* _errmsg; /* ptr to strerror()/_strerror() buff */
wchar_t* _werrmsg; /* ptr to _wcserror()/__wcserror() buff */
char* _namebuf0; /* ptr to tmpnam() buffer */
wchar_t* _wnamebuf0; /* ptr to _wtmpnam() buffer */
char* _namebuf1; /* ptr to tmpfile() buffer */
wchar_t* _wnamebuf1; /* ptr to _wtmpfile() buffer */
char* _asctimebuf; /* ptr to asctime() buffer */
wchar_t* _wasctimebuf; /* ptr to _wasctime() buffer */
void* _gmtimebuf; /* ptr to gmtime() structure */
char* _cvtbuf; /* ptr to ecvt()/fcvt buffer */
unsigned char _con_ch_buf[MB_LEN_MAX];
/* ptr to putch() buffer */
unsigned short _ch_buf_used; /* if the _con_ch_buf is used */
/* following fields are needed by _beginthread code */
void* _initaddr; /* initial user thread address */
void* _initarg; /* initial user thread argument */
/* following three fields are needed to support signal handling and runtime errors */
void* _pxcptacttab; /* ptr to exception-action table */
void* _tpxcptinfoptrs;/* ptr to exception info pointers */
int _tfpecode; /* float point exception code */
/* pointer to the of the multibyte character information used by the thread */
pthreadmbcinfo ptmbcinfo;
/* pointer to the of the locale information used by the thread */
pthreadlocinfo ptlocinfo;
int _ownlocale; /* if 1, this thread owns its own locale */
/* following field is needed by NLG routines */
unsigned long _NLG_dwCode;
/*
* Per-Thread data needed by C++ Exception Handling
*/
void* _terminate; /* terminate() routine */
void* _unexpected; /* unexpected() routine */
void* _translator; /* S.E. translator */
void* _purecall; /* called when pure virtual happens */
void* _curexception; /* current exception */
void* _curcontext; /* current exception context */
int _ProcessingThrow; /* for uncaught_exception */
void* _curexcspec; /* for handling exceptions thrown from std::unexpected */
#if defined (_M_IA64) || defined (_M_AMD64)
void* _pExitContext;
void* _pUnwindContext;
void* _pFrameInfoChain;
unsigned __int64 _ImageBase;
#if defined (_M_IA64)
unsigned __int64 _TargetGp;
#endif /* defined (_M_IA64) */
unsigned __int64 _ThrowImageBase;
void* _pForeignException;
#elif defined (_M_IX86)
void* _pFrameInfoChain;
#endif /* defined (_M_IX86) */
_setloc_struct _setloc_data;
void* _encode_ptr; /* EncodePointer() routine */
void* _decode_ptr; /* DecodePointer() routine */
void* _reserved1; /* nothing */
void* _reserved2; /* nothing */
void* _reserved3; /* nothing */
int _ cxxReThrow; /* Set to True if it's a rethrown C++ Exception */
unsigned long __initDomain; /* initial domain used by _beginthread[ex] for managed
function */
};
typedef struct _tiddata * _ptiddata;
為新線程分配並初始化_tiddata結構之後,接著應該知道這個結構是如何與線程關聯的。來看看
_threadstartex函數(它也在C/C++運行庫的Threadex.c文件中)。下面是我為這個函數及其helper
函數__callthreadstartex編寫的偽代碼版本:
static unsigned long WINAPI _threadstartex (void* ptd) {
// Note: ptd is the address of this thread's tiddata block.
// Associate the tiddata block with this thread so
// _getptd() will be able to find it in _callthreadstartex.
TlsSetValue(__tlsindex, ptd);
// Save this thread ID in the _tiddata block.
((_ptiddata) ptd)->_tid = GetCurrentThreadId();
// Initialize floating-point support (code not shown).
// call helper function.
_callthreadstartex();
// We never get here; the thread dies in _callthreadstartex.
return(0L);
}
static void _callthreadstartex(void) {
_ptiddata ptd; /* pointer to thread's _tiddata struct */
// get the pointer to thread data from TLS
ptd = _getptd();
// Wrap desired thread function in SEH frame to
// handle run-time errors and signal support.
__try {
// Call desired thread function, passing it the desired parameter.
// Pass thread's exit code value to _endthreadex.
_endthreadex(
( (unsigned (WINAPI *)(void *))(((_ptiddata)ptd)->_initaddr) )
( ((_ptiddata)ptd)->_initarg ) ) ;
}
__except(_XcptFilter(GetExceptionCode(), GetExceptionInformation())){
// The C run-time's exception handler deals with run-time errors
// and signal support; we should never get it here.
_exit(GetExceptionCode());
}
}
關於_threadstartex函數,要注意以下重點:
新的線程首先執行RtlUserThreadStart (在NTDLL.dll文件中),然後再跳轉到
_threadstartex。
_threadstartex惟一的參數就是新線程的_tiddata內存塊的地址。
TlsSetValue是一個操作系統函數,它將一個值與主調線程關聯起來。這就是所謂的線
程本地存儲(Thread Local Storage,TLS),詳情參見第21章。_threadstartex函數將
_tiddata內存塊與新建線程關聯起來。
在無參數的helper函數_callthreadstartex中,一個SEH幀將預期要執行的線程函數包圍
起來。這個幀處理著與運行庫有關的許多事情——比如運行時錯誤(如拋出未被捕捉的
C++異常)——和C/C++運行庫的signal函數。這一點相當重要。如果用CreateThread函
數新建了一個線程,然後調用C/C++運行庫的signal函數,那麼signal函數不能正常工作。
預期要執行的線程函數會被調用,並向其傳遞預期的參數。前面講過,函數的地址和
參數由_beginthreadex保存在TLS的_tiddata數據塊中;並會在_callthreadstartex中從
TLS中獲取。
線程函數的返回值被認為是線程的退出代碼。
注意_callthreadstartex不是簡單地返回到_threadstartex,繼而到RtlUserThreadStart;
如果是那樣的話,線程會終止運行,其退出代碼也會被正確設置,但線程的_tiddata
內存塊不會被銷毀。這會導致應用程序出現內存泄漏。為防止出現這個問題,會調用
_endthreadex(也是一個C/C++運行庫函數),並向其傳遞退出代碼。
最後一個需要關注的函數是_endthreadex(也在C運行庫的Threadex.c文件中)。下面是我編寫的該
函數的偽代碼版本:
void __cdecl _endthreadex (unsigned retcode) {
_ptiddata ptd; // Pointer to thread's data block
// Clean up floating-point support (code not shown).
// Get the address of this thread's tiddata block.
ptd = _getptd_noexit ();
// Free the tiddata block.
if (ptd != NULL)
_freeptd(ptd);
// Terminate the thread.
ExitThread(retcode);
}
對於_endthreadex函數,要注意幾下幾點:
C運行庫的_getptd_noexit函數在內部調用操作系統的TlsGetValue函數,後者獲取主調
線程的tiddata內存塊的地址。
然後,此數據塊被釋放,調用操作系統的ExitThread函數來實際地銷毀線程。當然,
退出代碼會被傳遞,並被正確地設置。
在本章早些時候,我曾建議大家應該避免使用ExitThread函數。這是千真萬確的,而且我在這
里並不打算自相矛盾。前面說過,此函數會殺死主調線程,而且不允許它從當前執行的函數返
回。由於函數沒有返回,所以構造的任何C++對象都不會被析構。現在,我們又有了不調用
ExitThread函數的另一個理由:它會阻止線程的_tiddata內存塊被釋放,使應用程序出現內存泄
漏(直到整個進程終止)。
Microsoft的C++開發團隊也意識到,總有一些開發人員喜歡調用ExitThread。所以,他們必須
使這成為可能,同時盡可能避免應用程序出現內存泄漏的情況。如果真的想要強行殺死自己的
線程,可以讓它調用_endthreadex(而不是ExitThread)來釋放線程的_tiddata塊並退出。不過,
我並不鼓勵你調用_endthreadex。
現在,你應該理解了C/C++運行庫函數為什麼要為每一個新線程准備一個獨立的數據塊,而且
應該理解了_beginthreadex如何分配和初始化此數據塊,並將它與新線程關聯起來。另外,你還
應理解了_endthreadex函數在線程終止運行時是如何釋放該數據塊的。
一旦這個數據塊被初始化並與線程關聯,線程調用的任何需要「每線程實例數據」的C/C++運
行庫函數都可以輕易獲取主調線程的數據塊的地址(通過TlsGetValue),並操縱線程的數據。這
對函數來說是沒有問題的。但是,對於errno之類的全局變數,它又是如何工作的呢?errno是在
標准C headers中定義的,如下所示:
_CRTIMP extern int * __cdecl _errno(void);
#define errno (*_errno())
int* __cdecl _errno(void) {
_ptiddata ptd = _getptd_noexit();
if (!ptd) {
return &ErrnoNoMem;
} else {
return (&ptd->_terrno);
}
}
任何時候引用errno,實際都是在調用內部的C/C++運行庫函數_errno。該函數將地址返回給「與
主調線程關聯的數據塊」中的errno數據成員。注意,errno宏被定義為獲取該地址的內容。這
個定義是必要的,因為很可能寫出下面這樣的代碼:
int *p = &errno;
if (*p == ENOMEM) {
...
}
如果內部函數_errno只是返回errno的值,上述代碼將不能通過編譯。
C/C++運行庫還圍繞特定的函數放置了同步原語(synchronization primitives)。例如,如果兩個
線程同時調用malloc,堆就會損壞。C/C++運行庫函數阻止兩個線程同時從內存堆中分配內存。
具體的辦法是讓第2個線程等待,直至第1個線程從malloc函數返回。然後,才允許第2個線程進
入。(線程同步將在第8章和第9章詳細討論。)顯然,所有這些額外的工作影響了C/C++運行庫的
多線程版本的性能。
C/C++運行庫函數的動態鏈接版本被寫得更加泛化,使其可以被使用了C/C++運行庫函數的所有
運行的應用程序和DLL共享。因此,庫只有一個多線程版本。由於C/C++運行庫是在一個DLL
中提供的,所以應用程序(.exe文件)和DLL不需要包含C/C++運行庫函數的代碼,所以可以更小
一些。另外,如果Microsoft修復了C/C++運行庫DLL的任何bug,應用程序將自動獲得修復。
就像你期望的一樣,C/C++運行庫的啟動代碼為應用程序的主線程分配並初始化了一個數據塊。
這樣一來,主線程就可以安全地調用任何C/C++運行庫函數。當主線程從其入口函數返回的時
候,C/C++運行庫函數會釋放關聯的數據塊。此外,啟動代碼設置了正確的結構化異常處理代
碼,使主線程能成功調用C/C++運行庫的signal函數。
6.7.1 用_beginthreadex 而不要用CreateThread 創建線程
你可能會好奇,假如調用CreateThread而不是C/C++運行庫的_beginthreadex來創建新線程,會
發生什麼呢?當一個線程調用一個需要_tiddata結構的C/C++運行庫函數時,會發生下面的情
況。(大多數C/C++運行庫函數都是線程安全的,不需要這個結構。)首先,C/C++運行庫函數嘗
試取得線程數據塊的地址(通過調用TlsGetValue)。如果NULL被作為_tiddata塊的地址返回,表
明主調線程沒有與之關聯的_tiddata塊。在這個時候,C/C++運行庫函數會為主調線程分配並初
始化一個_tiddata塊。然後,這個塊會與線程關聯(通過TlsSetValue) ,而且只要線程還在運行,
這個塊就會一直存在並與線程關聯。現在,C/C++運行庫函數可以使用線程的_tiddata塊,以後
調用的任何C/C++運行庫函數也都可以使用。
當然,這是相當誘人的,因為線程(幾乎)可以順暢運行。但事實上,問題還是有的。第一個
問題是,假如線程使用了C/C++運行庫的signal函數,則整個進程都會終止,因為結構化異常處
理(SEH)幀沒有就緒。第二個問題是,假如線程不是通過調用_endthreadex來終止的,數據
塊就不能被銷毀,從而導致內存泄漏。(對於一個用CreateThread函數來創建的線程,誰會調用
_endthreadex呢?)
③ C語言怎麼同時運行多個程序
在C語言中,實現同時運行多個程序的方法主要有兩種:多進程和多線程。多進程是通過fork()函數實現的,此函數可以創建一個與當前進程完全相同的進程,新進程與原進程共享代碼,但各自維護獨立的變數、棧和堆。因此,一個進程可以創建多個子進程,每個子進程可以運行不同的程序。同時,可以使用exec()函數族,包括execl()、execv()、execle()、execve()等,這些函數可以載入並運行新的程序,取代當前進程的程序。需要注意的是,多進程的每個進程都有獨立的內存空間,因此,進程間的數據共享需要通過文件或共享內存等方式實現。
另一種實現同時運行多個程序的方式是使用多線程,線程是進程中的一個執行單元,多個線程可以共享進程的資源,如代碼、數據和文件描述符等。多線程方式使用的庫函數包括pthread_create()、pthread_join()等。多線程的程序運行效率較高,因為線程之間的切換開銷較小,可以節省大量的切換時間。但是,多線程編程需要處理好線程間的同步和互斥問題,否則可能會出現死鎖或競態條件等問題。
多進程和多線程各有優缺點,適用於不同的場景。多進程方式可以實現完全獨立的程序,進程間的通信相對簡單,但是進程間的數據共享較為困難,且進程間切換的開銷較大。而多線程方式則可以提高程序的運行效率,但是線程間的數據共享和同步需要特別注意,否則可能會出現各種問題。因此,在選擇多進程或多線程方式時,需要根據具體的應用場景和需求進行權衡。
同時運行多個程序時,還需要注意資源管理和同步問題。資源管理主要涉及到內存、文件和系統資源的分配和回收,以及線程或進程之間的資源競爭和沖突。同步問題則涉及到多個線程或進程之間的協調和協作,以確保程序的正確性和效率。常見的同步機制包括互斥鎖、信號量、條件變數等。通過合理使用這些機制,可以有效地解決多線程或多進程間的同步問題。