Java8とC#でマルチスレッドとマルチプロセスの勉強をする。
1.はじめに
1-1.イメージ
マルチプロセスとマルチスレッドの簡単なイメージは以下になる。
マルチプロセスの場合は、メモリ空間が別になるので、データのやり取りにはプロセス間通信が必要になる。
逆にマルチスレッドでは、メモリ空間が同じなため、複数スレッドが同時に同じデータにアクセスして、「レースコンディション」のバグがでないように、排他制御を行い、「スレッドセーフ」にする必要がある。
1-2.非同期処理と並列処理について
マルチスレッドのメリットを2つ記載する。
・「フリーズ」状態にならず、すぐに応答する
→非同期処理(並行)
・パフォーマンスが良くなる
→並列処理
2.Javaのマルチスレッド
Java8でGUIアプリケーションを作成しながらマルチスレッドの学習を行う。
以下の画面のアプリケーションを作成する。
STARTボタンを押すと、1秒毎に画面中央に数値を表示する。
2-1.スレッドなし
package sample;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JTextField;
public class MainView {
private JFrame frame;
private JTextField textField;
/**
* Launch the application.
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
MainView window = new MainView();
window.frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* Create the application.
*/
public MainView() {
initialize();
}
/**
* Initialize the contents of the frame.
*/
private void initialize() {
frame = new JFrame();
frame.setBounds(100, 100, 450, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton btnNewButton = new JButton("START");
btnNewButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
for(int i = 0; i < 5; i++) {
textField.setText("" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
}
}
}
});
frame.getContentPane().add(btnNewButton, BorderLayout.NORTH);
JButton btnNewButton_1 = new JButton("STOP");
btnNewButton_1.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
textField.setText("");
}
});
frame.getContentPane().add(btnNewButton_1, BorderLayout.SOUTH);
textField = new JTextField();
frame.getContentPane().add(textField, BorderLayout.CENTER);
textField.setColumns(10);
JCheckBox chckbxNewCheckBox = new JCheckBox("New check box");
frame.getContentPane().add(chckbxNewCheckBox, BorderLayout.EAST);
}
}
本来はボタンを押した後は「0, 1, 2, 3 ,4」と表示されてほしいが、ボタンを押した後画面が固まり、最後に「4」だけが表示される。
2-2.SwingWorker
package sample;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.swing.SwingWorker;
public class MainView {
private JFrame frame;
private JTextField textField;
/**
* Launch the application.
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
MainView window = new MainView();
window.frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* Create the application.
*/
public MainView() {
initialize();
}
/**
* Initialize the contents of the frame.
*/
private void initialize() {
frame = new JFrame();
frame.setBounds(100, 100, 450, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton btnNewButton = new JButton("START");
btnNewButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// ボタンを無効化
btnNewButton.setEnabled(false);
// SwingWorkerで非同期実行
SwingWorker<Void,Void> sw = new SwingWorker<Void,Void>(){
@Override
protected Void doInBackground() throws Exception {
for(int i = 0; i < 5; i++) {
textField.setText("" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
}
}
return null;
}
@Override
protected void done() {
// ボタンを有効化
btnNewButton.setEnabled(true);
}
};
sw.execute();
}
});
frame.getContentPane().add(btnNewButton, BorderLayout.NORTH);
JButton btnNewButton_1 = new JButton("STOP");
btnNewButton_1.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
textField.setText("");
}
});
frame.getContentPane().add(btnNewButton_1, BorderLayout.SOUTH);
textField = new JTextField();
frame.getContentPane().add(textField, BorderLayout.CENTER);
textField.setColumns(10);
JCheckBox chckbxNewCheckBox = new JCheckBox("New check box");
frame.getContentPane().add(chckbxNewCheckBox, BorderLayout.EAST);
}
}
doInBackgroundの中でtextField.setText("" + i);がエラーにならないのはなぜだろう?
ワーカースレッドからUIを操作したらエラーになるのでは?
2-3.一般的な非同期処理
package sample;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JTextField;
public class MainView {
private JFrame frame;
private JTextField textField;
ExecutorService es = Executors.newCachedThreadPool();
/**
* Launch the application.
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
MainView window = new MainView();
window.frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* Create the application.
*/
public MainView() {
initialize();
}
/**
* Initialize the contents of the frame.
*/
private void initialize() {
frame = new JFrame();
frame.setBounds(100, 100, 450, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton btnNewButton = new JButton("START");
btnNewButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
CompletableFuture.supplyAsync(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
textField.setText("" + i);
} catch (InterruptedException e1) {
}
}
return "";
}, es);
}
});
frame.getContentPane().add(btnNewButton, BorderLayout.NORTH);
JButton btnNewButton_1 = new JButton("STOP");
btnNewButton_1.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
textField.setText("");
}
});
frame.getContentPane().add(btnNewButton_1, BorderLayout.SOUTH);
textField = new JTextField();
frame.getContentPane().add(textField, BorderLayout.CENTER);
textField.setColumns(10);
JCheckBox chckbxNewCheckBox = new JCheckBox("New check box");
frame.getContentPane().add(chckbxNewCheckBox, BorderLayout.EAST);
}
}
#3.C#のマルチスレッド
3-1.asyc/await
ボタンを押した場合に、押された回数とカウンタを画面に表示するプログラム。
インスタンス変数のiはボタンを押される度にカウントアップする。
ボタンを押された後にiの値をjに格納している。
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form3 : Form
{
private int i;
public Form3()
{
InitializeComponent();
}
public int getI()
{
return i++;
}
private async void button1_Click(object sender, EventArgs e)
{
int j = getI();
for (int k = 10; k < 20; k++)
{
textBox1.Text = j + ":" + k;
await Task.Run(() => System.Threading.Thread.Sleep(1000));
}
}
}
}
画面にはjの値とカウンタkを結合して表示しているが、ボタンを連打した場合に、以下の用に表示されるが、その理由について確認する。
0:10
'WindowsFormsApp1.exe' (CLR v4.0.30319: WindowsFormsApp1.exe): 'C:\Windows\Microsoft.Net\assembly\GAC_MSIL\mscorlib.resources\v4.0_4.0.0.0_ja_b77a5c561934e089\mscorlib.resources.dll' が読み込まれました。モジュールがシンボルなしでビルドされました。
1:10
2:10
0:11
3:10
1:11
4:10
2:11
0:12
3-1-1.疑問に思ったこと
メインスレッド(UIスレッド)でsleepを別スレッドで動かしているので、非同期になるので、処理が終了する前に画面操作ができるようになるのは理解できるが、処理が終わる前に再度ボタンを押したときの動きが理解できなかった。
jの値を何個も同時(上の例では0,1,2,3,4)に保持される仕組みがわからない。別スレッドを作って、スレッドの中にその変数を保持するのなら理解できる。
3-1-2.糖衣構文
async/awaitは糖衣構文(シンタックスシュガー)なのは知っていたので、実際のコードがどうなっているかを確認した。
確認するために、ILSpy(https://github.com/icsharpcode/ILSpy/releases)をダウンロードした。
exeファイルをILSpyにD&Dしてソースコードを確認したが、元のコードのまま。(async/awaitが表示されている)
ILSpyのOption設定で、C#5以降のチェックを外したら実際のコードが表示された。
// WindowsFormsApp1.Form3.<button1_Click>d__3
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
[CompilerGenerated]
private sealed class <button1_Click>d__3 : IAsyncStateMachine
{
public int <>1__state;
public AsyncVoidMethodBuilder <>t__builder;
public object sender;
public EventArgs e;
public Form3 <>4__this;
private int <j>5__1;
private int <k>5__2;
private TaskAwaiter <>u__1;
private void MoveNext()
{
int num = <>1__state;
try
{
if (num != 0)
{
<j>5__1 = <>4__this.getI();
<k>5__2 = 10;
goto IL_0114;
}
TaskAwaiter awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
goto IL_00fb;
IL_00fb:
awaiter.GetResult();
<k>5__2++;
goto IL_0114;
IL_0114:
if (<k>5__2 < 20)
{
<>4__this.textBox1.Text = <j>5__1 + ":" + <k>5__2;
Console.WriteLine(<j>5__1 + ":" + <k>5__2);
awaiter = Task.Run(delegate
{
Thread.Sleep(1000);
}).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<button1_Click>d__3 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
goto IL_00fb;
}
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
4.Javaのクラス、説明
4-1.一覧
名称 | 種類 | 説明 |
---|---|---|
Executors | クラス | このパッケージで定義されたExecutor、ExecutorService、ScheduledExecutorService、ThreadFactory、およびCallableクラス用のファクトリおよびユーティリティ・メソッドです。このクラスは、次の種類のメソッドをサポートします。 |
ExecutorService | インタフェース | 終了を管理するメソッド、および1つ以上の非同期タスクの進行状況を追跡するFutureを生成できるメソッドを提供するExecutor |
ScheduledExecutorService | インタフェース | 指定された遅延時間後または定期的にコマンドを実行するようにスケジュールできるExecutorService |
Future | インタフェース | 非同期計算の結果を表す |
4-2.用語
4-2-1.アンバウンド形式
JavaDocにある「アンバウンド形式のキューなしで動作する~」などのアンバウンドとはunboundedで、無制限を表す。「サイズ制限のないキュー」という意味。
4-3.中断方法
4-3-1.Futureクラスのcancel(true)
package sample;
import java.util.Date;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class Sample {
public static void main(String[] args) {
Sample sample = new Sample();
ExecutorService es = Executors.newCachedThreadPool();
methodA();
Future<String> future = (Future<String>) es.submit(() -> sample.methodB());
try {
future.get(3, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException e) {
System.out.println("例外が発生");
} catch (TimeoutException e) {
System.out.println(new Date() +"タイムアウトが発生");
future.cancel(true); //falseにすると中断が動かない
//es.shutdownNow();
}
methodC();
}
public static void methodA() {
System.out.println("method A:" + new Date());
}
public void methodB() {
System.out.println("method B START");
long start_point = System.currentTimeMillis();
for(int i = 0; i < Integer.MAX_VALUE; i++) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(new Date() +"中断されました");
return;
}
if(System.currentTimeMillis() % 1000 == 0) {
System.out.print("."); // この行をコメントアウトすると中断が動かない
}
if(System.currentTimeMillis() - start_point >= 10000) {
System.out.println("method B END");
return;
}
}
}
public static void methodC() {
System.out.println("method C:"+ new Date());
}
}
future.cancel(true)を行うことで、cancelのフラグが立つ。
InterruptedExceptionをキャッチするか、Thread.currentThread().isInterrupted()でフラグを確認して、自分で処理する必要がある。
4-3-2.cancel(false)の使い道
cancel(false)は実行中の処理が中断されるわけではないので、どのように使うのか調べてみた。
submitは遅延実行になるので、予約されているだけで実際に動くのはgetを呼ばれた時になる。
getを呼ぶ前にcancel(false)を実行すると、get時にCancellationException例外になる。
4-4.例外処理
別スレッドで発生した例外をメインスレッドで使用する方法について調べてみた。
4-4-1.RuntimeExceptionでラップする
public static void main(String[] args) {
log.trace("traceです。");
log.error("errorです。");
Sample sample = new Sample();
ExecutorService es = Executors.newCachedThreadPool();
methodA();
Future<String> future = (Future<String>) es.submit(() -> {
try {
sample.methodB(true);
} catch (UserException e1) {
throw new RuntimeException(e1);
}
});
try {
future.get(3, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException e) {
log.error("例外です。",e);;
Throwable error = e.getCause(); // RuntimeException
Throwable error2 = error.getCause(); // UserException
log.error(error2.toString(),error);
} catch (TimeoutException e) {
log.error("タイムアウトです。");
future.cancel(true); //falseにすると中断が動かない
//es.shutdownNow();
}catch(Exception e) {
log.error("Exception",e);
}
methodC();
}
ExecutionExceptionが発生するので、getCause()を行うとRuntimeExceptionが、さらにgetCause()を行えば元の例外を取得できる。
4-4-2.別の変数に格納
public class Sample {
private UserException ue;
public static void main(String[] args) {
log.trace("traceです。");
log.error("errorです。");
Sample sample = new Sample();
ExecutorService es = Executors.newCachedThreadPool();
methodA();
Future<String> future = (Future<String>) es.submit(() -> {
try {
sample.methodB(true);
} catch (UserException e1) {
sample.ue = e1;
}
});
try {
future.get(3, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException e) {
log.error("例外です。", e);
} catch (TimeoutException e) {
log.error("タイムアウトです。");
future.cancel(true); //falseにすると中断が動かない
//es.shutdownNow();
} catch (Exception e) {
log.error("Exception", e);
}
if(sample.ue != null) {
log.error("例外がセットされています" + sample.ue.toString());
}
methodC();
}
クラス変数として例外を定義し、例外発生時に、そこに発生した例外をセットする。