當(dāng)前位置:首頁(yè) > 芯聞號(hào) > 充電吧
[導(dǎo)讀]Blaise Barney, Lawrence Livermore National Laboratory??目錄表?摘要??譯者序Pthreads 概述?? 什么是線程? ?什么是Pthreads?



Blaise Barney, Lawrence Livermore National Laboratory
??

目錄表?

摘要??譯者序Pthreads 概述?? 什么是線程? ?什么是Pthreads? ?為什么使用Pthreads???使用線程設(shè)計(jì)程序 ?Pthreads API編譯多線程程序??線程管理 ? 創(chuàng)建和終止線程??向線程傳遞參數(shù)??連接(Joining)和分離( Detaching)線程 ?棧管理 ?其它函數(shù)??互斥量(Mutex Variables)?? 互斥量概述??創(chuàng)建和銷毀互斥量 ?鎖定(Locking)和解鎖(Unlocking)互斥量??條件變量(Condition Variable)?? 條件變量概述?創(chuàng)建和銷毀條件變量 ?等待(Waiting)和發(fā)送信號(hào)(Signaling)??沒(méi)有覆蓋的主題 ?Pthread 庫(kù)API參考??參考資料??

摘要??

在多處理器共享內(nèi)存的架構(gòu)中(如:對(duì)稱多處理系統(tǒng)SMP),線程可以用于實(shí)現(xiàn)程序的并行性。歷史上硬件銷售商實(shí)現(xiàn)了各種私有版本的多線程庫(kù),使得軟件開(kāi)發(fā)者不得不關(guān)心它的移植性。對(duì)于UNIX系統(tǒng),IEEE POSIX 1003.1標(biāo)準(zhǔn)定義了一個(gè)C語(yǔ)言多線程編程接口。依附于該標(biāo)準(zhǔn)的實(shí)現(xiàn)被稱為POSIX theads?或?Pthreads。?

該教程介紹了Pthreads的概念、動(dòng)機(jī)和設(shè)計(jì)思想。內(nèi)容包含了Pthreads API主要的三大類函數(shù):線程管理(Thread Managment)、互斥量(Mutex Variables)和條件變量(Condition Variables)。向剛開(kāi)始學(xué)習(xí)Pthreads的程序員提供了演示例程。?

適于:剛開(kāi)始學(xué)習(xí)使用線程實(shí)現(xiàn)并行程序設(shè)計(jì);對(duì)于C并行程序設(shè)計(jì)有基本了解。不熟悉并行程序設(shè)計(jì)的可以參考EC3500: Introduction To Parallel Computing。

?

Pthreads

概述?

什么是線程??

?

技術(shù)上,線程可以定義為:可以被操作系統(tǒng)調(diào)度的獨(dú)立的指令流。但是這是什么意思呢??對(duì)于軟件開(kāi)發(fā)者,在主程序中運(yùn)行的“函數(shù)過(guò)程”可以很好的描述線程的概念。?進(jìn)一步,想象下主程序(a.out)包含了許多函數(shù),操作系統(tǒng)可以調(diào)度這些函數(shù),使之同時(shí)或者(和)獨(dú)立的執(zhí)行。這就描述了“多線程”程序。?怎樣完成的呢??

?

在理解線程之前,應(yīng)先對(duì)UNIX進(jìn)程(process)有所了解。進(jìn)程被操作系統(tǒng)創(chuàng)建,需要相當(dāng)多的“額外開(kāi)銷”。進(jìn)程包含了程序的資源和執(zhí)行狀態(tài)信息。如下:? 進(jìn)程ID,進(jìn)程group ID,用戶ID和group ID?環(huán)境?工作目錄??程序指令?寄存器?棧?堆?文件描述符?信號(hào)動(dòng)作(Signal actions)?共享庫(kù)?進(jìn)程間通信工具(如:消息隊(duì)列,管道,信號(hào)量或共享內(nèi)存)?

???

?

UNIX PROCESS?

THREADS WITHIN A UNIX PROCESS?

線程使用并存在于進(jìn)程資源中,還可以被操作系統(tǒng)調(diào)用并獨(dú)立地運(yùn)行,這主要是因?yàn)榫€程僅僅復(fù)制必要的資源以使自己得以存在并執(zhí)行。?獨(dú)立的控制流得以實(shí)現(xiàn)是因?yàn)榫€程維持著自己的:? 堆棧指針?寄存器?調(diào)度屬性(如:策略或優(yōu)先級(jí))?待定的和阻塞的信號(hào)集合(Set of pending and blocked signals)?線程專用數(shù)據(jù)(TSD:Thread Specific Data.)?因此,在UNIX環(huán)境下線程:? 存在于進(jìn)程,使用進(jìn)程資源?擁有自己獨(dú)立的控制流,只要父進(jìn)程存在并且操作系統(tǒng)支持?只復(fù)制必可以使得獨(dú)立調(diào)度的必要資源?可以和其他線程獨(dú)立(或非獨(dú)立的)地共享進(jìn)程資源?當(dāng)父進(jìn)程結(jié)束時(shí)結(jié)束,或者相關(guān)類似的?是“輕型的”,因?yàn)榇蟛糠诸~外開(kāi)銷已經(jīng)在進(jìn)程創(chuàng)建時(shí)完成了?因?yàn)樵谕粋€(gè)進(jìn)程中的線程共享資源:? 一個(gè)線程對(duì)系統(tǒng)資源(如關(guān)閉一個(gè)文件)的改變對(duì)所有其它線程是可以見(jiàn)的?兩個(gè)同樣值的指針指向相同的數(shù)據(jù)?讀寫同一個(gè)內(nèi)存位置是可能的,因此需要成員顯式地使用同步?

?

Pthreads?概述?

什么是?Pthreads??

