少し前に、偶然にも、ある種の競争に参加しましたが、すぐに研究になりました。 この研究は、
Javaブログと
アルゴリズムの読者に等しく興味を引く結果をもたらしました。 一度に2か所に配置できないため、この投稿を2つの部分に分割することにしました。 キャプテンがおそらくあなたに言うように、このパートではJavaの結果について説明します。
ちなみに、私は研究チームからHabréに登録しているだけではありません。感謝を表明したい場合は、
markizと
icekeeperを忘れないでください。
私たちは何について話しているのですか?
いくつかの計算やタスクを並列化する際に留意する必要があることについてです。 このような計算を実装するためのいくつかのオプションを検討し、特定のソリューションがより効果的である理由を説明します。 行きましょう。
ああ、まあ、これらはあなたの流れです!
最も単純なものから始めましょう。多くのタスクを実行する必要がある1つのコアを持つコンピューターがあります。 提出されたタスクの識別子に応じていくつかのアクションを実行する作業メソッドを持つ特定のクラスTaskを作成しましょう。 同様に、多くのタスクを実行するクラス:
パブリック クラス Archiver {
public static long count = 0 ;
クラス Task {
// ...
void work ( String taskId ) {
カウント+ =何か( taskId ) ;
count + = somethingElse ( taskId ) ;
count + = commit ( taskId ) ;
}
}
public static void main ( String [ ] args ) {
for ( 文字列 id : args )
新しいタスク( ) 。 仕事 ( id ) ;
システム アウト 。 println ( Task。count ) ;
}
}
当分の間、すべてが完璧でしたが、サーバーがアップグレードされ、実際のデュアルコアプロセッサができました! もちろん、このコードを修正し、複数のスレッドで動作させる必要があります。
あまり同期しないでください、若いパダワン
少し考えた後、私たちは私たちが望んでいたことを実現するために急いで、このようなコードを書きました:
パブリック クラス Archiver {
public static long count = 0 ;
クラス Task は Runnableを 実装します {
// ...
public void run ( ) {
タイムアウト= 0 ;
while ( timeout < 10 ) {
if ( haveMoreTasks ( ) ) {
taskId = getTask ( ) ;
カウント+ =何か( taskId ) ;
count + = somethingElse ( taskId ) ;
count + = commit ( taskId ) ;
} else {
スレッド 睡眠 ( 10 ) ;
タイムアウト++;
}
}
}
}
private static LinkedList < String >タスク;
synchronized public static String getTask ( ) {
タスクを返します。 pollLast ( ) ;
}
synchronized public static boolean haveMoreTasks ( ) {
タスクを返します。 サイズ ( ) > 0 ;
}
同期された public static void addTask ( String taskId ) {
タスク。 追加 (タスク) ;
}
public static void main ( String [ ] args ) {
for ( 文字列 id : args )
addTask ( id ) ;
int threadNum = ランタイム 。 getRuntime ( ) 。 availableProcessors ( ) ;
for ( int i = 0 ; i < threadNum ; i ++ )
(スレッド[ i ] = 新しい スレッド ( 新しいタスク( ) ) ) 。 開始 ( ) ;
boolean finished = false ;
while ( ! finished ) {
finished = true ;
for ( int i = 0 ; i < threadNum ; i ++ )
終了&= ! スレッド[ i ] 。 isAlive ( ) ;
{
スレッド 睡眠 ( 10 ) ;
} catch ( 例外 e ) {
e。 printStackTrace ( ) ;
}
}
システム アウト 。 println (カウント) ;
}
}
残念なことに、このコードにはいくつかの欠点があります。 まず、正しく動作しない場合があります。
- longはアトミック型ではありません。 AtomicLongを使用する必要があります; <また、 カウントへのアクセスを同期することができ、この方法で:
長いデルタ=何か( taskId ) ;
delta + = somethingElse ( taskId ) ;
delta + = commit ( taskId ) ;
synchronized ( count ) {
数える + =デルタ;
}
同期ブロックからの結果の計算は、非常に長い時間がかかり、それによって他のスレッドの操作をブロックする可能性があるため、実行する必要があります。 - キューに1つの要素があり、両方のスレッドが同時に作業を終了します。 次に、そのうちの1つがhaveMoreTasks()に応答してtrueになり、2番目が同じ答えを取得します。その後、1番目が値を取得し、2番目がブレークして例外をスローします。
ただし、これらのエラーは別のエラーほど重要ではありません。タスクが多く、実行時間がそれほど長くない場合、シングルスレッドバージョン
よりも
長く機能します。 なんで? 答えは簡単ですが、必要なソリューションを作成することでしか解決できません。
正しい方法
幸いなことに、Javaにはデータを並列化するための多くのツールがあります。 含む
-ExecutorService 。 以下のコードは、他の発明されたすべてのオプションよりもはるかに高速に動作します。
パブリック クラス Archiver は Runnable {
public static AtomicLong count = AtomicLong (0 ) ;
public static String [ ] tasks ;
クラス Task は Runnableを 実装します {
タスク( String taskId ) {
これ 。 taskId = taskId ;
}
// ...
public void run ( ) {
長いデルタ=何か( taskId ) ;
delta + = somethingElse ( taskId ) ;
delta + = commit ( taskId ) ;
synchronized ( count ) {
数える addAndGet (デルタ) ;
}
}
}
public void run ( ) {
intプロセッサ= ランタイム 。 getRuntime ( ) 。 availableProcessors ( ) ;
ExecutorServiceプール=エグゼキューター。 newFixedThreadPool (プロセッサー) ;
for ( 文字列 id :タスク)
プール。 実行 ( 新しいタスク( id ) ) ;
プール。 シャットダウン ( ) ;
{
if ( ! pool。awaitTermination ( 20 、TimeUnit。MINUTES ) )
新しい InterruptedExceptionを スロー ( 「制限時間を超えました」 ) 。
} catch ( InterruptedException e ) {
e。 printStackTrace ( ) ;
プール。 shutdownNow ( ) ;
システム exit ( 1 ) ;
}
システム アウト 。 println (カウント) ;
}
public static void main ( String [ ] args ) {
tasks = args ;
新しい スレッド ( 新しいアーカイバ( ) ) 。 開始 ( ) ;
}
}
ソースを見ると、本当に重大なイデオロギーの違いが1つしかありません。解放されたフローにタスクを分配するエンティティがあります。 時間の増加は、同期が少ないためです。
同期が遅い。 最小化する必要があります!
あとがき
Javaには、特定のタスクを実行するための多くの優れた組み込みツールがありますが、人々は車輪の再発明を続けています。 この記事では、これは効果がないだけでなく、時には追加の難しいエラーにつながることが示されました。 ExecutorServiceと他のすべてについて聞いたことがない人のために、Joshua Bloshによるすばらしい本
Effective Java(2nd Edition) 、特にその10章を読むことを強くお勧めします。