Mt:負荷の高いサーバー用のC言語

こんにちは、ハブロビテス!

追加の言語ツールを導入することにより、Cでのサーバープログラムの作成を簡素化する方法についての議論のアイデアを提供したいと思います。 このトピックは、マルチスレッドまたは非同期コードの作成に対処しなければならなかったすべての開発者にとって興味深いものになると思います。

現時点では、ツールキット(パーサージェネレーター、Cパーサー、および部分的にC ++)の作成をほぼ完了しています。これにより、言語拡張機能をサポートするトランスレーターの作成を開始できます。 しかし、続行する前に、ワークショップで同僚と相談し、志を同じくする人を見つけたいと思います。

マルチスレッドの非同期サーバーについてです。 非同期-つまり、イベントベースの顧客サービスロジック(epoll / kqueue)を使用し、一度に1つのスレッドで複数のクライアントにサービスを提供します。 マルチスレッド-最新のマルチコアプロセッサをすぐに利用できることを意味し、同じプロセス内の複数のスレッドによる接続の並行メンテナンスを実行します。

非同期サービスモデルを使用すると、非常に高い負荷で動作するように最も準備されたサーバーを作成できます。 ただし、非同期(非ブロッキング)コードは、同期ブロッキングコードよりも作成が困難です。 さらに、非同期マルチスレッドコードを記述することははるかに困難です。共有データへのアクセスを同期するときにミスを犯しやすく、そのようなエラーを検出することは非常に困難です。 これは、実際にはマルチスレッドのサポートを完全に拒否することが賢明であるという事実につながります。

このようなプログラムの作成である程度の経験を積んだ後、スレッド同期の主な問題の解決策が標準的な方法で解決されていることに気づき始めました。その使用は言語レベルで自動化でき、開発者のタスクを大幅に簡素化できます。 次に、同様のアプローチを使用して、非同期イベント処理コードの記述を簡素化できることを見ました。 ソリューションの明らかな一般性のために、これらの直接関係のないタスクのための言語追加の単一セットを提案します。

アイデアは非常に単純であるため、複雑さを感じさせないために、Cへの追加に関する主なポイントをリストします。これらの追加を「Mt言語」と呼びます。

だから山は:C ++ではなくCがベースとして選択された理由を説明します。 主な理由は、ソーステキストのセマンティック分析に関してC ++の非常に高い複雑さです。 過去1年間、C ++パーサーとC ++コードの静的分析システムを作成しようとしました。 結果-CパーサーとCスタイルの構文解析は非常に簡単でしたが、C ++構文解析は完全にはほど遠いです:作業量が大きすぎます。 したがって、純粋なCに依存し、実装が非常に簡単なC ++機能で補完することにより、実用的な結果を得ることができると判断しました。

次に、各追加について詳しく説明します。 C ++のコードの例をいくつか示します。これは、Mtの同等のコードとの関連付けが簡単だからです。

1.自動ミューテックス制御


一般的なリクエスト処理スキームを検討してください。 サーバーの実装であるMyFrontendクラスのオブジェクトがあるとします。 クライアントからリクエストを受信して​​デコードした後、サーバーオブジェクトprocessRequest()のメソッドを呼び出します。 このメソッドは、サーバーの内部状態の変更を引き起こすいくつかの操作を実行します。 たとえば、リクエストを処理した結果、リクエストの数num_requestsのカウンターが増加するとします。

class MyFrontend
{
private :
Mutex state_mutex ;
unsigned num_requests ;

public :
void processRequest ( )
{
state_mutex. lock ( ) ;
++ num_requests ;
state_mutex. unlock ( ) ;
}
} ;


Mtは、手動同期の必要性を排除します。

async object MyFrontend
{
private :
unsigned num_requests ;

public :
async void processRequest ( )
{
++ num_requests ;
}
} ;


非同期オブジェクトは、オブジェクトのスレッドセーフクラスを宣言します。 このようなオブジェクトは非同期と呼ばれます。 指定されたMtコードの例は、指定されたC ++の例に相当するコードに変換されます。

ここで、リクエストを処理するために、MyFrontendがMyBackendオブジェクトのdoSomething()メソッドを呼び出し、num_backバックエンドアクセスカウンターをパラメーターとして渡す必要があるとします。 さらに、デッドロックの可能性について考えたくないし、すべてが非再帰的ミューテックスで動作することを望んでいます。 これを行うには、MyBackendにアクセスする前にstate_mutexを解放する必要があります。