歷史上,硬件銷售商實(shí)現(xiàn)了私有版本的多線程庫(kù)。這些實(shí)現(xiàn)在本質(zhì)上各自不同,使得程序員難于開(kāi)發(fā)可移植的應(yīng)用程序。?為了使用線程所提供的強(qiáng)大優(yōu)點(diǎn),需要一個(gè)標(biāo)準(zhǔn)的程序接口。對(duì)于UNIX系統(tǒng),IEEE POSIX 1003.1c(1995)標(biāo)準(zhǔn)制訂了這一標(biāo)準(zhǔn)接口。依賴于該標(biāo)準(zhǔn)的實(shí)現(xiàn)就稱為POSIX threads?或者Pthreads。現(xiàn)在多數(shù)硬件銷售商也提供Pthreads,附加于私有的API。?Pthreads?被定義為一些C語(yǔ)言類型和函數(shù)調(diào)用,用pthread.h頭(包含)文件和線程庫(kù)實(shí)現(xiàn)。這個(gè)庫(kù)可以是其它庫(kù)的一部分,如libc。?

?

Pthreads?概述?

為什么使用?Pthreads??

使用Pthreads的主要?jiǎng)訖C(jī)是提高潛在程序的性能。?當(dāng)與創(chuàng)建和管理進(jìn)程的花費(fèi)相比,線程可以使用操作系統(tǒng)較少的開(kāi)銷,管理線程需要較少的系統(tǒng)資源。?

例如,下表比較了fork()函數(shù)和pthread_create()函數(shù)所用的時(shí)間。計(jì)時(shí)反應(yīng)了50,000個(gè)進(jìn)程/線程的創(chuàng)建,使用時(shí)間工具實(shí)現(xiàn),單位是秒,沒(méi)有優(yōu)化標(biāo)志。?

備注:不要期待系統(tǒng)和用戶時(shí)間加起來(lái)就是真實(shí)時(shí)間,因?yàn)檫@些SMP系統(tǒng)有多個(gè)CPU同時(shí)工作。這些都是近似值。?

平臺(tái)?

fork()?

pthread_create()?

real?

user?

sys?

real?

user?

sys?

AMD 2.4 GHz Opteron (8cpus/node)??

41.07?

60.08?

9.01?

0.66?

0.19?

0.43?

IBM 1.9 GHz POWER5 p5-575 (8cpus/node)??

64.24?

30.78?

27.68?

1.75?

0.69?

1.10?

IBM 1.5 GHz POWER4 (8cpus/node)??

104.05?

48.64?

47.21?

2.01?

1.00?

1.52?

INTEL 2.4 GHz Xeon (2 cpus/node)??

54.95?

1.54?

20.78?

1.64?

0.67?

0.90?

INTEL 1.4 GHz Itanium2 (4 cpus/node)??

54.54?

1.07?

22.22?

2.03?

1.26?

0.67?

fork_vs_thread.txt??

在同一個(gè)進(jìn)程中的所有線程共享同樣的地址空間。較于進(jìn)程間的通信,在許多情況下線程間的通信效率比較高,且易于使用。?較于沒(méi)有使用線程的程序,使用線程的應(yīng)用程序有潛在的性能增益和實(shí)際的優(yōu)點(diǎn):? CPU使用I/O交疊工作:例如,一個(gè)程序可能有一個(gè)需要較長(zhǎng)時(shí)間的I/O操作,當(dāng)一個(gè)線程等待I/O系統(tǒng)調(diào)用完成時(shí),CPU可以被其它線程使用。?優(yōu)先/實(shí)時(shí)調(diào)度:比較重要的任務(wù)可以被調(diào)度,替換或者中斷較低優(yōu)先級(jí)的任務(wù)。?異步事件處理:頻率和持續(xù)時(shí)間不確定的任務(wù)可以交錯(cuò)。例如,web服務(wù)器可以同時(shí)為前一個(gè)請(qǐng)求傳輸數(shù)據(jù)和管理新請(qǐng)求。?考慮在SMP架構(gòu)上使用Pthreads的主要?jiǎng)訖C(jī)是獲的最優(yōu)的性能。特別的,如果一個(gè)程序使用MPI在節(jié)點(diǎn)通信,使用Pthreads可以使得節(jié)點(diǎn)數(shù)據(jù)傳輸?shù)玫斤@著提高。?例如:? MPI庫(kù)經(jīng)常用共享內(nèi)存實(shí)現(xiàn)節(jié)點(diǎn)任務(wù)通信,這至少需要一次內(nèi)存復(fù)制操作(進(jìn)程到進(jìn)程)。?Pthreads沒(méi)有中間的內(nèi)存復(fù)制,因?yàn)榫€程和一個(gè)進(jìn)程共享同樣的地址空間。沒(méi)有數(shù)據(jù)傳輸。變成cache-to-CPU或memory-to-CPU的帶寬(最壞情況),速度是相當(dāng)?shù)目臁?比較如下:?

Platform?

MPI Shared Memory Bandwidth
(GB/sec)?

Pthreads Worst Case
Memory-to-CPU Bandwidth?
(GB/sec)?

AMD 2.4 GHz Opteron ?

1.2?

5.3?

IBM 1.9 GHz POWER5 p5-575 ?

4.1?

16?

IBM 1.5 GHz POWER4 ?

2.1?

4?

Intel 1.4 GHz Xeon ?

0.3?

4.3?

Intel 1.4 GHz Itanium 2 ?

1.8?

6.4?

?

Pthreads?概述?

使用線程設(shè)計(jì)程序?

并行編程:??

