OpenMPの使い方Daquanの

 

OpenMPの基本概念
のOpenMPは共有メモリ並列システムのためのマルチスレッドプログラミング方式であり、サポートされているプログラミング言語はC、C ++およびFortranを含みます。OpenMPのは、並列アルゴリズムの抽象化のレベルは、マシンのマルチコアCPUの設計上の並列プログラムのために特に適して記載されて提供されます。プログラムコンパイラプラグマの命令は、OpenMPを用いた並列プログラミングは困難と複雑さを低減、自動並列処理プログラムを追加しました。コンパイラは、OpenMPをサポートしていない場合は、プログラムが正常な(シリアル)プログラムに退化します。すでにプログラムでのOpenMPディレクティブは、コンパイラの正常な動作には影響しません。有効OpenMPのは、VS、OpenMPのを建て、多くの主流のコンパイラ環境では非常に簡単です。プロジェクトを右- >プロパティ- >構成プロパティ- > C / C ++ - >言語- > OpenMPサポート、 "はい"ボタンを選択します。

OpenMPの実行モード
フォーク参加を使用してOpenMPの実行モード。並列コンピューティングの必要性は、並列タスクを実行するスレッドの分岐数を導出する際に最初に、唯一のメインスレッドがあります。並列コードの実行が完了すると、スレッドの分岐は、結合、および別メインスレッドへの制御フロー。

典型的な概略フォークジョイン実行モデルを次のように

 

OpenMPのプログラミングモデルは、指令誘導を並列化するために、コンパイラによって導かれた基礎を、スレッドために、彼らは指導、API関数セットと環境変数をコンパイルしている並列プログラミング制御を実現することができる3つの要素があります。

コンパイラディレクティブ
ターゲットコンパイラディレクティブOpenMPのは、1)並列領域を生成するステップと、2)スレッドのコードブロックに、スレッド間3)配布ループ反復、コードセグメントの4)配列; 5)スレッドを同期ジョブ間。#pragma ompのディレクティブ[句]、[句] ...]ます。#pragma ompのコンパイラガイダンスは、特定の機能命令でバックなどのフォーマットを開始するコマンド。次のように共通の特徴の説明は、次のとおりです。

パラレル:並列スレッドで実行されるコードの複数のブロック構造を使用する前に、
のために:ループ文のために使用する前に、並列実行の複数のスレッドに割り当てられた周期計算タスクを示し、タスクの共有を実現するために、必要があります各サイクルの間にデータの相関プログラマが保証することにより、
並列用:結合と平行するための命令は、前のループのために使用されていない、並列に実行する複数のスレッドによってループ本体のコードを表し、それはまた有します;二つの機能を共有する並列ドメインとタスクを生成する
構成タスクを共有する文の複数のブロックを実装するために使用されるパラレルコード・セグメントで実行することができる前に命令を示すコード・セグメントは、各セクションと並行して実行することができ、使用される(注:セクションそして、区別セクションsectionTop)
平行切片:セクションと二つの平行な結合文、同様の平行ため、
単一:技術と並行して、コードは、単一のセクションが実行のスレッドを表している。
クリティカル:コードのクリティカルセクションの前の周期で、 1つのスレッドだけ入力することを確認するのOpenMP;
フラッシュは:確実にするために、そのすべてのOpenMPスレッド 画像全体の一貫性をよれば、
バリア:バリアがダウン継続する場合、バリアにコード同期、スレッド実行中の並列のスレッドが停止し、すべてのスレッドが実行されるまで待機するため、
アトミック:データ操作を指定する必要アトミック完了し;
マスタは:コードのセクションは、メインスレッドによって実行される指定し、
スレッドプライベート:後述するように1つ以上の変数は、特別なスレッドである特定し、スレッド固有のプライベート区別。

 
対応するOpenMPの句: 