class MyFrontend
{
private :
Mutex state_mutex ;
unsigned num_requests ;
unsigned num_back ;
MyBackend * backend ;

public :
void processRequest ( )
{
state_mutex. lock ( ) ;
++ num_back ;
unsigned tmp_num_back = tmp_num_back ;
state_mutex. unlock ( ) ;

backend - > doSomething ( tmp_num_back ) ;

state_mutex. lock ( ) ;
++ num_requests ;
state_mutex. unlock ( ) ;
}
} ;


Mtでも同じこと:

async object MyFrontend
{
private :
unsigned num_requests ;
unsigned num_back ;
MyBackend * backend ;

public :
async void processRequest ( )
{
call backend - > doSomething ( ++ num_back ) ;
++ num_requests ;
}
} ;


Mtトランスレーターは、state_mutexを使用して操作を個別に挿入し、パラメーターを渡すための一時変数を入力します。 呼び出し演算子は、別のオブジェクトの非同期メソッドが呼び出される間、state_mutexロックを解放することを強調するために導入されました。 さらに、これにより、式で非同期メソッドを呼び出すことを明示的に禁止できます。

上記の例に加えて、ロックを使用する他の多くのケースが自動化されています:イベントの待機(イベントは通常、待機の完了のための条件を保護するミューテックスと組み合わせて使用​​されます)、同じオブジェクトの別のメソッドからオブジェクトの非同期メソッドを呼び出します。

したがって、オブジェクトの状態の単純なロックを管理するときにミスを犯すリスクを最小限に抑えます。 私の観察によれば、オブジェクトへのアクセスの同期が1つではなく複数のロックを使用して実行される場合、おそらく失敗した設計があります:オブジェクトを複数の独立したオブジェクトに分割するか、ロックを1つにマージする必要があります。 したがって、1つのミューテックスで最も単純なケースを自動化することにより、ほとんどの同期の問題を解決します。 手動および他のプリミティブを使用して同期する機能
保存しました。

2.非同期処理チェーン


私の意見では、これは最も興味深い珍しいアイデアです。 実際のサービスの動作を示す例を使用して説明します。

私たちのサーバーが写真ホスティングのフロントエンドであるとしましょう。 ユーザーの写真を含むHTMLページを顧客に返すことを実現したいと考えています。 このページには、写真の所有者のプロファイルと写真自体の情報が表示されます。 写真は、写真の所有者の友人のみが表示できます。 したがって、HTTP要求に応答するには、多数の内部サービスをポーリングして必要な情報を収集する必要があります。

リクエスト処理アルゴリズム:
  1. Cookie Cookieを認証サーバーに送信して、ユーザーを識別します(check_auth)。
  2. 承認を確認したら、ソーシャルグラフを提供するサーバーにリクエストを送信して、ユーザーが友達かどうかを確認します(check_friends)。
  3. 写真所有者に個人データリポジトリ(get_userinfo)にリクエストを送信します。
  4. check_friendsおよびget_userinfoから回答を受け取った後、HTMLページを生成してクライアントに送信します。
操作2は、要求1に対する応答を受信した後にのみ実行できます。要求3は、要求1と2に依存せず、要求と並行して実行する必要があります。

Cの実装をスケッチしましょう。この例を理解することはできません。 彼の仕事は、そのような単純な非同期処理チェーンの手動実装がいかに厄介であるかを示すことです。

// .
typedef struct {
// "" , .
MReferenced parent_referenced ;
// .
MMutex state_mutex ;

uint64_t owner_id ;
uint32_t photo_id ;

// 'true', .
bool cancel ;

// get_userinfo?
bool got_userinfo ;
// ( ).
uint32_t owner_carma ;

// check_friends?
bool got_friends ;
} HttpPhotoData ;

static void http_photo_data_init ( HttpPhotoData * data )
{
m_referenced_init ( ( MReferenced * ) data ) ;
m_mutex_init ( & data - > state_mutex ) ;

data - > cancel = false ;
data - > got_userinfo = false ;
data - > got_friends = false ;
}

static void http_photo__get_userinfo_ret ( uin32_t owner_carma,
void * _data ) ;

static void http_photo__check_auth_ret ( uint64_t client_id,
void * _data ) ;

static void http_photo__end ( HttpPhotoData * data ) ;