在現(xiàn)代多CPU機(jī)器上,pthread非常適于并行編程。可以用于并行程序設(shè)計(jì)的,也可以用于pthread程序設(shè)計(jì)。?并行程序要考慮許多,如下:? 用什么并行程序設(shè)計(jì)模型??問(wèn)題劃分?加載平衡(Load balancing)?通信?數(shù)據(jù)依賴?同步和競(jìng)爭(zhēng)條件?內(nèi)存問(wèn)題?I/O問(wèn)題?程序復(fù)雜度?程序員的努力/花費(fèi)/時(shí)間?... ?包含這些主題超出本教程的范圍,有興趣的讀者可以快速瀏覽下“Introduction to Parallel Computing”教程。?大體上,為了使用Pthreads的優(yōu)點(diǎn),必須將任務(wù)組織程離散的,獨(dú)立的,可以并發(fā)執(zhí)行的。例如,如果routine1和routine2可以互換,相互交叉和(或者)重疊,他們就可以線程化。?

?

擁有下述特性的程序可以使用pthreads:? 工作可以被多個(gè)任務(wù)同時(shí)執(zhí)行,或者數(shù)據(jù)可以同時(shí)被多個(gè)任務(wù)操作。?阻塞與潛在的長(zhǎng)時(shí)間I/O等待。?在某些地方使用很多CPU循環(huán)而其他地方?jīng)]有。?對(duì)異步事件必須響應(yīng)。?一些工作比其他的重要(優(yōu)先級(jí)中斷)。?Pthreads?也可以用于串行程序,模擬并行執(zhí)行。很好例子就是經(jīng)典的web瀏覽器,對(duì)于多數(shù)人,運(yùn)行于單CPU的桌面/膝上機(jī)器,許多東西可以同時(shí)“顯示”出來(lái)。?使用線程編程的幾種常見(jiàn)模型:?管理者/工作者(Manager/worker):一個(gè)單線程,作為管理器將工作分配給其它線程(工作者),典型的,管理器處理所有輸入和分配工作給其它任務(wù)。至少兩種形式的manager/worker模型比較常用:靜態(tài)worker池和動(dòng)態(tài)worker池。?管道(Pipeline):任務(wù)可以被劃分為一系列子操作,每一個(gè)被串行處理,但是不同的線程并發(fā)處理。汽車裝配線可以很好的描述這個(gè)模型。?Peer:?和manager/worker模型相似,但是主線程在創(chuàng)建了其它線程后,自己也參與工作。?

共享內(nèi)存模型(Shared Memory Model):??

所有線程可以訪問(wèn)全局,共享內(nèi)存?線程也有自己私有的數(shù)據(jù)?程序員負(fù)責(zé)對(duì)全局共享數(shù)據(jù)的同步存?。ūWo(hù))?

?

線程安全(Thread-safeness):??

線程安全:簡(jiǎn)短的說(shuō),指程序可以同時(shí)執(zhí)行多個(gè)線程卻不會(huì)“破壞“共享數(shù)據(jù)或者產(chǎn)生“競(jìng)爭(zhēng)”條件的能力。?例如:假設(shè)你的程序創(chuàng)建了幾個(gè)線程,每一個(gè)調(diào)用相同的庫(kù)函數(shù):? 這個(gè)庫(kù)函數(shù)存取/修改了一個(gè)全局結(jié)構(gòu)或內(nèi)存中的位置。?當(dāng)每個(gè)線程調(diào)用這個(gè)函數(shù)時(shí),可能同時(shí)去修改這個(gè)全局結(jié)構(gòu)活內(nèi)存位置。?如果函數(shù)沒(méi)有使用同步機(jī)制去阻止數(shù)據(jù)破壞,這時(shí),就不是線程安全的了。?

?

如果你不是100%確定外部庫(kù)函數(shù)是線程安全的,自己負(fù)責(zé)所可能引發(fā)的問(wèn)題。?建議:小心使用庫(kù)或者對(duì)象,當(dāng)不能明確確定是否是線程安全的。若有疑慮,假設(shè)其不是線程安全的直到得以證明。可以通過(guò)不斷地使用不確定的函數(shù)找出問(wèn)題所在。?

?

Pthreads API?

?

Pthreads API在ANSI/IEEE POSIX 1003.1 – 1995標(biāo)準(zhǔn)中定義。不像MPI,該標(biāo)準(zhǔn)不是免費(fèi)的,必須向IEEE購(gòu)買。?Pthreads API中的函數(shù)可以非正式的劃分為三大類:?線程管理(Thread management):?第一類函數(shù)直接用于線程:創(chuàng)建(creating),分離(detaching),連接(joining)等等。包含了用于設(shè)置和查詢線程屬性(可連接,調(diào)度屬性等)的函數(shù)。?互斥量(Mutexes):?第二類函數(shù)是用于線程同步的,稱為互斥量(mutexes),是"mutual exclusion"的縮寫。Mutex函數(shù)提供了創(chuàng)建,銷毀,鎖定和解鎖互斥量的功能。同時(shí)還包括了一些用于設(shè)定或修改互斥量屬性的函數(shù)。?條件變量(Condition variables):第三類函數(shù)處理共享一個(gè)互斥量的線程間的通信,基于程序員指定的條件。這類函數(shù)包括指定的條件變量的創(chuàng)建,銷毀,等待和受信(signal)。設(shè)置查詢條件變量屬性的函數(shù)也包含其中。?命名約定:線程庫(kù)中的所有標(biāo)識(shí)符都以pthread開(kāi)頭?

Routine Prefix?

Functional Group?

pthread_?

線程本身和各種相關(guān)函數(shù)?

pthread_attr_?

線程屬性對(duì)象?

pthread_mutex_?

互斥量?

pthread_mutexattr_?

互斥量屬性對(duì)象?

pthread_cond_?

條件變量?

pthread_condattr_?

條件變量屬性對(duì)象?

pthread_key_?

線程數(shù)據(jù)鍵(Thread-specific data keys)?