プライベート:各スレッドに独自のプライベートコピーを持っている1つまたは複数の変数を指定します。
FIRSTPRIVATE:指定一つ以上の変数は、各スレッドの独自のプライベートコピーを持っており、プライベート変数は、フィールドまたは並列に入力しますタスク共有ドメインは、同じ変数の値を初期値として、メインスレッドに継承される。
LASTPRIVATE:並列処理の後、メインスレッドにコピーされ、同じ名前の変数またはスレッドプライベート変数の複数の値を指定するために使用され、糸のセクションのタスクの共有または最後のスレッドをコピーするための責任があり、 
削減:プライベートであることを1つまたは複数の変数を指定し、これらの変数は、並列処理後に縮小操作を実行するために指定し、その結果同じ名前の変数のメインスレッドに戻す;
NOWAIT:同時スレッドは、他のガイダンスは、暗黙のバリア同期を命令無視でき指摘;
NUM_THREADS:並列当スレッドの数、 
スケジュール:タイプを共有するタスクの指定されたタスクの割り当てスケジュール;
共有:1つを指定します複数の変数は、複数のスレッド間で変数を共有し、
注文:に指定します タスクを共有するシリアルループの順序に従って、指定されたコード・セグメント内で実行される必要がある。
COPYPRIVATE:同じ変数が当並列に他のスレッドにブロードキャストされ、指定されたスレッド固有の変数を持つ単一の命令、
COPYINのN-た:必要スレッドプライベート変数のタイプを指定するために使用しますメインスレッドは、変数と同じ名前で初期化されます。
デフォルト:同時使用可変ドメインに指定された、デフォルトが共有されています。

 

 
 

API関数
上記のコンパイラガイダンスコマンドに加えて、OpenMPのも同時スレッドの特定の動作を制御するためのAPI関数のセットを提供し、ここではいくつかの一般的な機能やOpenMPのAPIの説明は、次のとおりです。 

 

環境変数
 のOpenMPは、実行時にコードの並列実行を制御するために使用される環境変数の数を提供します。これらの環境変数を制御することができる:1)スレッドの数; 2)分裂周期を指定する方法と、前記プロセッサに結合された3)スレッドが、4)ネストされた並列処理を有効/無効にする、並列の最大ネスティング・レベルが提供; 5)有効/動的スレッドを無効にする; 6)スレッド・スタック・サイズを設定する; 7)糸保持ポリシーを設定します。一般的な環境変数:

;:OMP_SCHEDULEループの並列化のためにスケジューリングするため、値はラウンドロビンスケジューリングタイプである  
OMP_NUM_THREADS:ドメインの並列内のスレッドの数;   
OMP_DYNAMIC:変数の値を設定することにより、動的セットパラレル技術を許可するかどうかを決定しますスレッドの数;  
OMP_NESTEDは:パラレルネストかどうかを示します。 

および使用方法の指示句のOpenMP
並列 
パラレルは、それに関連して使用される他のための指示書、セクションなど、使用することができるタイルを構築するために使用されます。並列命令は、それにコードの一部を実行する複数のスレッドを作成するために使用されます。スレッドの並列複数のコードブロックの各行が繰り返し実行されます。伝統的な機能と、それがスレッドを作成し、実行したスレッドを待つスレッド関数を作成するために繰り返し呼び出さスレッドエントリ関数と等価であるよりも、スレッドを作成します。以下のプログラム例:

空FUN1()

{

テスト出力6:6つのスレッドの#pragmaがOMP平行NUM_THREADS(6)//定義は、各スレッド{}は、結果内のコードを実行します

    {

        裁判所未満<< "テスト" <<てendl;

    }

    システム(「一時停止」)。

}

以下のための
命令のためには、実行ループの複数のスレッドを割り当てるために使用されます。使用するために一緒に並行して、一般的並列命令形式命令のための命令は、それだけではタイル並行ステートメントを使用することができます。タスクの共有のために割り当てられた複数のスレッド間の並列ドメインと計算タスク並列を生成します。以下のプログラム例:

空fun2()

{

NUM_THREADSための#pragma OMP平行(6){

        printf( "OpenMPのテストは、スレッドの数は、%Dを\ n"、OMP_GET_THREAD_NUM())。

    } //各スレッドが12/6 = 2回の反復の量を割り当て、12回の繰り返しの量では、6つのスレッドを指定します。

    システム(「一時停止」)。

}

&sectionTop部
sectionTop声明は、いくつかの異なるセグメントにコードセクション内の文に、それぞれが並行して行わセクションで使用されています。構文は次のとおりです。

#pragma OMP [パラレル]セクション[句]

{

   #pragma ompのセクション

   {

            ブロック

   } 

   #pragma ompのセクション

   {

            ブロック

   } 

}

コードの各セクションの記述は、並列に実行され、各セクションは異なるスレッドに割り当てられます。