// .
void http_photo ( char * cookie_str,
uint64_t owner_id,
uint32_t photo_id )
{
HttpPhotoData * data = m_malloc ( sizeof ( HttpPhotoData ) ) ;
http_photo_data_init ( data ) ;

data - > owner_id = owner_id ;
data - > photo_id = photo_id ;

{
m_referenced_ref ( ( MReferenced * ) data ) ;

MCallbackDesc cb ;
m_callback_desc_init_refdata ( & cb, http_photod__get_userinfo_ret, data ) ;

get_userinfo ( & cb, owner_id ) ;
}

{
m_referenced_ref ( ( MReferenced * ) data ) ;

MCallbackDesc cb ;
m_callback_desc_init_refdata ( & cb, http_photod__check_auth_ret, data ) ;

check_auth ( & cb, cookie_str ) ;
}

m_referenced_unref ( ( MReferenced * ) data ) ;
}

// get_userinfo.
static void http_photo__get_userinfo_ret ( uint32_t owner_carma,
void * _data )
{
HttpPhotoData * data = ( HttpPhotoData * ) _data ;

m_mutex_lock ( & data - > state_mutex ) ;

if ( data - > cancel ) {
m_mutex_unlock ( & data - > state_mutex ) ;
return ;
}

data - > owner_carma = owner_carma ;
data - > got_userinfo = true ;

if ( data - > got_friends ) {
// , .
http_photo_end ( data ) ;
}

m_mutex_unlock ( & data - > state_mutex ) ;
}

// check_auth.
static void http_photo__check_auth_ret ( uint64_t client_id,
void * _data )
{
HttpPhotoData * data = ( HttpPhotoData * ) _data ;

m_referenced_ref ( ( MReferenced * ) data ) ;

MCallbackDesc cb ;
m_callback_desc_init_refdata ( & cb, http_photo__check_friends_ret, data ) ;

check_friends ( & cb, client_id, data - > owner_id ) ;
}

// check_friends.
static void http_photo__check_friends_ret ( bool friends,
void * _data )
{
HttpPhotoData * data = ( HttpPhotoData * ) _data ;

m_mutex_lock ( & data - > state_mutex ) ;
if ( data - > cancel ) {
m_mutex_unlock ( & data - > state_mutex ) ;
return ;
}

data - > got_friends = true ;

if ( ! friends ) {
// . .
data - > cancel = true ;
m_mutex_unlock ( & data - > state_mutex ) ;
return ;
}

if ( data - > got_userinfo ) {
// , .
http_photo_end ( data ) ;
}

m_mutex_unlock ( data - > state_mutex ) ;
}

// .
static void http_photo_end ( HttpPhotoData * data )
{
photo_send_page ( data - > owner_id, data - > photo_id, data - > owner_carma ) ;
}


結果のコードは複雑で、読みにくいです。 書き込みのほとんどの時間は、操作の必要な順序の定期的なメンテナンスに費やされました。

Mtでの同等の実装:

async chain void http_photo ( char * cookie_str,
uint64_t owner_id,
uint32_t photo_id )
{
async call GetUserinfoCall get_userinfo ( owner_id ) ;

async call check_auth ( cookie ) ;
gives uint64_t client_id ;

async call check_friends ( client_id, owner_id )
gives bool friends ;

if ( ! friends ) {
//
return ;
}

GetUserinfoCall gives uint32_t owner_carma ;

// HTTP- .
photo_send_page ( owner_id, photo_id, owner_carma ) ;
}


必要な実行順序を整理するという通常の作業からほぼ完全に解放されていることに注意してください。 さらに、生成されたCコードはマルチスレッドプログラムで使用できます。

上記の例では、2つの新しい演算子が使用されています。示されている演算子に加えて、非同期呼び出しの結果を早期に処理して、対応する失敗した演算子呼び出しに到達する前にエラーが発生した場合にチェーンを中断することができます。 チェーンの状態構造に含まれるデータに追加のデストラクタを指定することもできます。

Cで非同期チェーンを変換するときにMtが行う主な作業は次のとおりです。これらの各操作は、自動化に適していることがわかります。

Mtの単一の関数では表現できない複雑な操作チェーンの並列実行は、いくつかの異なる非同期チェーンを使用して実装できます。 つまり、チェーン内のブランチが非常に複雑であるため、単一の関数で記述できない場合、複数の関数の構成として記述を作成できます。