在API的設(shè)計(jì)中充滿了不透明對(duì)象的概念,基本調(diào)用可以創(chuàng)建或修改不透明對(duì)象。不透明的對(duì)象可以被一些屬性函數(shù)調(diào)用修改。?Pthread API包含了60多個(gè)函數(shù)。該教程僅限于一部分(對(duì)于剛開(kāi)始學(xué)習(xí)Pthread的程序是非常有用的)。?為了可移植性,使用Pthread庫(kù)時(shí),pthread.h頭文件必須在每個(gè)源文件中包含。?現(xiàn)行POSIX標(biāo)準(zhǔn)僅定義了C語(yǔ)言的使用。Fortran程序員可以嵌入C函數(shù)調(diào)用使用,有些Fortran編譯器(像IBM AIX Fortran)可能提供了Fortran pthreads API。?關(guān)于Pthreads有些比較優(yōu)秀的書籍。其中一些在該教程的參考一節(jié)列出。?

編譯多線程程序?

?

下表列出了一些編譯使用了pthreads庫(kù)程序的命令:?

Compiler / Platform?

Compiler Command?

Description?

IBM?
AIX?

xlc_r ?/? cc_r?

C (ANSI ?/? non-ANSI)?

xlC_r?

C++?

xlf_r -qnosave
xlf90_r -qnosave?

Fortran - using IBM's Pthreads API (non-portable)?

INTEL
Linux?

icc -pthread?

C?

icpc -pthread?

C++?

PathScale
Linux?

pathcc -pthread?

C?

pathCC -pthread?

C++?

PGI
Linux?

pgcc -lpthread?

C?

pgCC -lpthread?

C++?

GNU
Linux, AIX?

gcc -pthread?

GNU C?

g++ -pthread?

GNU C++?

?

線程管理(Thread Management)?

創(chuàng)建和結(jié)束線程?

函數(shù):??

pthread_create?(thread,attr,start_routine,arg) ?

pthread_exit?(status)??

pthread_attr_init?(attr) ?

pthread_attr_destroy?(attr)??

創(chuàng)建線程:??

最初,main函數(shù)包含了一個(gè)缺省的線程。其它線程則需要程序員顯式地創(chuàng)建。?pthread_create?創(chuàng)建一個(gè)新線程并使之運(yùn)行起來(lái)。該函數(shù)可以在程序的任何地方調(diào)用。?pthread_create參數(shù):? thread:返回一個(gè)不透明的,唯一的新線程標(biāo)識(shí)符。?attr:不透明的線程屬性對(duì)象??梢灾付ㄒ粋€(gè)線程屬性對(duì)象,或者NULL為缺省值。?start_routine:線程將會(huì)執(zhí)行一次的C函數(shù)。?arg:?傳遞給start_routine單個(gè)參數(shù),傳遞時(shí)必須轉(zhuǎn)換成指向void的指針類型。沒(méi)有參數(shù)傳遞時(shí),可設(shè)置為NULL。?一個(gè)進(jìn)程可以創(chuàng)建的線程最大數(shù)量取決于系統(tǒng)實(shí)現(xiàn)。?一旦創(chuàng)建,線程就稱為peers,可以創(chuàng)建其它線程。線程之間沒(méi)有指定的結(jié)構(gòu)和依賴關(guān)系。?

?

?

?

Q:一個(gè)線程被創(chuàng)建后,怎么知道操作系統(tǒng)何時(shí)調(diào)度該線程使之運(yùn)行??

A:除非使用了Pthreads的調(diào)度機(jī)制,否則線程何時(shí)何地被執(zhí)行取決于操作系統(tǒng)的實(shí)現(xiàn)。強(qiáng)壯的程序應(yīng)該不依賴于線程執(zhí)行的順序。

?

線程屬性:??

線程被創(chuàng)建時(shí)會(huì)帶有默認(rèn)的屬性。其中的一些屬性可以被程序員用線程屬性對(duì)象來(lái)修改。?pthread_attr_init?和?pthread_attr_destroy用于初始化/銷毀先成屬性對(duì)象。?其它的一些函數(shù)用于查詢和設(shè)置線程屬性對(duì)象的指定屬性。?一些屬性下面將會(huì)討論。?

結(jié)束終止:??

結(jié)束線程的方法有一下幾種:? 線程從主線程(main函數(shù)的初始線程)返回。?線程調(diào)用了pthread_exit函數(shù)。?其它線程使用?pthread_cancel函數(shù)結(jié)束線程。?調(diào)用exec或者exit函數(shù),整個(gè)進(jìn)程結(jié)束。?pthread_exit用于顯式退出線程。典型地,pthread_exit()函數(shù)在線程完成工作時(shí),不在需要時(shí)候被調(diào)用,退出線程。?如果main()在其他線程創(chuàng)建前用pthread_exit()退出了,其他線程將會(huì)繼續(xù)執(zhí)行。否則,他們會(huì)隨著main的結(jié)束而終止。?程序員可以可選擇的指定終止?fàn)顟B(tài),當(dāng)任何線程連接(join)該線程時(shí),該狀態(tài)就返回給連接(join)該線程的線程。?清理:pthread_exit()函數(shù)并不會(huì)關(guān)閉文件,任何在線程中打開(kāi)的文件將會(huì)一直處于打開(kāi)狀態(tài),知道線程結(jié)束。?討論:對(duì)于正常退出,可以免于調(diào)用pthread_exit()。當(dāng)然,除非你想返回一個(gè)返回值。然而,在main中,有一個(gè)問(wèn)題,就是當(dāng)main結(jié)束時(shí),其它線程還沒(méi)有被創(chuàng)建。如果此時(shí)沒(méi)有顯式的調(diào)用pthread_exit(),當(dāng)main結(jié)束時(shí),進(jìn)程(和所有線程)都會(huì)終止。可以在main中調(diào)用pthread_exit(),此時(shí)盡管在main中已經(jīng)沒(méi)有可執(zhí)行的代碼了,進(jìn)程和所有線程將保持存活狀態(tài),。?


例子: Pthread?創(chuàng)建和終止?