セクション・ステートメントを使用する場合には、このようにそれ以外の区間以外の実行時間は、並列実行の効果を達成するためには長すぎる、コードの実行時間以下に、各セクションを確保する必要があることに留意すべきです。手分割方法によってスレッドであるスレッドを分割するセクションを使用し、その後、共有が非常に均一で、自動的に、すべての時間があれば、サイクル間の時間に隙間がないように、システムによって評価される文のために使用します。

プライベート
スレッドプライベート変数、プライベート変数として宣言された変数として宣言し、1つ以上の変数のためのプライベート句、各スレッドを割り当てるには、変数の独自のプライベートコピーを持ち、他のスレッドがプライベート・コピーにアクセスすることはできません。でも、並列領域外の共有変数に同じ名前の変数が動作しないように共有変数の外側の領域に並列領域で何の役割も果たしていない、と並行して共有しました。以下のプログラム例:

 INT K = 100。

プライベート(K)用の#pragma OMP平行

         用(K = 0; K <3; kは++)

         {

                   printf( "K =%のD / N"、K)。

         }

         printf( "最後のkは=%のD / N"、K)。

次のように印刷した後に実行上記のプログラムの結果は次のとおりです。

K = 0

K = 1

K = 2

K = 3

最後のk = 100

内部ループ変数kとループ変数kの前方の領域に対して、印刷結果から分かるように、実際には二つの異なる変数です。並列領域内専用の入り口とプライベート変数宣言節の初期値は、同じ名前の共有変数の値を継承しない、未定義です。

プライベートプライベート変数宣言は、同じ名前の変数の値を継承することはできませんが、現実には、OpenMPは、この機能を実現するFIRSTPRIVATE句を提供し、時には共有変数の元の値を継承する必要があります。FIRSTPRIVATE(k)は、100の初期値として変数継承kの共有k値の外側の領域に平行にプライベート変数、及び平行出口領域を使用して上記の手順は、共有変数k 100の値は不変に維持される場合。

時々、計算後、平行出口領域は、平行プライベート変数共有変数の領域内に同じ名前の値に値を代入する必要があり、プライベートFIRSTPRIVATE句出口領域の前にプライベート変数に平行ではありません最後に、対応する共有変数に割り当てられる値は、LASTPRIVATE句はプライベート変数割当共有変数の値の並列出口領域で達成するために使用されます。以下のプログラム例:

 INT K = 100。

FIRSTPRIVATE(K)用の#pragma OMP平行、LASTPRIVATE(K)

         用(i = 0; iは4 <; iは++)

         {

                   K + = I。

                   printf( "K =%のD / N"、K)。

         }

         printf( "最後のkは=%のD / N"、K)。

次のように上記のコードの印刷結果が行われます。

K = 100

K = 101

K = 103

K = 102

最後のk = 103

印刷結果から分かるように、平行部のループを出た後、共有変数kの値がかなり変化しない原稿100を保持するより、103となります。セクションが構築されている場合、最後のセクションの値は、共有変数の対応するステートメントに割り当てられ、OpenMP仕様は、ループの反復が、それがある場合、ループの最後の反復の値が対応する共有変数に割り当てられていることを述べています。最後のセクションではなく終了した最後の実行の実際の実行時間よりも、プログラムの構文上の最後を参照していることを、ここで注意してください。LASTPRIVATE変数のパラメータで使用されるタイプのクラス(クラス)は、その後、いくつかの制限がアクセスを使用して、変数は、パラメータFIRSTPRIVATE句として使用されていない限り、デフォルトコンストラクタをクリアする必要がある場合は、またのコピーが必要異なるオブジェクトの代入演算子、代入演算子コピー動作のシーケンスは、コンパイラの定義に応じて、指定されていません。

A THREADPRIVATE
THREADPRIVATE命令がグローバルオブジェクトにコピーの各スレッドプライベートコピーを指定するために使用され、すなわち、それぞれのプライベートグローバルオブジェクトを有する各スレッド。その中で、民間とTHREADPRIVATEスレッドプライベート変数の宣言の違いは、一般的に効果的なグローバルスコープであり、プライベート変数は、それが属する並列構成で有効と宣言しました。アドレスは一定ではないスレッドプライベート変数として使用されます。外部定義と初期化は、明確なコピーコンストラクタを持たなければならないパラメータTHREADPRIVATEとして使用する場合、C ++クラス(クラス)のためのいくつかの制限は、変数の型。以下のプログラム例:

int型のグラム。

#pragmaのOMP THREADPRIVATE(G)//なければならない最初の文

(int型ARGC、チャー*のARGV [])INT主

{

       / *明示的に動的なスレッドをオフにします* /

       OMP_SET_DYNAMIC(0)。

#pragma ompの平行

       {

              G = OMP_GET_THREAD_NUM()。   

              printf( "TID:%Dを\ n"、G); //ランダム順次出力0-3

       並列領域の終わり} //

 

#pragma ompの平行

       {

              int型のTEMP =グラム* gを、

              printf( "TIDた:%d、TIDの*のTID:%dは\ nを"、G、温度);異なる値の異なるスレッドで//グローバル変数

       並列領域の終わり} //

}

注:THREADPRIVATEを使用する場合、使用OMP_SET_DYNAMIC(0)正しい結果を確実にするために、プロパティの動的スレッドを閉じ。

このコンテンツシェア報告
共有句は、共有変数に1つまたは複数の変数を宣言するために使用することができます。いわゆる共有変数、並列領域のチーム内のすべてのスレッドの値は、すべてのスレッドが同じアドレスにアクセスし、変数のメモリアドレスを有しています。だから、地域における並列で共有変数のために、データは、競争、保護を強化するために、対応する必要性を防ぐために、レースの条件を考慮する必要があります。以下のプログラム例:

#define COUNT 10000

int型のmain(int型のargc、_TCHAR * ARGV [])

{

       int型の合計= 0;

共有(合計)のための#pragma OMP平行

       以下のために(int型私= 0; iの数<;私は++)

       {

              合計=合計+ I;

       }

       printf( "%d個の\ n"、合計)。

       0を返します。

}

多くの場合、結果は異なる場合があります。なお:ループ反復変数は、ループがセクションを作成してプライベートで、ループ構造領域内で宣言自動変数はプライベートです。ループ反復変数が共有されている場合、OpenMPのは、どのように実行するので、それは唯一の民間することができます。でも、ループの反復変数を変更するために、それはループ反復変数は、ループの建築面積にプライベート共有この機能を使用している変更されません。プログラム例:
の#define COUNT 10

int型のmain(int型のargc、_TCHAR * ARGV [])

{

       int型の合計= 0;

       int型私= 0;

共有(和、I)用の#pragma OMP平行

       用(i = 0; iは数<; iは++)

       {

              合計=合計+ I;

       }

       printf( "%d個の\ n" は、i)は、

       printf( "%d個の\ n"、合計)。

       0を返します。

}

上記の手順では、可変反復ループの出力は、iが修飾変数iの共用が、0です。ここではルールだけ平行ループ領域のために、他の並列領域にはそのような要件は存在しないことに注意してください。並列ループ領域が、可変ループの繰り返しを変更することはできません。すなわち、このプログラムでは、もはやインビボで可変反復のループを循環させるために私が変更されます。

デフォルトの
デフォルトの属性は、OpenMP C ++でのデフォルトパラメータのみが共有またはnoneすることができ、並列に可変領域を指定します。デフォルト(共有):共有属性なし並列領域内の共有変数を表すが指定されています

デフォルト(なし):明確な属性定義の変数(このような並列ループ反復ループ可変領域としては唯一の民間することができます)がない限り表現が明示的に使用していない、並列領域ならば、それ以外の場合はエラーになり、すべての共有変数の属性データを指定する必要があります。デフォルトの句は、デフォルトの動作では、デフォルト(共有)です。

COPYIN
COPYIN句は、チーム内のサブスレッドが同一の初期値を有するように、並列領域を実行する各スレッドにメインスレッドのスレッドプライベート変数スレッドプライベート変数の値をコピーするために使用され、メインスレッドです。以下のプログラム例:

書式#include <omp.h> 

int型のA = 100; 

#pragmaのOMP THREADPRIVATE(A) 

int型のmain(int型のargc、_TCHAR * ARGV []) 