Mtの助けを借りて、非同期呼び出しからの応答を待つことによって中断されるループと条件付きジャンプを、通常のコードを記述するときと同じシンプルで馴染みのある形式で説明できます。 3つの独立したキャッシュで順番に値を検索するとします。

async chain void search ( int key )
{
for ( int i = 0 ; i < 3 ; i ++ ) {
async call search_cache ( key, i /* cache id */ )
gives int value ;

if ( value > 0 ) {
printf ( "VALUE: %d \n " , value ) ;
break ;
}
}
}


したがって、非同期処理チェーンの場合、MtはCと比較して表現力と使いやすさの飛躍的な向上を実現できます。

3.注釈


注釈は、注釈語を使用した追加のサポート情報のコードの紹介です。 単語に注釈を付けることは、constおよびvolatile修飾子が立つところであればどこでもかまいません。 注釈を付ける単語はすべて、文字「$」で始まります。 例:
int * $nonnull ptr ; // NULL
MyObject * $heap myobj ; //


Mtトランスレータは、追加のチェックに注釈を使用する静的コード検証システムの基盤でもあります。

4. C ++機能の借用


トランスレータの実装の観点から見ると、C ++と比較してC ++の最も複雑な機能は、引数のデータ型に対する言語構造の意味の依存性です。 これはまず、関数名のオーバーロード(演算子のオーバーロードを含む)のルールと、部分的な特殊化を考慮したテンプレートの選択のルールに関係します。 このような依存関係は、注入で使用される関数またはテンプレートを決定するために、関数またはテンプレートへのすべての引数のデータ型を導出する必要があることを意味します。 これには、型変換ルールを完全かつ正確に実装する必要があります。 これらすべてが、かなりの量の作業を構成しています。

同時に、名前のオーバーロードは、言語の物議を醸すプロパティです。 たとえば、Go言語の作成者は、FAQに有意義なエントリがあるため、拒否しました

Mtは、関数名と演算子のオーバーロード、多重継承、例外、RTTIをオーバーロードしません。 部分的な特殊化のないテンプレートは実装がそれほど難しくなく、これは一般化されたコンテナクラスを作成するのに十分なので、言語にテンプレートを含めることは理にかなっています。 名前のオーバーロードがない場合、テンプレートははるかに単純で簡潔なツールになります。C++のようなメタプログラミング機能はありません。

5.リンクと弱いリンク


演算子のオーバーロードがないため、std :: auto_ptrやboost :: shared_ptrなど、Mtに「スマートポインター」の便利な類似物を実装することはできません。 代わりに、リンクカウントメカニズムが言語レベルで提案されています。
int @a ; //
int @@b ; //


基本バージョンでは、単純な参照カウントを使用します。 ビルトインリンクカウンター
各同期または非同期Mtオブジェクト(オブジェクトと非同期オブジェクト)に。 リンク管理に対するこのアプローチでは、プログラマはメモリリークにつながる循環リンクの作成を避ける必要があります。 弱いリンクは、ループを中断するために使用されます。 将来的には、より複雑なガベージコレクターを使用する可能性を検討できますが、今ではこの必要性はわかりません。

演算子のオーバーロードがない場合、MtでC ++リンク(intおよびa形式)をサポートする必要はありません。

言語レベルでリンクを実装すると、C ++テンプレートで構築できるよりも完全で便利なメカニズムを取得できることに注意してください。

おわりに


山に含める予定の主な機能の概要を説明しました。 それらに加えて、開発を簡素化し、プログラムの予想エラー数を減らすことを目的とした開発の他のアイデアがあります。

Mt言語翻訳者は、オープンソースプロジェクトです。 現在、C ++言語パーサーのサブセットとして存在し、svnリポジトリmync.svn.sourceforge.net/svnroot/mync/trunkに 「scruffy」という作業名で存在しています。

C / C ++言語の特定のプロパティを追加または変更することについて考えやアイデアがある場合は、それらを発声することをお勧めします:おそらくそれらの実用的なテストの時が来ました。

このプロジェクトに参加することをお勧めします。 新しい知識と経験を提供できる独立した仕事のトピックを探している場合、そして高品質で成功した製品に成功した場合は、新しい言語コンパイラが優れた選択肢になります。 Mtが目指している問題があまり関係ない場合は、C ++パーサーとC / C ++プログラムの静的分析システムの開発に興味があるかもしれません。

一緒にもっとできる!

ご清聴ありがとうございました。

Source: https://habr.com/ru/post/J111514/


All Articles