該例用pthread_create()創(chuàng)建了5個(gè)線程。每一個(gè)線程都會(huì)打印一條“Hello World”的消息,然后調(diào)用pthread_exit()終止線程。?

Example Code - Pthread Creation and Termination??

#include

#include

#define NUM_THREADS?????5?

?

void *PrintHello(void *threadid)?

{?

???int tid;?

???tid = (int)threadid;?

???printf("Hello World! It's me, thread #%d!/n", tid);?

???pthread_exit(NULL);?

}?

?

int main (int argc, char *argv[])?

{?

???pthread_t threads[NUM_THREADS];?

???int rc, t;?

???for(t=0; t<NUM_THREADS; t++){?

??????printf("In main: creating thread %d/n", t);?

??????rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t);?

??????if (rc){?

?????????printf("ERROR; return code from pthread_create() is %d/n", rc);?

?????????exit(-1);?

??????}?

???}?

???pthread_exit(NULL);?

}?

?

?

線程管理?

向線程傳遞參數(shù)?

pthread_create()函數(shù)允許程序員想線程的start routine傳遞一個(gè)參數(shù)。當(dāng)多個(gè)參數(shù)需要被傳遞時(shí),可以通過(guò)定義一個(gè)結(jié)構(gòu)體包含所有要傳的參數(shù),然后用pthread_create()傳遞一個(gè)指向改結(jié)構(gòu)體的指針,來(lái)打破傳遞參數(shù)的個(gè)數(shù)的限制。?所有參數(shù)都應(yīng)該傳引用傳遞并轉(zhuǎn)化成(void*)。?

?

?

Q:怎樣安全地向一個(gè)新創(chuàng)建的線程傳遞數(shù)據(jù)??

A:確保所傳遞的數(shù)據(jù)是線程安全的(不能被其他線程修改)。下面三個(gè)例子演示了那個(gè)應(yīng)該和那個(gè)不應(yīng)該。?

?

Example 1 - Thread Argument Passing??

下面的代碼片段演示了如何向一個(gè)線程傳遞一個(gè)簡(jiǎn)單的整數(shù)。主線程為每一個(gè)線程使用一個(gè)唯一的數(shù)據(jù)結(jié)構(gòu),確保每個(gè)線程傳遞的參數(shù)是完整的。?


int *taskids[NUM_THREADS];?

?

for(t=0; t<NUM_THREADS; t++)?

{?

???taskids[t] = (int *) malloc(sizeof(int));?

???*taskids[t] = t;?

???printf("Creating thread %d/n", t);?

???rc = pthread_create(&threads[t], NULL, PrintHello, ?

????????(void *) taskids[t]);?

???...?

}?

?

?

Example 2 - Thread Argument Passing??

例子展示了用結(jié)構(gòu)體向線程設(shè)置/傳遞參數(shù)。每個(gè)線程獲得一個(gè)唯一的結(jié)構(gòu)體實(shí)例。?


struct thread_data{?

???int??thread_id;?

???int??sum;?

???char *message;?

};?

?

struct thread_data thread_data_array[NUM_THREADS];?

?

void *PrintHello(void *threadarg)?

{?

???struct thread_data *my_data;?

???...?

???my_data = (struct thread_data *) threadarg;?

???taskid = my_data->thread_id;?

???sum = my_data->sum;?

???hello_msg = my_data->message;?

???...?

}?

?

int main (int argc, char *argv[])?

{?

???...?

???thread_data_array[t].thread_id = t;?

???thread_data_array[t].sum = sum;?

???thread_data_array[t].message = messages[t];?

???rc = pthread_create(&threads[t], NULL, PrintHello, ?

????????(void *) &thread_data_array[t]);?

???...?

}?

?

?

Example 3 - Thread Argument Passing?(Incorrect)??

例子演示了錯(cuò)誤地傳遞參數(shù)。循環(huán)會(huì)在線程訪問(wèn)傳遞的參數(shù)前改變傳遞給線程的地址的內(nèi)容。?


int rc, t;?

?

for(t=0; t<NUM_THREADS; t++) ?

{?

???printf("Creating thread %d/n", t);?

???rc = pthread_create(&threads[t], NULL, PrintHello, ?

????????(void *) &t);?

???...?

}?

?

?

線程管理?

連接(Joining)和分離(Detaching)線程?

函數(shù):??

pthread_join?(threadid,status) ?

pthread_detach?(threadid,status)??

pthread_attr_setdetachstate?(attr,detachstate) ?

pthread_attr_getdetachstate?(attr,detachstate)??

連接:??

?“連接”是一種在線程間完成同步的方法。例如:?

?

pthread_join()函數(shù)阻賽調(diào)用線程知道threadid所指定的線程終止。?如果在目標(biāo)線程中調(diào)用pthread_exit(),程序員可以在主線程中獲得目標(biāo)線程的終止?fàn)顟B(tài)。?連接線程只能用pthread_join()連接一次。若多次調(diào)用就會(huì)發(fā)生邏輯錯(cuò)誤。?兩種同步方法,互斥量(mutexes)和條件變量(condition variables),稍后討論。?

可連接(Joinable or Not)???

當(dāng)一個(gè)線程被創(chuàng)建,它有一個(gè)屬性定義了它是可連接的(joinable)還是分離的(detached)。只有是可連接的線程才能被連接(joined),若果創(chuàng)建的線程是分離的,則不能連接。?POSIX標(biāo)準(zhǔn)的最終草案指定了線程必須創(chuàng)建成可連接的。然而,并非所有實(shí)現(xiàn)都遵循此約定。?使用pthread_create()的attr參數(shù)可以顯式的創(chuàng)建可連接或分離的線程,典型四步如下:? 聲明一個(gè)pthread_attr_t數(shù)據(jù)類型的線程屬性變量?用?pthread_attr_init()初始化改屬性變量?用pthread_attr_setdetachstate()設(shè)置可分離狀態(tài)屬性?完了后,用pthread_attr_destroy()釋放屬性所占用的庫(kù)資源?