以下のための#pragma ompの平行 

    以下のために(; iは10 <; I = 0 int型私は++) 

    { 

        ++; 

        printf( "スレッドID:%のD、%のD:%Dを\ n"、OMP_GET_THREAD_NUM()、I、A)。//#1 

    } 

    printf(「グローバルA:%Dを\ n」、A);平行出口領域、すなわちのみマスタースレッドので、等しい//値並行印刷「Globa A」の領域外に、結果はスレッド0の前に常にあります0のスレッドが実行します。

 

COPYINための#pragma OMP平行(A)

    以下のために(; iは10 <; I = 0 int型私は++) 

    { 

        ++; 

        printf( "スレッドID:%のD、%のD:%Dを\ n"、OMP_GET_THREAD_NUM()、I、A)。//#1 

    } 

 

    printf( "グローバルA:%Dを\ n"、A)。//#2 

 

    0を返します。 

}

後COPYINを使用せずに、第2の並列領域、我々は本明細書で使用したのと同じ、COPYINない異なるスレッドの初期値のプライベートコピーを入力する場合、すべてのスレッドの初期値はメインスレッドからの値で初期化されることが見出さ、動作は継続し、これはスレッド0の結果の出力の値です。シンプルで理解するために、COPYINを使用した後、変数のコピーになりますスレッドのすべてのタイプと変数のメインスレッドのコピーをTHREADPRIVATE「同期」。さらにCOPYINパラメータは、明示的な代入演算子を用いてコピーされる変数のクラスタイプのために、スレッドプライベートとして宣言されなければなりません。

COPYPRIVATE
COPYPRIVATE句は、同一の並列領域を実行するために同じ変数の他のスレッド1つのスレッドからの変数値放送のプライベートコピーを通すために使用されます。COPYPRIVATE単一の命令のみを使用することができる:句、ブロードキャスト動作は、単一ブロックの最後に完了した(単一の命令期間は、コードセグメントの実行の単一スレッドの前にのみ使用され、次のコードセグメントは、実行のシングルスレッドであることがあることを示します)。変数COPYPRIVATEはプライベート/ firstprivateのスレッドプライベートのために使用されるか、または変更されます。以下のプログラム例:

int型のカウンタ= 0;

#pragmaのOMP THREADPRIVATE(カウンタ)

int型increment_counter()

{

         カウンタ++;

         (カウンタ)を返します。

}

#pragma ompの平行

         {

                   int型カウント;

#pragma OMP単一COPYPRIVATE(カウンタ)

                   {

                            カウンタ= 50。

                   }

                   = increment_counterを数えます();

                   printf( "スレッドID:%のLD、=%のLD / Nカウント"、OMP_GET_THREAD_NUMを()、カウント)。

}

印刷結果:

スレッドID:2、カウント= 51

スレッドID:0、カウント= 51

スレッドID:3、カウント= 51

スレッドID:1、カウント= 51

あなたはcopyprivate指示句を使用しない場合は、次のように結果を出力します。

スレッドID:2、カウント= 1

スレッドID:1、カウント= 1

スレッドID:0、カウント= 51

スレッドID:3、カウント= 1

図から分かるように、カウンタ値を割り当てるには、単一の構造内のCOPYPRIVATE句の使用は、他のスレッドにブロードキャストされますが、copyprivate指示句を使用していない、単一の構造に割り当てを取得する唯一つのスレッド、単一構造を得るために、他のスレッドはありません内の割り当て。

OpenMPのスケジューリングタスク
のOpenMP、タスクのスケジューリングは、主に単純反復の同じ数、各スレッドは、各スレッドの算出結果に割り当てられている場合、ループの各反復における計算量は、等しくない場合に、並行して循環するために使用されますいくつかのCPUコアを引き起こす、任意のスレッドがいくつかの完了後に最初の実行を実行することになる負荷の不均衡は、アプリケーションのパフォーマンスの影響アイドル状態です。OpenMPのは、タスクを達成するために、句の発送スケジュールを提供します。スケジュール句形式:スケジュール(種類、[サイズ] )。

  これは、型パラメータスケジューリングタイプ、値の導かれ、静的、動的、ランタイム四種類の値を指します。実行時に許可される実行時のスケジューリングタイプ、最初の3つの実際のスケジューリングポリシーを決定することを特徴とします。

  パラメータサイズは、各スケジューリングが整数でなければならない反復回数を示しています。このパラメータはオプションです。タイプの値は、ランタイムの場合、このパラメータを使用することはできません。

