マルチスレッド:どの考え方

太陽蛇ゴリー
少し前に、偶然にも、ある種の競争に参加しましたが、すぐに研究になりました。 この研究は、 Javaブログとアルゴリズムの読者に等しく興味を引く結果をもたらしました。 一度に2か所に配置できないため、この投稿を2つの部分に分割することにしました。 キャプテンがおそらくあなたに言うように、このパートではJavaの結果について説明します。
ちなみに、私は研究チームからHabréに登録しているだけではありません。感謝を表明したい場合は、 markizicekeeperを忘れないでください。

私たちは何について話しているのですか?

いくつかの計算やタスクを並列化する際に留意する必要があることについてです。 このような計算を実装するためのいくつかのオプションを検討し、特定のソリューションがより効果的である理由を説明します。 行きましょう。

ああ、まあ、これらはあなたの流れです!

最も単純なものから始めましょう。多くのタスクを実行する必要がある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 カウント ;
}
}


残念なことに、このコードにはいくつかの欠点があります。 まず、正しく動作しない場合があります。
  1. longはアトミック型ではありません。 AtomicLongを使用する必要があります; <また、 カウントへのアクセスを同期することができ、この方法で:
    長いデルタ=何か taskId ;
    delta + = somethingElse taskId ;
    delta + = commit taskId ;
    synchronized count {
    数える + =デルタ;
    }
    同期ブロックからの結果の計算は、非常に長い時間がかかり、それによって他のスレッドの操作をブロックする可能性があるため、実行する必要があります。
  2. キューに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章を読むことを強くお勧めします。

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


All Articles