分離(Detaching):??

pthread_detach()可以顯式用于分離線程,盡管創(chuàng)建時(shí)是可連接的。?沒(méi)有與pthread_detach()功能相反的函數(shù)?

建議:??

若線程需要連接,考慮創(chuàng)建時(shí)顯式設(shè)置為可連接的。因?yàn)椴⒎撬袆?chuàng)建線程的實(shí)現(xiàn)都是將線程創(chuàng)建為可連接的。?若事先知道線程從不需要連接,考慮創(chuàng)建線程時(shí)將其設(shè)置為可分離狀態(tài)。一些系統(tǒng)資源可能需要釋放。?


例子: Pthread Joining?

Example Code - Pthread Joining??

這個(gè)例子演示了用Pthread join函數(shù)去等待線程終止。因?yàn)橛行?shí)現(xiàn)并不是默認(rèn)創(chuàng)建線程是可連接狀態(tài),例子中顯式地將其創(chuàng)建為可連接的。??


#include

#include

#define NUM_THREADS????3?

?

void *BusyWork(void *null)?

{?

???int i;?

???double result=0.0;?

???for (i=0; i<1000000; i++)?

???{?

?????result = result + (double)random();?

???}?

???printf("result = %e/n",result);?

???pthread_exit((void *) 0);?

}?

?

int main (int argc, char *argv[])?

{?

???pthread_t thread[NUM_THREADS];?

???pthread_attr_t attr;?

???int rc, t;?

? ?void *status;?

?

???/* Initialize and set thread detached attribute */?

???pthread_attr_init(&attr);?

???pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);?

?

???for(t=0; t<NUM_THREADS; t++)?

???{?

??????printf("Creating thread %d/n", t);?

??????rc = pthread_create(&thread[t], &attr, BusyWork, NULL);??

??????if (rc)?

??????{?

?????????printf("ERROR; return code from pthread_create() ?

????????????????is %d/n", rc);?

?????????exit(-1);?

??????}?

???}?

?

???/* Free attribute and wait for the other threads */?

???pthread_attr_destroy(&attr);?

???for(t=0; t<NUM_THREADS; t++)?

???{?

??????rc = pthread_join(thread[t], &status);?

??????if (rc)?

??????{?

?????????printf("ERROR; return code from pthread_join() ?

????????????????is %d/n", rc);?

?????????exit(-1);?

??????}?

??????printf("Completed join with thread %d status= %ld/n",t, (long)status);?

???}?

?

???pthread_exit(NULL);?

}?

?

?

線程管理?

棧管理?

函數(shù):??

pthread_attr_getstacksize?(attr, stacksize) ?

pthread_attr_setstacksize?(attr, stacksize)??

pthread_attr_getstackaddr?(attr, stackaddr) ?

pthread_attr_setstackaddr?(attr, stackaddr)??

防止棧問(wèn)題:??

POSIX標(biāo)準(zhǔn)并沒(méi)有指定線程棧的大小,依賴于實(shí)現(xiàn)并隨實(shí)現(xiàn)變化。?很容易超出默認(rèn)的棧大小,常見(jiàn)結(jié)果:程序終止或者數(shù)據(jù)損壞。?安全和可移植的程序應(yīng)該不依賴于默認(rèn)的棧限制,但是取而代之的是用pthread_attr_setstacksize分配足夠的棧大小。?pthread_attr_getstackaddr和pthread_attr_setstackaddr函數(shù)可以被程序用于將棧設(shè)置在指定的內(nèi)存區(qū)域。?

在LC上的一些實(shí)際例子:??

默認(rèn)棧大小經(jīng)常變化很大,最大值也變化很大,可能會(huì)依賴于每個(gè)節(jié)點(diǎn)的線程數(shù)目。?

Node
Architecture?

#CPUs?

Memory (GB)?

Default Size
(bytes)?

AMD Opteron?

8?

16?

2,097,152?

Intel IA64?

4?

8?

33,554,432?

Intel IA32?

2?

4?

2,097,152?

IBM Power5?

8?

32?

196,608?

IBM Power4?

8?

16?

196,608?

IBM Power3?

16?

16?

98,304?


例子:?棧管理?

Example Code - Stack Management??

這個(gè)例子演示了如何去查詢和設(shè)定線程棧大小。??


#include

#include

#define NTHREADS 4?

#define N 1000?

#define MEGEXTRA 1000000?

??

pthread_attr_t attr;?

??

void *dowork(void *threadid)?

{?

???double A[N][N];?

???int i,j,tid;?

???size_t mystacksize;?

?

???tid = (int)threadid;?

???pthread_attr_getstacksize (&attr, &mystacksize);?

???printf("Thread %d: stack size = %li bytes /n", tid, mystacksize);?

???for (i=0; i<N; i++)?

?????for (j=0; j<N; j++)?

??????A[i][j] = ((i*j)/3.452) + (N-i);?

???pthread_exit(NULL);?

}?

??

int main(int argc, char *argv[])?

{?

???pthread_t threads[NTHREADS];?

???size_t stacksize;?

???int rc, t;?

??

???pthread_attr_init(&attr);?

???pthread_attr_getstacksize (&attr, &stacksize);?

???printf("Default stack size = %li/n", stacksize);?

???stacksize = sizeof(double)*N*N+MEGEXTRA;?

???printf("Amount of stack needed per thread = %li/n",stacksize);?

???pthread_attr_setstacksize (&attr, stacksize);?

???printf("Creating threads with stack size = %li bytes/n",stacksize);?

???for(t=0; t<NTHREADS; t++){?

??????rc =?pthread_create(&threads[t], &attr, dowork, (void *)t);?

??????if (rc){?

?????????printf("ERROR; return code from pthread_create() is %d/n", rc);?

?????????exit(-1);?

??????}?

???}?

???printf("Created %d threads./n", t);?

???pthread_exit(NULL);?

}?

?

線程管理?

其他各種函數(shù):?

pthread_self?() ?

pthread_equal?(thread1,thread2)??

pthread_self返回調(diào)用該函數(shù)的線程的唯一,系統(tǒng)分配的線程ID。?pthread_equal比較兩個(gè)線程ID,若不同返回0,否則返回非0值。?注意這兩個(gè)函數(shù)中的線程ID對(duì)象是不透明的,不是輕易能檢查的。因?yàn)榫€程ID是不透明的對(duì)象,所以C語(yǔ)言的==操作符不能用于比較兩個(gè)線程ID。?

