I'm trying to parallelize the processing of a file set using bash. I'm using named pipes for keeping number of process fixed and to gather output from the processes.
I'm assuming that the writes to named pipe are atomic, i.e the output of different process is not mixed up. Is that a safe assumption?
Any advice is greatly appreciated. I'm limited to using bash.
Here's the code:
mytask()
{
wItem=$1
#dummy func; process workItem
rt=$RANDOM
st=$rt;
let "rt %= 2"
let "st %= 10"
sleep $st
return $rt
}
parallelizeTask()
{
workList=$1
threadCnt=$2
task=$3
threadSyncPipeD=$4
outputSyncPipeD=$5
ti=0
for workItem in $workList; do
if [ $ti -lt $threadCnt ]; then
{ $task $workItem; if [ $? == 0 ]; then result="success"; else result="failure"; fi; \
echo "$result:$workItem" >&$outputSyncPipeD; echo "$result:$workItem" >&$threadSyncPipeD; } &
((ti++))
continue;
fi
while read n; do
((ti--))
break;
done <&$threadSyncPipeD
{ $task $workItem; if [ $? == 0 ]; then result="success"; else result="failure"; fi; \
echo "$result:$workItem" >&$outputSyncPipeD; echo "$result:$workItem" >&$threadSyncPipeD;} &
((i++))
done
wait
echo "quit" >&$outputSyncPipeD
while read n; do
if [[ $n == "quit" ]]; then
break;
else
eval $6="\${$6}\ \$n"
fi
done <&$outputSyncPipeD;
}
main()
{
if [ ! -p threadSyncPipe ]; then
mkfifo threadSyncPipe
fi
if [ ! -p outputSyncPipe ]; then
mkfifo outputSyncPipe
fi
exec 4<>threadSyncPipe
exec 3<>outputSyncPipe
gout=
parallelizeTask "f1 f2 f3 f4 f5 f6" 2 mytask 3 4 gout
echo "finalOutput: $gout";
for f in $gout; do
echo $f
done
rm outputSyncPipe
rm threadSyncPipe
}
main
I found below related post with answer to my question. I have revised the title to make it more appropriate.
Are there repercussions to having many processes write to a single reader on a named pipe in posix?
I found answer in the below given related post, according to it, the writes to fifo are atomic as long as the write messages is less than the page size 4k(page size depends on system configuration).
Are there repercussions to having many processes write to a single reader on a named pipe in posix?
Thank you all for the replies and suggestions.
Related
Context
I finally fixed my issue to have stdout & stderr to screen and a file plus having a separate file for the errors. See: Output stdout and stderr to file and screen and stderr to file in a limited environment
Issue
The problem is the ordering of lines.
Some times it is OK:
FIRST
ERROR
LAST
ERROR2
LAST2
Some times the errors are at the end:
FIRST
LAST
LAST2
ERROR
ERROR2
I can't figure out why (except maybe a semaphore underneath that... but... not sure. And if it is the case, there is no solution exception adding line numbers to each echo).
Part of the code where the problem occurs
{ "$0" "${mainArgs[#]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 ; } 3>&1 | tee -a "$logPath/$logFileName.log" &
Full testable code
m=0
declare -a mainArgs
if [ ! "$#" = "0" ]; then
for arg in "$#"; do
mainArgs[$m]=$arg
m=$(($m + 1))
done
fi
function containsElement()
# $1 string to find
# $2 array to search in
# return 0 if there is a match, otherwise 1
{
local e match="$1"
shift
for e; do [[ "$e" == "$match" ]] && return 0; done
return 1
}
function hasMainArg()
# $1 string to find
# return 0 if there is a match, otherwise 1
{
local match="$1"
containsElement "$1" "${mainArgs[#]}"
return $?
}
function activateLogs()
# $1 = logOutput: What is the output for logs: SCREEN, DISK, BOTH. Default is DISK. Optional parameter.
{
local logOutput=$1
if [ "$logOutput" != "SCREEN" ] && [ "$logOutput" != "BOTH" ]; then
logOutput="DISK"
fi
if [ "$logOutput" = "SCREEN" ]; then
echo "Logs will only be output to screen"
return
fi
hasMainArg "--force-log"
local forceLog=$?
local isFileDescriptor3Exist=$(command 2>/dev/null >&3 && echo "Y")
if [ "$isFileDescriptor3Exist" = "Y" ]; then
echo "Logs are configured"
elif [ "$forceLog" = "1" ] && ([ ! -t 1 ] || [ ! -t 2 ]); then
# Use external file descriptor if they are set except if having "--force-log"
echo "Logs are configured externally"
else
echo "Relaunching with logs files"
local logPath="logs"
if [ ! -d $logPath ]; then mkdir $logPath; fi
local logFileName=$(basename "$0")"."$(date +%Y-%m-%d.%k-%M-%S)
exec 4<> "$logPath/$logFileName.log" # File descriptor created only to get the underlying file in any output option
if [ "$logOutput" = "DISK" ]; then
# FROM: https://stackoverflow.com/a/45426547/214898
exec 3<> "$logPath/$logFileName.log"
"$0" "${mainArgs[#]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 &
else
# FROM: https://stackoverflow.com/a/70790574/214898
{ "$0" "${mainArgs[#]}" 2>&1 1>&3 | tee -a "$logPath/$logFileName.err" 1>&3 ; } 3>&1 | tee -a "$logPath/$logFileName.log" &
fi
exit
fi
}
#activateLogs "DISK"
#activateLogs "SCREEN"
activateLogs "BOTH"
echo "FIRST"
echo "ERROR" >&2
echo "LAST"
echo "ERROR2" >&2
echo "LAST2"
Stdout 1 and errout 2 pass by different file descriptors. If is very busy then very close calls like these can get mixed up.
You could use sleep 1 between your calls which would give the system time to process each call in order, but would slow down your script.
You can sleep less than a second : for example sleep 0.5.
currently I'm writing a bash script like this:
foo(){
while true
do
sleep 10
done
}
bar(){
while true
do
sleep 20
done
}
foo &
bar &
wait
(I know there is no point in such a script, it's just about the structure)
Now I want to add signal handling with trap -- <doSomething> RTMIN+1. This works at first. When the script receives the rtmin+1 signal it does doSomething but afterwards it exists (with the 163 exit code, which is the number of the signal being sent).
This is not the behavior I want. I want that after receiving the signal, the script continues to wait for the processes (in this case the two functions) to terminate (which of course will not happen in this case, but the script should wait).
I tried it with adding a ; wait to the things that should be done when receiving the signal, but this does not help (or I'm doing something wrong).
Does anyone know how to achieve the desired behavior?
Thanks in advance and with best wishes.
EDIT: Maybe a more precise example helps:
clock(){
local prefix=C
local interval=1
while true
do
printf "${prefix} $(date '+%d.%m %H:%M:%S')\n"
sleep $interval
done
}
volume(){
prefix=V
volstat="$(amixer get Master 2>/dev/null)"
echo "$volstat" | grep "\[off\]" >/dev/null && icon="" #alternative: deaf: mute:
vol=$(echo "$volstat" | grep -o "\[[0-9]\+%\]" | sed "s/[^0-9]*//g;1q")
if [ -z "$icon" ] ; then
if [ "$vol" -gt "50" ]; then
icon=""
#elif [ "$vol" -gt "30" ]; then
# icon=""
else
icon=""
fi
fi
printf "${prefix}%s %3s%%\n" "$icon" "$vol"
}
clock &
volume &
trap -- "volume" RTMIN+2
wait
Now the RTMIN+2 signal should rerun the volume function, but the clock process should not be interrupted. (Up to now, the whole script (with all subprocesses) is terminated upon the receiving of the signal)
When invoked with no operands, wait exits with either 0; which means all process IDs known by the invoking shell have terminated, or a value greater than 128; which means a signal for which a trap has been set is received. So, looping until wait exits with 0 is enough to keep the script alive after receiving a signal. You may also check whether its exit status is less than 128 within the loop, but I don't think that's necessary.
However, if you're sending those signals using pkill, the jobs started at the background will receive them too since child processes inherit the process name from their parent but not the custom signal handlers, and pkill signals all processes whose names match. You need to handle that case as well.
A minimal, working example would be:
#! /bin/bash -
# ignore RTMIN+1 and RTMIN+2 temporarily
# to prevent pkill from killing jobs
trap '' RTMIN+1 RTMIN+2
# start jobs
sleep 20 && echo slept for 20 seconds &
sleep 30 && echo slept for 30 seconds &
# set traps
trap 'echo received rtmin+1' RTMIN+1
trap 'echo received rtmin+2' RTMIN+2
# wait for jobs to terminate
until wait; do
echo still waiting
done
It's also worth to note that ignoring RTMIN+1 and RTMIN+2 before starting jobs prevents shells descending from them from trapping/reseting those signals. If that is a problem, you may set an empty trap within jobs as F. Hauri suggested; or you may totally drop ignoring and use pkill with -o option to send the signal to the oldest matching process.
References:
Shell Command Language § Signals and Error Handling
wait spec. § EXIT STATUS
Related:
Reliably kill sleep process after USR1 signal
Something rewritted
In order to avoid some useless forks.
clock(){ local prefix=C interval=2
trap : RTMIN{,+{{,1}{1,2,3,4,5},6,7,8,9,10}}
while :;do
printf "%s: %(%d.%m %H:%M:%S)T\n" $prefix -1
sleep $interval
done
}
volume(){ local prefix=V vol=() field playback val foo
while IFS=':[]' read field playback val foo;do
[ "$playback" ] && [ -z "${playback//*Playback*}" ] && [ "$val" ] &&
vol+=(${val%\%})
done < <(amixer get Master)
suffix='%%'
if [ "$vol" = "off" ] ;then
icon="" #alternative: deaf: mute:
suffix=''
elif (( vol > 50 )) ;then icon=""
elif (( vol > 30 )) ;then icon=""
else icon=""
fi
printf -v values "%3s$suffix " ${vol[#]}
printf "%s%s %s\n" $prefix "$icon" "$values"
}
clock & volume &
trap volume RTMIN+2
trap : RTMIN{,+{{,1}{1,3,4,5},6,7,8,9,10,12}}
echo -e "To get status, run:\n kill -RTMIN+2 $$"
while :;do wait ;done
Regarding my last comment about stereo bug, there is a volume function working for stereo, mono or even quadra:
volume(){
local prefix=V vol=() field playback val foo
local -i overallvol=0
while IFS=':[]' read field playback val foo ;do
[ "$playback" ] && [ -z "${playback//*Playback*}" ] && [ "$val" ] && {
vol+=($val)
val=${val%\%}
overallvol+=${val//off/0}
}
done < <(
amixer get Master
)
overallvol=$overallvol/${#vol[#]}
if (( overallvol == 0 )) ;then
icon=""
elif (( overallvol > 50 )) ;then
icon=""
elif (( overallvol > 30 )) ;then
icon=""
else
icon=""
fi
printf "%s%s %s\n" $prefix "$icon" "${vol[*]}"
}
or even:
volume(){
local prefix=V vol=() field playback val foo icons=(⏻ ¼ ¼ ¼ ½ ½ ¾ ¾ ¾ ¾ ¾)
local -i overallvol=0
while IFS=':[]' read field playback val foo ;do
[ "$playback" ] && [ -z "${playback//*Playback*}" ] && [ "$val" ] && {
vol+=($val)
val=${val%\%}
overallvol+=${val//off/0}
}
done < <(
amixer get Master
)
overallvol=$overallvol/${#vol[#]}
printf "%s%s %s\n" $prefix "${icons[(9+overall)/10]}" "${vol[*]}"
Some explanations
Regarding useless forks in volume() function
I've posted there some ideas to improve the job, reducing resource eating and doing same job of choosing an icon as function of current volume set.
About while :;do wait;done loop
As requested sample stand for an infinite loop in backgrounded sub function, the main script use same infinite loop.
But as question title stand for wait afterwards for processes to terminate, I have to agree with oguz-ismail's comment.
In fact, last line would better be written:
until wait;do :;done
For more information on how wait command work and good practice, please have a look on good oguz-ismail's answer
I've been scratching my head about this for a while... I'm trying to get my code to react like:
If no parameters, go to menu
If more OR less than 4 parameters, call error and go to menu
If exactly 4 parameters, write to file and exit
I can't get this to work in any way, and if you can help that would be majorly appreciated!
username=$1
firstname=$2
surname=$3
password=$4
if test "$#" = 0; then
{
menu
}
elif test "$#" = 4; then
{
echo Error
sleep 2
menu
}
else {
echo Done
echo "$firstname" "$surname" >> "$username".log
echo "$password" >> "$username".log
curdate=$(date +'%d/%m/%Y %H:%M:%S')
echo "$curdate" >> "$username".log
sleep 2
clear
exit
}
fi
In bash, numeric comparisons are not done with = but are done with -eq and its ilk. (= is for string comparison.)
So you want something like this. I'm going to replace your test with the much more common [ notation.
if [ "$#" -eq 0 ] ; then
{
menu
}
elif [ "$#" -eq 4 ] ; then
...
Mort
I have this script:
#!/bin/bash
CONTOR=0
total=`grep -c . $1`
for i in `cat $1`
do
CONTOR=`ps x | grep -c bash`
while [ $CONTOR -ge 500 ];do
CONTOR=`ps x | grep -c bash`
sleep 5
done
if [ $CONTOR -le 500 ]; then
./bing-ip2hosts -n $i >> url.txt &
fi
done
The scripts takes an IP from a list then runs ./bing-ip2hosts -n $i[the ip].
How can I make it multi-threaded so it runs faster. Now it opens like 20-30 processes and I would like for it to open 150 maybe even 200.
Try using the parallel command:
cat $1 | parallel -j4 s./bing-ip2hosts -n {} >> url.txt
see http://www.xensoft.com/content/use-multiple-cpu-cores-single-threaded-linux-commands for more examples.
Sure, technically these are processes, and this program should really be called a process spawning manager, but this is only due to the way that BASH works when it forks using the ampersand, it uses the fork() or perhaps clone() system call which clones into a separate memory space, rather than something like pthread_create() which would share memory. If BASH supported the latter, each "sequence of execution" would operate just the same and could be termed to be traditional threads whilst gaining a more efficient memory footprint. Functionally however it works the same, though a bit more difficult since GLOBAL variables are not available in each worker clone hence the use of the inter-process communication file and the rudimentary flock semaphore to manage critical sections. Forking from BASH of course is the basic answer here but I feel as if people know that but are really looking to manage what is spawned rather than just fork it and forget it. This demonstrates a way to manage up to 200 instances of forked processes all accessing a single resource. I hope you enjoy it, I enjoyed writing it.
#!/bin/bash
ME=$(basename $0)
IPC="/tmp/$ME.ipc" #interprocess communication file (global thread accounting stats)
DBG=/tmp/$ME.log
echo 0 > $IPC #initalize counter
F1=thread
SPAWNED=0
COMPLETE=0
SPAWN=10000 #number of jobs to process
SPEEDFACTOR=1 #dynamically compensates for execution time
THREADLIMIT=200 #maximum concurrent threads
TPS=1 #threads per second delay
THREADCOUNT=0 #number of running threads
SCALE="scale=5" #controls bc's precision
START=$(date +%s) #whence we began
MAXTHREADDUR=30 #maximum thread life span - demo mode
LOWER=$[$THREADLIMIT*100*90/10000] #90% worker utilization threshold
UPPER=$[$THREADLIMIT*100*95/10000] #95% worker utilization threshold
DELTA=10 #initial percent speed change
threadspeed() #dynamically adjust spawn rate based on worker utilization
{
#vaguely assumes thread execution average will be consistent
THREADCOUNT=$(threadcount)
if [ $THREADCOUNT -ge $LOWER ] && [ $THREADCOUNT -le $UPPER ] ;then
echo SPEED HOLD >> $DBG
return
elif [ $THREADCOUNT -lt $LOWER ] ;then
#if maxthread is free speed up
SPEEDFACTOR=$(echo "$SCALE;$SPEEDFACTOR*(1-($DELTA/100))"|bc)
echo SPEED UP $DELTA%>> $DBG
elif [ $THREADCOUNT -gt $UPPER ];then
#if maxthread is active then slow down
SPEEDFACTOR=$(echo "$SCALE;$SPEEDFACTOR*(1+($DELTA/100))"|bc)
DELTA=1 #begin fine grain control
echo SLOW DOWN $DELTA%>> $DBG
fi
echo SPEEDFACTOR $SPEEDFACTOR >> $DBG
#average thread duration (total elapsed time / number of threads completed)
#if threads completed is zero (less than 100), default to maxdelay/2 maxthreads
COMPLETE=$(cat $IPC)
if [ -z $COMPLETE ];then
echo BAD IPC READ ============================================== >> $DBG
return
fi
#echo Threads COMPLETE $COMPLETE >> $DBG
if [ $COMPLETE -lt 100 ];then
AVGTHREAD=$(echo "$SCALE;$MAXTHREADDUR/2"|bc)
else
ELAPSED=$[$(date +%s)-$START]
#echo Elapsed Time $ELAPSED >> $DBG
AVGTHREAD=$(echo "$SCALE;$ELAPSED/$COMPLETE*$THREADLIMIT"|bc)
fi
echo AVGTHREAD Duration is $AVGTHREAD >> $DBG
#calculate timing to achieve spawning each workers fast enough
# to utilize threadlimit - average time it takes to complete one thread / max number of threads
TPS=$(echo "$SCALE;($AVGTHREAD/$THREADLIMIT)*$SPEEDFACTOR"|bc)
#TPS=$(echo "$SCALE;$AVGTHREAD/$THREADLIMIT"|bc) # maintains pretty good
#echo TPS $TPS >> $DBG
}
function plot()
{
echo -en \\033[${2}\;${1}H
if [ -n "$3" ];then
if [[ $4 = "good" ]];then
echo -en "\\033[1;32m"
elif [[ $4 = "warn" ]];then
echo -en "\\033[1;33m"
elif [[ $4 = "fail" ]];then
echo -en "\\033[1;31m"
elif [[ $4 = "crit" ]];then
echo -en "\\033[1;31;4m"
fi
fi
echo -n "$3"
echo -en "\\033[0;39m"
}
trackthread() #displays thread status
{
WORKERID=$1
THREADID=$2
ACTION=$3 #setactive | setfree | update
AGE=$4
TS=$(date +%s)
COL=$[(($WORKERID-1)/50)*40]
ROW=$[(($WORKERID-1)%50)+1]
case $ACTION in
"setactive" )
touch /tmp/$ME.$F1$WORKERID #redundant - see main loop
#echo created file $ME.$F1$WORKERID >> $DBG
plot $COL $ROW "Worker$WORKERID: ACTIVE-TID:$THREADID INIT " good
;;
"update" )
plot $COL $ROW "Worker$WORKERID: ACTIVE-TID:$THREADID AGE:$AGE" warn
;;
"setfree" )
plot $COL $ROW "Worker$WORKERID: FREE " fail
rm /tmp/$ME.$F1$WORKERID
;;
* )
;;
esac
}
getfreeworkerid()
{
for i in $(seq 1 $[$THREADLIMIT+1])
do
if [ ! -e /tmp/$ME.$F1$i ];then
#echo "getfreeworkerid returned $i" >> $DBG
break
fi
done
if [ $i -eq $[$THREADLIMIT+1] ];then
#echo "no free threads" >> $DBG
echo 0
#exit
else
echo $i
fi
}
updateIPC()
{
COMPLETE=$(cat $IPC) #read IPC
COMPLETE=$[$COMPLETE+1] #increment IPC
echo $COMPLETE > $IPC #write back to IPC
}
worker()
{
WORKERID=$1
THREADID=$2
#echo "new worker WORKERID:$WORKERID THREADID:$THREADID" >> $DBG
#accessing common terminal requires critical blocking section
(flock -x -w 10 201
trackthread $WORKERID $THREADID setactive
)201>/tmp/$ME.lock
let "RND = $RANDOM % $MAXTHREADDUR +1"
for s in $(seq 1 $RND) #simulate random lifespan
do
sleep 1;
(flock -x -w 10 201
trackthread $WORKERID $THREADID update $s
)201>/tmp/$ME.lock
done
(flock -x -w 10 201
trackthread $WORKERID $THREADID setfree
)201>/tmp/$ME.lock
(flock -x -w 10 201
updateIPC
)201>/tmp/$ME.lock
}
threadcount()
{
TC=$(ls /tmp/$ME.$F1* 2> /dev/null | wc -l)
#echo threadcount is $TC >> $DBG
THREADCOUNT=$TC
echo $TC
}
status()
{
#summary status line
COMPLETE=$(cat $IPC)
plot 1 $[$THREADLIMIT+2] "WORKERS $(threadcount)/$THREADLIMIT SPAWNED $SPAWNED/$SPAWN COMPLETE $COMPLETE/$SPAWN SF=$SPEEDFACTOR TIMING=$TPS"
echo -en '\033[K' #clear to end of line
}
function main()
{
while [ $SPAWNED -lt $SPAWN ]
do
while [ $(threadcount) -lt $THREADLIMIT ] && [ $SPAWNED -lt $SPAWN ]
do
WID=$(getfreeworkerid)
worker $WID $SPAWNED &
touch /tmp/$ME.$F1$WID #if this loops faster than file creation in the worker thread it steps on itself, thread tracking is best in main loop
SPAWNED=$[$SPAWNED+1]
(flock -x -w 10 201
status
)201>/tmp/$ME.lock
sleep $TPS
if ((! $[$SPAWNED%100]));then
#rethink thread timing every 100 threads
threadspeed
fi
done
sleep $TPS
done
while [ "$(threadcount)" -gt 0 ]
do
(flock -x -w 10 201
status
)201>/tmp/$ME.lock
sleep 1;
done
status
}
clear
threadspeed
main
wait
status
echo
I feel like an idiot. I want a BASH function that alternates values every time it's called. The script itself is very simple, and it works if I call the function directly. But it doesn't work the same when called inside a string. Here's the code:
odd_or_even()
{
if [ $ODDEVEN -eq 1 ]; then
echo "odd"
let "ODDEVEN+=1"
else
echo "even"
let "ODDEVEN-=1"
fi
}
ODDEVEN=1
odd_or_even # Prints "odd"
odd_or_even # Prints "even"
echo "<td class=\"`odd_or_even`\">Test</td>" # Prints class=odd
echo "<td class=\"`odd_or_even`\">Test</td>" # Prints class=odd
Does BASH have restrictions about calling functions inside strings? It seems to work because it's outputting something, but it's not performing the mathematical operation.
The back quotes create sub-shells and the environment is reset in each sub-shell so you don't actually modify the same variable ODDEVEN.
You can use a file:
odd_or_even()
{
ODDEVEN=$(cat oddfile)
if [ $ODDEVEN -eq 1 ]; then
echo "odd"
let "ODDEVEN=0"
else
echo "even"
let "ODDEVEN=1"
fi
echo $ODDEVEN > oddfile
}
Or let the function do the string manipulation:
odd_or_even()
{
prefix=$1
suffix=$2
if [ $ODDEVEN -eq 1 ]; then
out="odd"
let "ODDEVEN=0"
else
out="even"
let "ODDEVEN=1"
fi
echo $prefix$out$suffix
}
ODDEVEN=1
odd_or_even "<td class=\"" "\">Test</td>"
odd_or_even "<td class=\"" "\">Test</td>"
Perhaps not the most elegant solution around, but you could use file descriptors since they get inherited by child processes (such as subshells).
As already pointed out, variable assignments (such as your let "ODDEVEN+=1" or let "ODDEVEN-=1") in a (backticked) subshell (child process) are not visible to the parent shell (parent process).
odd_or_even()
{
if [ $ODDEVEN -eq 1 ]; then
#echo "odd"
exec 3<&-
exec 3<<<"odd"
let "ODDEVEN+=1"
else
#echo "even"
exec 3<&-
exec 3<<<"even"
let "ODDEVEN-=1"
fi
}
export -f odd_or_even
{
ODDEVEN=1
odd_or_even && cat <&3 3<&- # Prints "odd"
odd_or_even && cat <&3 3<&- # Prints "even"
odd_or_even
echo "<td class=\"`cat <&3 3<&-`\">Test</td>" # Prints class=odd
odd_or_even
echo "<td class=\"`cat <&3 3<&-`\">Test</td>" # Prints class=odd
}
# output:
# odd
# even
# <td class="odd">Test</td>
# <td class="even">Test</td>
The goal in cases like this is to get the function out of the subshell.
function odd_or_even {
case $oddeven in
?([01]))
typeset -a strings=(even odd)
printf %s "${strings[oddeven^=1]}"
;;
*) return 1
esac
}
odd_or_even > >(echo "${prefix}$(</dev/fd/0)${suffix}") || exit
Since you're assigning to a nonlocal variable anyway there isn't really any point in not just using it directly instead of worring about I/O. This keeps both halves out of a subshell.
oddeven=
typeset -a strings=(even odd)
echo "${prefix}${strings[oddeven^=1]}${suffix}"
Your original solution is only possible in ksh93t and mksh R41 or greater using a special command substitution form that doesn't create a subshell.
function odd_or_even {
...
}
print -r -- "${prefix}${ odd_or_even;}${suffix}"
As an aside, Stop using backticks!