Explicit parallelism - to take advantage of multiple processors
To reduce the cost of context switching
Implicit parallelism - to keep a single processor busy
Example Client-Server - a TCP echo server
public class TcpServer
{
private Socket _socket;
private Thread _serverThread;
public TcpServer() {
_socket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_serverThread = new Thread (Server);
_serverThread.Start ();
}
private void Server() {
_socket.Bind (new IPEndPoint (IPAddress.Any, 8080));
_socket.Listen (100);
while (true) {
var serverClientSocket = _socket.Accept ();
new Thread (Server) { IsBackground = true }.Start (serverClientSocket);
}
}
private void ServerThread(Object arg) {
try {
var socket = (Socket)arg;
socket.Send (Encoding.UTF8.GetBytes ("Echo"));
} catch(SocketException se) {
Console.WriteLine (se.Message);
}
}
}
public class TcpClient {
public void ConnectToServer() {
var socket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect (new IPEndPoint (IPAddress.Parse("127.0.0.1"), 8080));
var buffer = new byte[1024];
var receivedBytes = socket.Receive (buffer);
if (receivedBytes > 0) {
Array.Resize (ref buffer, receivedBytes);
Console.WriteLine (System.Text.Encoding.UTF8.GetString (buffer));
}
}
}
Warning
The focus of this course is on distributed (not parallel) systems. Nevertheless, you may find that you want to take advantage of parallel computing in your work. We encourage you to read Christopher and Thiruvathukal, http://hpjpc.googlecode.com, which contains many examples of parallel algorithms in Java. You may also find Läufer, Lewis, and Thiruvathukal’s Scala workshop tutorial helpful. See http://scalaworkshop.cs.luc.edu.
public class ParallelComputationWorkItem {
public readonly long LowerBound;
public readonly long UpperBound;
public readonly long Number;
public readonly ICollection<long> Divisors;
public ParallelComputationWorkItem(long lower, long upper, long number, ICollection<long> divisors) {
LowerBound = lower;
UpperBound = upper;
Number = number;
Divisors = divisors;
}
}
public class ParallelComputation {
public void PrintFactors(long number) {
var threads = new List<Thread>();
var divisors = new List<long> ();
var cpuCount = Environment.ProcessorCount;
long lower = 1;
for (var i = 0; i < cpuCount; i++) {
var thread = new Thread (Worker);
var upper = lower + (number / cpuCount);
thread.Start(new ParallelComputationWorkItem(lower, upper, number, divisors));
threads.Add (thread);
lower = upper;
}
foreach (var thread in threads) {
thread.Join ();
}
Console.WriteLine ("Divisors - ");
foreach (var divisor in divisors) {
Console.WriteLine (divisor);
}
}
private void Worker(object args) {
var workItem = (ParallelComputationWorkItem)args;
for (var i = workItem.LowerBound; i < workItem.UpperBound; i++) {
if (workItem.Number % i == 0) {
lock (workItem.Divisors) {
workItem.Divisors.Add (i);
}
}
}
}
}
Example Pipeline Processing - file compression
public class PipelineComputation {
private readonly Queue<byte[]> _readData;
private readonly Queue<byte[]> _compressionData;
private volatile bool _reading = true;
private volatile bool _compressing = true;
public PipelineComputation () {
_readData = new Queue<byte[]>();
_compressionData = new Queue<byte[]>();
}
public void PerformCompression() {
var readerThread = new Thread (FileReader);
var compressThread = new Thread (Compression);
var writerThread = new Thread (FileWriter);
readerThread.Start ();
compressThread.Start ();
writerThread.Start ();
readerThread.Join ();
compressThread.Join ();
writerThread.Join ();
}
private void FileReader() {
using (var stream = new FileStream("file.txt", FileMode.Open, FileAccess.Read)) {
int len;
var buffer = new byte[1024];
while ((len = stream.Read(buffer, 0, buffer.Length)) > 0) {
if (len != buffer.Length) {
Array.Resize (ref buffer, len);
}
lock (_readData) {
while (_readData.Count > 10) {
Monitor.Wait (_readData);
}
_readData.Enqueue(buffer);
Monitor.Pulse (_readData);
}
}
}
_reading = false;
}
private void Compression() {
var workLeft = false;
while (_reading || workLeft) {
workLeft = false;
byte[] dataToCompress = null;
lock (_readData) {
while (_reading && _readData.Count == 0) {
Monitor.Wait (_readData, 100);
}
workLeft = _readData.Count > 1;
if (_readData.Count > 0) {
dataToCompress = _readData.Dequeue ();
}
}
if (dataToCompress != null) {
var compressed = Compress(dataToCompress);
lock (_compressionData) {
while (_compressionData.Count > 10) {
Monitor.Wait (_compressionData, 100);
}
_compressionData.Enqueue (compressed);
Monitor.Pulse (_compressionData);
}
}
}
_compressing = false;
}
private static byte[] Compress(byte[] data) {
var memStream = new MemoryStream ();
using(var compressionStream = new GZipStream(memStream, CompressionMode.Compress)) {
compressionStream.Write(data, 0, data.Length);
}
return memStream.ToArray ();
}
private void FileWriter() {
using (var stream = new FileStream("file.gz", FileMode.OpenOrCreate, FileAccess.Write)) {
var workLeft = false;
while (_compressing || workLeft) {
workLeft = false;
byte[] compressedData = null;
lock (_compressionData) {
while (_compressionData.Count == 0 && _compressing) {
Monitor.Wait (_compressionData, 100);
}
workLeft = _compressionData.Count > 1;
if (_compressionData.Count > 0) {
compressedData = _compressionData.Dequeue ();
}
}
if (compressedData != null) {
stream.Write (compressedData, 0, compressedData.Length);
}
}
}
}
}
public class ConcisePipelineComputation
{
public ConcisePipelineComputation () {
}
public void PerformCompression() {
var fileBlocks = new ThreadedList<byte[]>(FileReader());
var compressedBlocks = new ThreadedList<byte[]> (Compression(fileBlocks));
FileWriter (compressedBlocks);
}
private IEnumerable<byte[]> FileReader() {
using (var stream = new FileStream("file.txt", FileMode.Open, FileAccess.Read)) {
int len;
var buffer = new byte[1024];
while ((len = stream.Read(buffer, 0, buffer.Length)) > 0) {
if (len != buffer.Length) {
Array.Resize (ref buffer, len);
}
yield return buffer;
}
}
}
private IEnumerable<byte[]> Compression(IEnumerable<byte[]> readBuffer) {
foreach (var buffer in readBuffer) {
yield return Compress (buffer);
}
}
private static byte[] Compress(byte[] data) {
var memStream = new MemoryStream ();
using(var compressionStream = new GZipStream(memStream, CompressionMode.Compress)) {
compressionStream.Write(data, 0, data.Length);
}
return memStream.ToArray ();
}
private void FileWriter(IEnumerable<byte[]> compressedBuffer) {
using (var stream = new FileStream("file.gz", FileMode.OpenOrCreate, FileAccess.Write)) {
foreach (var buffer in compressedBuffer) {
stream.Write (buffer, 0, buffer.Length);
}
}
}
}
public class ThreadedList<T> : IEnumerable<T>
{
private readonly IEnumerable<T> _list;
public ThreadedList (IEnumerable<T> list){
_list = list;
}
public IEnumerator<T> GetEnumerator ()
{
return new ThreadedEnumerator<T>(_list.GetEnumerator ());
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator ()
{
return GetEnumerator ();
}
private class ThreadedEnumerator<S> : IEnumerator<S> {
private readonly IEnumerator<S> _enumerator;
private readonly Queue<S> _queue;
private const int _maxQueueSize = 10;
private readonly Thread _thread;
private volatile bool _keepGoing = true;
private volatile bool _finishedEnumerating = false;
private S _current;
public ThreadedEnumerator(IEnumerator<S> enumerator) {
_enumerator = enumerator;
_thread = new Thread(Enumerate);
_thread.Start();
}
private void Enumerate() {
while (_keepGoing) {
if (_enumerator.MoveNext ()) {
var current = _enumerator.Current;
lock (_queue) {
while (_queue.Count > _maxQueueSize && _keepGoing) {
Monitor.Wait (_queue, 100);
}
if (_keepGoing) {
_queue.Enqueue (current);
Monitor.Pulse (_queue);
}
}
} else {
break;
}
}
_finishedEnumerating = true;
}
public bool MoveNext ()
{
lock (_queue) {
while (!_finishedEnumerating && _queue.Count == 0) {
Monitor.Wait (_queue, 100);
}
if (_queue.Count > 0) {
_current = _queue.Dequeue ();
Monitor.Pulse (_queue);
return true;
} else {
_current = default(S);
return false;
}
}
}
public void Reset () {
lock (_queue) {
lock (_enumerator) {
_enumerator.Reset ();
_queue.Clear ();
}
}
}
object System.Collections.IEnumerator.Current {
get { return _current; }
}
public void Dispose () {
_keepGoing = false;
_thread.Join ();
}
public S Current {
get { return _current; }
}
}
}
Mutual exclusion is a general problem that applies to both processes and threads.
When mutual exclusion is achieved, atomic operations on shared data structures are guaranteed to be atomic and not interrupted by other threads.
Has two operations: Up() and Down()
Has N states: a counter that has a value from 0 - N
Up() increases the value by 1
Down() decreases the value by 1
When the semaphore has a value > 0, then a thread of execution can enter the critical region
When the semaphore has a value = 0, then a thread is blocked
This code example shows how to implement a classic mutex, a.k.a. a Lock, in Java.
These examples come from http://hpjpc.googlecode.com by Christopher and Thiruvathukal.
public class Lock {
protected boolean locked;
public Lock() {
locked = false;
}
public synchronized void lock() throws InterruptedException {
while (locked)
wait();
locked = true;
}
public synchronized void unlock() {
locked = false;
notify();
}
}
This shows how to implement a counting semaphore in the Java programming language.
public class Semaphore {
/**
* The current count, which must be non-negative.
*/
protected int count;
/**
* Create a counting Semaphore with a specified initial count.
*
* @param initCount
* The initial value of count.
* @throws com.toolsofcomputing.thread.NegativeSemaphoreException
* if initCount < 0.
*/
public Semaphore(int initCount) throws NegativeSemaphoreException {
if (initCount < 0)
throw new NegativeSemaphoreException();
count = initCount;
}
/**
* Create a counting Semaphore with an initial count of zero.
*/
public Semaphore() {
count = 0;
}
/**
* Subtract one from the count. Since count must be non-negative, wait until
* count is positive before decrementing it.
*
* @throws InterruptedException
* if thread is interrupted while waiting.
*/
public synchronized void down() throws InterruptedException {
while (count == 0)
wait();
count--;
}
/**
* Add one to the count. Wake up a thread waiting to "down" the semaphore, if
* any.
*/
public synchronized void up() {
count++;
notify();
}
}
This shows how to implement a barrier, which is a synchronization mechanism for awaiting a specified number of threads before processing can continue. Once all threads have arrived, processing can continue.
public class SimpleBarrier {
/**
* Number of threads that still must gather.
*/
protected int count;
/**
* Total number of threads that must gather.
*/
protected int initCount;
/**
* Creates a Barrier at which n threads may repeatedly gather.
*
* @param n
* total number of threads that must gather.
*/
public SimpleBarrier(int n) {
if (n <= 0)
throw new IllegalArgumentException(
"Barrier initialization specified non-positive value " + n);
initCount = count = n;
}
/**
* Is called by a thread to wait for the rest of the n threads to gather
* before the set of threads may continue executing.
*
* @throws InterruptedException
* If interrupted while waiting.
*/
public synchronized void gather() throws InterruptedException {
if (--count > 0)
wait();
else {
count = initCount;
notifyAll();
}
}
}
A classic problem in computer science and one that is often studied in operating systems to show the hazards of working with shared, synchronized state, is the dining philosophers problem. We won’t describe the entire problem here but you can read http://en.wikipedia.org/wiki/Dining_philosophers_problem.
our “solution” has the following design:
class Fork {
public char id;
private Lock lock = new Lock();
public void pickup() throws InterruptedException {
lock.lock();
}
public void putdown() throws InterruptedException {
lock.unlock();
}
public Fork(int value) {
this.id = new Integer(value).toString().charAt(0);
}
}
class Diner0 extends Thread {
private char state = 't';
private Fork L, R;
public Diner0(Fork left, Fork right) {
super();
L = left;
R = right;
}
protected void think() throws InterruptedException {
sleep((long) (Math.random() * 7.0));
}
protected void eat() throws InterruptedException {
sleep((long) (Math.random() * 7.0));
}
public char getDinerState() {
return state;
}
public void run() {
int i;
try {
for (i = 0; i < 1000; i++) {
state = 't';
think();
state = L.id;
sleep(1);
L.pickup();
state = R.id;
sleep(1);
R.pickup();
state = 'e';
eat();
L.putdown();
R.putdown();
}
state = 'd';
} catch (InterruptedException e) {
}
}
}
class Diners0 {
static Fork[] fork = new Fork[5];
static Diner0[] diner = new Diner0[5];
public static void main(String[] args) {
int i, j = 0;
boolean goOn;
for (i = 0; i < 5; i++) {
fork[i] = new Fork(i);
}
for (i = 0; i < 5; i++) {
diner[i] = new Diner0(fork[i], fork[(i + 1) % 5]);
}
for (i = 0; i < 5; i++) {
diner[i].start();
}
int newPrio = Thread.currentThread().getPriority() + 1;
Thread.currentThread().setPriority(newPrio);
goOn = true;
while (goOn) {
for (i = 0; i < 5; i++) {
System.out.print(diner[i].getDinerState());
}
if (++j % 5 == 0)
System.out.println();
else
System.out.print(' ');
goOn = false;
for (i = 0; i < 5; i++) {
goOn |= diner[i].getDinerState() != 'd';
}
try {
Thread.sleep(51);
} catch (InterruptedException e) {
return;
}
}
}
}
for (i = 0; i < 4; i++) {
diner[i] = new Diner0(fork[i], fork[(i + 1) % 5]);
}
diner[4] = new Diner0(fork[0], fork[4]);
If you have Java and Gradle installed on your computer, you can try these out!
Make sure you have the HPJPC source code:
hg clone https://bitbucket.org/loyolachicagocs_books/hpjpc-source-java
The following Gradle task in build.gradle shows how to run Diners0’s main() method:
task Diners0(dependsOn: 'classes', type: JavaExec) {
main = 'info.jhpc.textbook.chapter03.Diners0'
classpath = sourceSets.main.runtimeClasspath
args = []
}
To build:
gradle build
To run:
gradle Diners0
If you run this, you will notice that deadlock ensues fairly quick. The diners get into a state where they are waiting on each others’ forks in a cycle:
$ gradle Diners0
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:Diners0
tet4t 023et 12ett 0et40 e134e
et340 12ett 12ett 123et e1340
0234e e23e4 1tt40 t23et ett40
t23et 1ett0 12et0 t23et 1tt40
1t34e 12et0 1et40 12e30 t234e
12e30 1et40 tetet et3t4 1t3e4
1e240 1tte4 12tt0 t2ete t2tt0
11e3t et3t0 t234e e1340 11t40
1t340 0e24e tttet tt34e 12e3t
1t24e 0t3e4 tet4e 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
12340 12340 12340 12340 12340
Diners1 has a similar Gradle task:
task Diners1(dependsOn: 'classes', type: JavaExec) {
main = 'info.jhpc.textbook.chapter03.Diners1'
classpath = sourceSets.main.runtimeClasspath
args = []
}
Run:
gradle Diners1
Output:
$ gradle Diners1
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:Diners1
ttttt 1t240 t2et4 1et4t tt2e4
1e2et 1et4t e13e0 tt3e4 ettt0
t2te0 0e24e 1ettt e1e3t t1te4
0tt4e 1etet e13tt tt24e 1t3et
tettt 0ttet ete3t tt33e 0et4e
1ete0 01t3t 0tt3e 1e240 te2te
e1et0 1e2et 02e3e t1t3t 1t3tt
02ete 1et4t e13et et33t 02tte
1ett0 et3t0 ete30 t2e3e et3e0
0et4e ettt0 0e2e4 01t4e 1e2et
...
12e30 tet3e 1etet 0ttt0 0etet
1et4t e2tt4 tt3e4 0t3et 12et0
1ett0 e1tet 12e30 1tttt etet0
tettt 1e2t0 0t3e4 tettt ttttt
023e4 ttttt 023e4 1e2d0 e13d0
02ed4 e2edt 1etd0 et3d0 1tedt
02ede 0etde 1etd0 t2tdt t2ede
01tde et2d0 112dt tedde tedd4
tedde 02dd0 1edd0 etdd0 1tddt
1eddt 1eddt 01dde 0tdd0 t2dde
t2ddt eddd4 tddde tddd4 tdddt
0ddde eddd0 tdddt 0ddde eddd0
tddd4 eddd0 0ddde 0ddde eddd0
eddd0 0ddde 0ddde tddd4 0dddt
eddd0 tddd4 1dddd 0dddd 1dddd
tdddd tdddd ddddd
BUILD SUCCESSFUL
Total time: 18.426 secs
The diners, as desired, end up finishing their meal after some time.
We assume they have moved over to the bar or found a nice place to have dessert.
Example Bound Buffer
public class BoundBuffer<T> {
private readonly Semaphore _full;
private readonly Semaphore _empty;
private readonly Semaphore _lock;
private readonly Queue<T> _queue;
public BoundBuffer (int maxCount) {
_empty = new Semaphore (maxCount, maxCount);
_full = new Semaphore (0, maxCount);
_lock = new Semaphore (1, 1);
_queue = new Queue<T> ();
}
public void Enqueue(T item) {
_empty.WaitOne ();
_lock.WaitOne ();
_queue.Enqueue (item);
_lock.Release (1);
_full.Release (1);
}
public T Dequeue() {
_full.WaitOne ();
_lock.WaitOne ();
var item = _queue.Dequeue ();
_lock.Release(1);
_empty.Release(1);
return item;
}
}
Threads can be started and stopped on demand or a thread pool can be used
-Supported by modern operating systems -Scheduled by the operating system
-Supported by almost everything -Scheduled by the process
public class FileCopy0 {
public static final int BLOCK_SIZE = 4096;
public static void copy(String src, String dst) throws IOException {
FileReader fr = new FileReader(src);
FileWriter fw = new FileWriter(dst);
char[] buffer = new char[BLOCK_SIZE];
int bytesRead;
while (true) {
bytesRead = fr.read(buffer);
System.out.println(bytesRead + " bytes read");
if (bytesRead < 0)
break;
fw.write(buffer, 0, bytesRead);
System.out.println(bytesRead + " bytes written");
}
fw.close();
fr.close();
}
public static void main(String args[]) {
String srcFile = args[0];
String dstFile = args[1];
try {
copy(srcFile, dstFile);
} catch (Exception e) {
System.out.println("Copy failed.");
}
}
}
Quick overview of the various classes:
public class FileCopy1 {
public static int getIntProp(Properties p, String key, int defaultValue) {
try {
return Integer.parseInt(p.getProperty(key));
} catch (Exception e) {
return defaultValue;
}
}
public static void main(String args[]) {
String srcFile = args[0];
String dstFile = args[1];
Properties p = new Properties();
try {
FileInputStream propFile = new FileInputStream("FileCopy1.rc");
p.load(propFile);
} catch (Exception e) {
System.err.println("FileCopy1: Can't load Properties");
}
int buffers = getIntProp(p, "buffers", 20);
int bufferSize = getIntProp(p, "bufferSize", 4096);
System.out.println("source = " + args[0]);
System.out.println("destination = " + args[1]);
System.out.println("buffers = " + buffers);
System.out.println("bufferSize = " + bufferSize);
Pool pool = new Pool(buffers, bufferSize);
BufferQueue copyBuffers = new BufferQueue();
FileCopyReader1 src;
try {
src = new FileCopyReader1(srcFile, pool, copyBuffers);
} catch (Exception e) {
System.err.println("Cannot open " + srcFile);
return;
}
FileCopyWriter1 dst;
try {
dst = new FileCopyWriter1(dstFile, pool, copyBuffers);
} catch (Exception e) {
System.err.println("Cannot open " + dstFile);
return;
}
src.start();
dst.start();
try {
src.join();
} catch (Exception e) {
}
try {
dst.join();
} catch (Exception e) {
}
}
}
class FileCopyReader1 extends Thread {
private Pool pool;
private BufferQueue copyBuffers;
FileReader fr;
public FileCopyReader1(String filename, Pool pool, BufferQueue copyBuffers)
throws IOException {
this.pool = pool;
this.copyBuffers = copyBuffers;
fr = new FileReader(filename);
}
public void run() {
Buffer buffer;
int bytesRead = 0;
do {
try {
buffer = pool.use();
bytesRead = fr.read(buffer.getBuffer());
} catch (Exception e) {
buffer = new Buffer(0);
bytesRead = 0;
}
if (bytesRead < 0) {
buffer.setSize(0);
} else {
buffer.setSize(bytesRead);
}
copyBuffers.enqueueBuffer(buffer);
} while (bytesRead > 0);
try {
fr.close();
} catch (Exception e) {
return;
}
}
}
class FileCopyWriter1 extends Thread {
private Pool pool;
private BufferQueue copyBuffers;
FileWriter fw;
public FileCopyWriter1(String filename, Pool pool, BufferQueue copyBuffers)
throws IOException {
this.pool = pool;
this.copyBuffers = copyBuffers;
fw = new FileWriter(filename);
}
public void run() {
Buffer buffer;
while (true) {
try {
buffer = copyBuffers.dequeueBuffer();
} catch (Exception e) {
return;
}
if (buffer.getSize() > 0) {
try {
char[] bufferData = buffer.getBuffer();
int size = bufferData.length;
fw.write(bufferData, 0, size);
} catch (Exception e) {
break;
}
pool.release(buffer);
} else
break;
}
try {
fw.close();
}
catch (Exception e) {
return;
}
}
}
public class Buffer {
private char[] buffer;
private int size;
public Buffer(int bufferSize) {
buffer = new char[bufferSize];
size = bufferSize;
}
public char[] getBuffer() {
return buffer;
}
public void setSize(int newSize) {
if (newSize > size) {
char[] newBuffer = new char[newSize];
System.arraycopy(buffer, 0, newBuffer, 0, size);
buffer = newBuffer;
}
size = newSize;
}
public int getSize() {
return size;
}
}
class BufferQueue {
public Vector<Buffer> buffers = new Vector<Buffer>();
public synchronized void enqueueBuffer(Buffer b) {
if (buffers.size() == 0)
notify();
buffers.addElement(b);
}
public synchronized Buffer dequeueBuffer() throws InterruptedException {
while (buffers.size() == 0)
wait();
Buffer firstBuffer = buffers.elementAt(0);
buffers.removeElementAt(0);
return firstBuffer;
}
}
public class Pool {
Vector<Buffer> freeBufferList = new Vector<Buffer>();
OutputStream debug = System.out;
int buffers, bufferSize;
public Pool(int buffers, int bufferSize) {
this.buffers = buffers;
this.bufferSize = bufferSize;
freeBufferList.ensureCapacity(buffers);
for (int i = 0; i < buffers; i++)
freeBufferList.addElement(new Buffer(bufferSize));
}
public synchronized Buffer use() throws InterruptedException {
while (freeBufferList.size() == 0)
wait();
Buffer nextBuffer = freeBufferList.lastElement();
freeBufferList.removeElement(nextBuffer);
return nextBuffer;
}
public synchronized void release(Buffer oldBuffer) {
if (freeBufferList.size() == 0)
notify();
if (freeBufferList.contains(oldBuffer))
return;
if (oldBuffer.getSize() < bufferSize)
oldBuffer.setSize(bufferSize);
freeBufferList.addElement(oldBuffer);
}
}
You can run FileCopy0 and FileCopy1 by using the corresponding Gradle tasks.
task FileCopy0(dependsOn: 'classes', type: JavaExec) {
main = 'info.jhpc.textbook.chapter03.FileCopy0'
classpath = sourceSets.main.runtimeClasspath
args = [fc0_src, fc0_dest]
}
As shown, there are two properties you can set: fc0_src and fc0_dest:
gradle FileCopy0 -Pfc0_src=inputFile -Pfc0_dest=outputFile
You can also run FileCopy1 (the same parameter names are used):
gradle FileCopy1 -Pfc0_src=inputFile -Pfc0_dest=outputFile