pthread_once?(once_control, init_routine)??

pthread_once?在一個(gè)進(jìn)程中僅執(zhí)行一次init_routine。任何線程第一次調(diào)用該函數(shù)會(huì)執(zhí)行給定的init_routine,不帶參數(shù),任何后續(xù)調(diào)用都沒(méi)有效果。?init_routine函數(shù)一般是初始化的程序?once_control參數(shù)是一個(gè)同步結(jié)構(gòu)體,需要在調(diào)用pthread_once前初始化。例如:?

pthread_once_t once_control = PTHREAD_ONCE_INIT;??

?

?

互斥量(Mutex Variables)?

概述?

互斥量(Mutex)是“mutual exclusion”的縮寫。互斥量是實(shí)現(xiàn)線程同步,和保護(hù)同時(shí)寫共享數(shù)據(jù)的主要方法?互斥量對(duì)共享數(shù)據(jù)的保護(hù)就像一把鎖。在Pthreads中,任何時(shí)候僅有一個(gè)線程可以鎖定互斥量,因此,當(dāng)多個(gè)線程嘗試去鎖定該互斥量時(shí)僅有一個(gè)會(huì)成功。直到鎖定互斥量的線程解鎖互斥量后,其他線程才可以去鎖定互斥量。線程必須輪著訪問(wèn)受保護(hù)數(shù)據(jù)。?互斥量可以防止“競(jìng)爭(zhēng)”條件。下面的例子是一個(gè)銀行事務(wù)處理時(shí)發(fā)生了競(jìng)爭(zhēng)條件:?

Thread 1?

Thread 2?

Balance?

Read balance: $1000?

??

$1000?

??

Read balance: $1000?

$1000?

??

Deposit $200?

$1000?

Deposit $200?

??

$1000?

Update balance $1000+$200?

??

$1200?

??

Update balance $1000+$200?

$1200?

?

上面的例子,當(dāng)一個(gè)線程使用共享數(shù)據(jù)資源時(shí),應(yīng)該用一個(gè)互斥量去鎖定“Balance”。?一個(gè)擁有互斥量的線程經(jīng)常用于更新全局變量。確保了多個(gè)線程更新同樣的變量以安全的方式運(yùn)行,最終的結(jié)果和一個(gè)線程處理的結(jié)果是相同的。這個(gè)更新的變量屬于一個(gè)“臨界區(qū)(critical section)”。?使用互斥量的典型順序如下:? 創(chuàng)建和初始一個(gè)互斥量?多個(gè)線程嘗試去鎖定該互斥量?僅有一個(gè)線程可以成功鎖定改互斥量?鎖定成功的線程做一些處理?線程解鎖該互斥量?另外一個(gè)線程獲得互斥量,重復(fù)上述過(guò)程?最后銷毀互斥量?當(dāng)多個(gè)線程競(jìng)爭(zhēng)同一個(gè)互斥量時(shí),失敗的線程會(huì)阻塞在lock調(diào)用處??梢杂谩皌rylock”替換“l(fā)ock”,則失敗時(shí)不會(huì)阻塞。?當(dāng)保護(hù)共享數(shù)據(jù)時(shí),程序員有責(zé)任去確認(rèn)是否需要使用互斥量。如,若四個(gè)線程會(huì)更新同樣的數(shù)據(jù),但僅有一個(gè)線程用了互斥量,則數(shù)據(jù)可能會(huì)損壞。?

?

互斥量(Mutex Variables)?

創(chuàng)建和銷毀互斥量?

函數(shù):??

pthread_mutex_init?(mutex,attr) ?

pthread_mutex_destroy?(mutex)??

pthread_mutexattr_init?(attr) ?

pthread_mutexattr_destroy?(attr)??

用法:??

互斥量必須用類型pthread_mutex_t類型聲明,在使用前必須初始化,這里有兩種方法可以初始化互斥量:? 聲明時(shí)靜態(tài)地,如:
pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER;??動(dòng)態(tài)地用pthread_mutex_init()函數(shù),這種方法允許設(shè)定互斥量的屬性對(duì)象attr。?

互斥量初始化后是解鎖的。?

attr對(duì)象用于設(shè)置互斥量對(duì)象的屬性,使用時(shí)必須聲明為pthread_mutextattr_t類型,默認(rèn)值可以是NULL。Pthreads標(biāo)準(zhǔn)定義了三種可選的互斥量屬性:?? 協(xié)議(Protocol):?指定了協(xié)議用于阻止互斥量的優(yōu)先級(jí)改變?優(yōu)先級(jí)上限(Prioceiling):指定互斥量的優(yōu)先級(jí)上限?進(jìn)程共享(Process-shared):指定進(jìn)程共享互斥量?

注意所有實(shí)現(xiàn)都提供了這三個(gè)可先的互斥量屬性。?

pthread_mutexattr_init()和pthread_mutexattr_destroy()函數(shù)分別用于創(chuàng)建和銷毀互斥量屬性對(duì)象。?pthread_mutex_destroy()應(yīng)該用于釋放不需要再使用的互斥量對(duì)象。?

?

互斥量(Mutex Variables)?

鎖定和解鎖互斥量?