静的スケジューリング静的
スケジュール句を使用しない場合、コンパイラのほとんどは、デフォルトでは静的スケジューリングです。コンパイル時に静的には、スレッドによって実行されるこれらのループを決定されています。次に静的各スレッド約n / T算出反復に割り当てられたn個のループ反復、Tスレッドが存在すると仮定する。N / Tは、必ずしも整数ではないので、反復回数の実際の分布が相違する場合であってもよいです。

使用サイズパラメータで、各スレッドは、サイクル数が10である場合、スレッドの数が2で、スレッド0は0~4連続する反復た、N / Tの連続する反復に割り当てられていない場合、スレッド10 1〜から出発して9回の連続する反復。

サイズ、サイズ分布スレッドに各反復を使用する場合。サイクル数が10である場合、スレッドの数は、2つの指定サイズがそのように0、0,2、3回の反復スレッド1に割り当てられた反復、およびスレッドに割り当てられ、2です。

ダイナミックスケジューリング、動的
  動的スケジューリングは、スレッドが既に割り当てられたタスクを完了した後に、動的にタスク(静的スケジューリングとの最大の違いを受け取るためにそこに行くだろう、ある反復実行スレッドを決定するために実行時の状態に依存し、各スレッドが完成しますタスクの数)が異なる場合があります。反復が事前に知られていないされているスレッドに割り当てられているので、スレッドは、開始と不確実性の実行時間の仕上げとして。

  使用しないときのサイズは、各スレッドに割り当てられた一つの反復です。各スレッドへの1回の反復によってサイズ、サイズ分布を使用する場合、この使用は静的スケジューリングと同様です。

ヒューリスティック導か
   スケジューリングのための発見的スケジューリング方法は、すべてのスレッドが反復の異なる番号に割り当てられている、より大きな開始し、その後、徐々に減少します。各スレッドの開始時に反復の大きな塊に割り当てられ、その後徐々に減少しますの反復ブロックに割り当てられています。反復ブロックサイズが指定されたサイズの大きさに指数関数的に減少するサイズパラメータが指定されていない場合、その後の反復は1の最小ブロックサイズに縮小されます。

  あまり大きさは、もはや減少しないであろうしながら反復の数がそれぞれ割り当ては、徐々に減少するので、大きさは、各分配の反復の最小数を表します。特定のは、特定の情報コンパイラおよび関連マニュアルへの参照を必要とヒューリスティックアルゴリズムを使用しています。

スケジュールの概要
静的スケジューリング静的:、コンパイラのデバッグをループスレッドによって実行されたとき、すべてが固定という。タスクの各スレッドが固定されているが、いくつかの簡単なタスクサイクルを実行する可能性があるため、いくつかの遅い、最適ではありません。

動的スケジューリング動的:スレッドの実行速度に応じて、スレッドは自動的に新しいタスクまたはタスクのブロックを要求するタスクを持って、各受信タスクブロックが固定されています。

ヒューリスティック導かれた:最初の大きな指標が下落した後、各タスクに割り当てられたタスクは小さいです。多数のタスクがサイクルに必要がある場合、スレッドに割り当てられたタスクの番号の先頭には、最後の少しの間は、各スレッドに小さなタスクを与えるために、タスクのスレッドが平衡に達することができます。

OpenMPのプログラミング技術の概要
1.実行するためにあまりにも多くのスレッドに分割した場合のサイクルの数が少ないが、総走行時間がスレッドまたはスレッドの実装未満であるようなもの、及びエネルギー消費を増加させることができます。

2.スレッドの数は、CPUコアの数よりもはるかに大きいを設定した場合は、オーバーヘッドおよびスケジューリングタスク切り替えが多い、全体の効率が低下します。

ネストされたループ3.、それほど頻繁にアウターループ反復場合、ある程度のCPUコアの数、将来の場合、作成されたスレッドの数は、おそらくCPUコアの数よりも少ないであろう。ロードバランシングを実現するためにも、内部ループ・ロード・バランシングの存在ならば、それは困難なスケジューリング外側のループです。
---------------------
著者:ArrowYL
出典:CSDN
オリジナルます。https://blog.csdn.net/ArrowYL/article/details/81094837
免責事項:この記事ブロガーのオリジナルの記事、複製など、ボーエンのリンクを添付してください!

おすすめ

転載: www.cnblogs.com/jfdwd/p/10960544.html