函數(shù):??

pthread_mutex_lock?(mutex) ?

pthread_mutex_trylock?(mutex)??

pthread_mutex_unlock?(mutex)??

用法:??

線程用pthread_mutex_lock()函數(shù)去鎖定指定的mutex變量,若該mutex已經(jīng)被另外一個(gè)線程鎖定了,該調(diào)用將會(huì)阻塞線程直到mutex被解鎖。?pthread_mutex_trylock()?will attempt to lock a mutex. However, if the mutex is already locked, the routine will return immediately with a "busy" error code. This routine may be useful in ?pthread_mutex_trylock()嘗試著去鎖定一個(gè)互斥量,然而,若互斥量已被鎖定,程序會(huì)立刻返回并返回一個(gè)忙錯(cuò)誤值。該函數(shù)在優(yōu)先級(jí)改變情況下阻止死鎖是非常有用的。?線程可以用pthread_mutex_unlock()解鎖自己占用的互斥量。在一個(gè)線程完成對(duì)保護(hù)數(shù)據(jù)的使用,而其它線程要獲得互斥量在保護(hù)數(shù)據(jù)上工作時(shí),可以調(diào)用該函數(shù)。若有一下情形則會(huì)發(fā)生錯(cuò)誤:? 互斥量已經(jīng)被解鎖?互斥量被另一個(gè)線程占用?互斥量并沒(méi)有多么“神奇”的,實(shí)際上,它們就是參與的線程的“君子約定”。寫代碼時(shí)要確信正確地鎖定,解鎖互斥量。下面演示了一種邏輯錯(cuò)誤:?

·????????????????????Thread 1???? Thread 2???? Thread 3?

·????????????????????Lock?????????Lock??????????

·????????????????????A = 2????????A = A+1??????A = A*B?

·????????????????????Unlock???????Unlock?????

?

?

Q:有多個(gè)線程等待同一個(gè)鎖定的互斥量,當(dāng)互斥量被解鎖后,那個(gè)線程會(huì)第一個(gè)鎖定互斥量??

A:除非線程使用了優(yōu)先級(jí)調(diào)度機(jī)制,否則,線程會(huì)被系統(tǒng)調(diào)度器去分配,那個(gè)線程會(huì)第一個(gè)鎖定互斥量是隨機(jī)的。?


例子:使用互斥量?

Example Code - Using Mutexes??

例程演示了線程使用互斥量處理一個(gè)點(diǎn)積(dot product)計(jì)算。主數(shù)據(jù)通過(guò)一個(gè)可全局訪問(wèn)的數(shù)據(jù)結(jié)構(gòu)被所有線程使用,每個(gè)線程處理數(shù)據(jù)的不同部分,主線程等待其他線程完成計(jì)算并輸出結(jié)果。?


#include

#include

#include

?

/*????

The following structure contains the necessary information???

to allow the function "dotprod" to access its input data and ?

place its output into the structure.???

*/?

?

typedef struct ?

?{?

???double??????*a;?

???double??????*b;?

???double?????sum; ?

???int?????veclen; ?

?} DOTDATA;?

?

/* Define globally accessible variables and a mutex */?

?

#define NUMTHRDS 4?

#define VECLEN 100?

???DOTDATA dotstr; ?

???pthread_t callThd[NUMTHRDS];?

???pthread_mutex_t mutexsum;?

?

/*?

The function dotprod is activated when the thread is created.?

All input to this routine is obtained from a structure ?

of type DOTDATA and all output from this function is written into?

this structure. The benefit of this approach is apparent for the ?

multi-threaded program: when a thread is created we pass a single?

argument to the activated function - typically this argument?

is a thread number. All??the other information required by the ?

function is accessed from the globally accessible structure. ?

*/?

?

void *dotprod(void *arg)?

{?

?

???/* Define and use local variables for convenience */?

?

???int i, start, end, offset, len ;?

???double mysum, *x, *y;?

???offset = (int)arg;?

??????

???len = dotstr.veclen;?

???start = offset*len;?

???end???= start + len;?

???x = dotstr.a;?

???y = dotstr.b;?

?

???/*?

???Perform the dot product and assign result?

???to the appropriate variable in the structure. ?

???*/?

?

???mysum = 0;?

???for (i=start; i<end ; i++) ?

????{?

??????mysum += (x[i] * y[i]);?

????}?

?

???/*?

???Lock a mutex prior to updating the value in the shared?

???structure, and unlock it upon updating.?

???*/?

???pthread_mutex_lock (&mutexsum);?

???dotstr.sum += mysum;?

???pthread_mutex_unlock (&mutexsum);?

?

???pthread_exit((void*) 0);?

}?

?

/* ?

The main program creates threads which do all the work and then ?

print out result upon completion. Before creating the threads,?

the input data is created. Since all threads update a shared structure, ?

we need a mutex for mutual exclusion. The main thread needs to wait for?

all threads to complete, it waits for each one of the threads. We specify?

a thread attribute value that allow the main thread to join with the?

threads it creates. Note also that we free up handles when they are?

no longer needed.?

*/?

?

int main (int argc, char *argv[])?

{?

???int i;?

???double *a, *b;?

???void *status;?

???pthread_attr_t attr;?

?

???/* Assign storage and initialize values */?

???a = (double*) malloc (NUMTHRDS*VECLEN*sizeof(double));?

???b = (double*) malloc (NUMTHRDS*VECLEN*sizeof(double));?

???

???for (i=0; i<VECLEN*NUMTHRDS; i++

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國(guó)汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開(kāi)發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來(lái)越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來(lái)越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開(kāi)幕式在貴陽(yáng)舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語(yǔ)權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營(yíng)業(yè)績(jī)穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤(rùn)率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長(zhǎng) 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營(yíng)商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國(guó)電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長(zhǎng)三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱"軟通動(dòng)力")與長(zhǎng)三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