0Pricing
R Academy · Lektion

Parallelen Code debuggen und Lasten verteilen

Behandeln Sie Fehler in parallelen Workern und verteilen Sie ungleichmäßige Arbeitslasten effektiv

Parallelen Code debuggen und Lasten verteilen ist eine kostenlose R Academy-Lektion auf CoddyKit. Dies ist Lektion 4 von 4. Du kannst die komplette Lektion unten kostenlos lesen – dann übst du sie direkt im Browser mit einem integrierten Code-Editor und einem KI-Tutor rund um die Uhr. Sie ist Teil des R Academy-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der R Academy-Kurs umfasst insgesamt 4 Lektionen.

Warum paralleles Debugging schwierig ist

Das Debuggen parallelen Codes ist schwierig, weil Worker in separaten Prozessen ausgeführt werden: print()-Ausgaben sind in der Master-Konsole nicht sichtbar, interaktive Debugger wie browser() funktionieren innerhalb von Workern nicht, und Fehler werden serialisiert und im Master erneut ausgelöst, wodurch ihr ursprünglicher Stacktrace verloren geht.

library(parallel)

# Problem: cat() inside worker is invisible in the master
cl <- makeCluster(2)
clusterExport(cl, c())

result <- parLapply(cl, 1:4, function(i) {
  # This cat() goes to the worker's stdout, NOT the master
  cat('Worker processing', i, '\n')  # you will NOT see this
  i^2
})

cat('Master received:', unlist(result), '\n')
stopCluster(cl)

Fehlerweitergabe in parLapply

Wenn ein Worker einen Fehler auslöst, kapselt parLapply() ihn und löst ihn im Master erneut aus. Die Fehlermeldung bleibt erhalten, der entfernte Stacktrace jedoch nicht. Testen Sie Ihre Funktion immer zuerst sequenziell, bevor Sie sie parallelisieren.

library(parallel)
cl <- makeCluster(2)

# Step 1: test sequentially first!
my_fn <- function(x) {
  if (x == 3) stop('bad value: 3')
  sqrt(x)
}

# Sequential test reveals the bug before parallelising
tryCatch(
  lapply(1:5, my_fn),
  error = function(e) cat('Sequential error caught:', e$message, '\n')
)

# Fix: guard the bad case
my_fn_safe <- function(x) {
  if (x <= 0) return(NA_real_)
  sqrt(x)
}
clusterExport(cl, 'my_fn_safe')
cat(unlist(parLapply(cl, 1:5, my_fn_safe)), '\n')

stopCluster(cl)

tryCatch innerhalb von Worker-Funktionen

Wenn Sie den Funktionskörper des Workers mit tryCatch() umschließen, können Sie Fehler für jedes Element abfangen und protokollieren, ohne den gesamten parallelen Auftrag zum Absturz zu bringen. Geben Sie bei einem Fehler einen Platzhalterwert wie NA zurück, damit die Nachbearbeitung problematische Eingaben erkennen kann.

library(parallel)
cl <- makeCluster(2)

safe_compute <- function(x) {
  tryCatch({
    if (x == 3) stop('simulated error on element 3')
    list(value = x^2, error = NULL)
  }, error = function(e) {
    list(value = NA_real_, error = conditionMessage(e))
  })
}
clusterExport(cl, 'safe_compute')

results <- parLapply(cl, 1:5, safe_compute)

for (i in seq_along(results)) {
  r <- results[[i]]
  if (is.null(r$error)) {
    cat('Element', i, ': value =', r$value, '\n')
  } else {
    cat('Element', i, ': ERROR -', r$error, '\n')
  }
}

stopCluster(cl)

future::value() mit tryCatch

Im Paket future löst value(f) den entfernten Fehler erneut aus. Wenn Sie den Aufruf mit tryCatch() umschließen, können Sie einzelne Fehler von Futures behandeln und gleichzeitig weiterhin Ergebnisse anderer Futures sammeln.

library(future)
plan(multisession, workers = 2)

# Create futures — some will fail
inputs <- list(4, -1, 9, 'x', 16)

futures <- lapply(inputs, function(val) {
  future({
    if (!is.numeric(val)) stop('non-numeric input')
    if (val < 0) stop('negative input')
    sqrt(val)
  })
})

# Collect with individual error handling
results <- lapply(futures, function(f) {
  tryCatch(
    value(f),
    error = function(e) paste('ERROR:', e$message)
  )
})

for (i in seq_along(results)) {
  cat('Input:', inputs[[i]], '-> Result:', as.character(results[[i]]), '\n')
}

plan(sequential)

foreach mit %dopar% und .errorhandling

Der Operator %dopar% des Pakets foreach verteilt Iterationen auf ein registriertes Backend. Das Argument .errorhandling steuert das Verhalten bei Fehlern: 'stop' (Standard), 'remove' (überspringen) oder 'pass' (das Condition-Objekt einschließen).

library(foreach)
library(doParallel)

cl <- makeCluster(2)
registerDoParallel(cl)

# .errorhandling = 'pass': failed elements return the condition
results <- foreach(
  x = 1:6,
  .errorhandling = 'pass'
) %dopar% {
  if (x == 4) stop('bad element')
  x^2
}

for (i in seq_along(results)) {
  if (inherits(results[[i]], 'error')) {
    cat('Element', i, ': ERROR -', results[[i]]$message, '\n')
  } else {
    cat('Element', i, ': value =', results[[i]], '\n')
  }
}

stopCluster(cl)

Lastverteilung: statisch oder dynamisch

Bei statischer Planung werden gleich große Blöcke vorab den Workern zugewiesen. Bei dynamischer Planung erhält jeder Worker jeweils eine Aufgabe, sodass schnelle Worker weitere Aufgaben übernehmen können. Die dynamische Planung ist besser geeignet, wenn die Aufgabendauer stark variiert.

library(parallel)
cl <- makeCluster(2)

# Simulate unequal task durations (element i takes i*0.05 seconds)
unequal_task <- function(i) {
  Sys.sleep(i * 0.05)
  i
}
clusterExport(cl, 'unequal_task')

# Static: parLapply distributes in fixed chunks
static_time <- system.time(
  parLapply(cl, 1:6, unequal_task)
)[['elapsed']]

# Dynamic: clusterApplyLB assigns one task per available worker
dynamic_time <- system.time(
  clusterApplyLB(cl, 1:6, unequal_task)
)[['elapsed']]

cat('Static LB: ', round(static_time, 2), 's\n')
cat('Dynamic LB:', round(dynamic_time, 2), 's\n')

stopCluster(cl)

Chunking: Overhead reduzieren

Die Kommunikation zwischen Prozessen verursacht pro Aufgabe einen festen Overhead. Wenn Sie viele kleine Elemente verarbeiten, gruppieren Sie sie zu größeren Blöcken. Dadurch erledigt jeder Worker pro Nachricht mehr Arbeit und das Verhältnis von Overhead zu Berechnungszeit sinkt.

library(parallel)
cl <- makeCluster(2)

# Naive: 1000 tiny tasks — high overhead
tiny_task <- function(x) x^2
clusterExport(cl, 'tiny_task')

t1 <- system.time(parLapply(cl, 1:1000, tiny_task))[['elapsed']]

# Chunked: 10 tasks of 100 items each — low overhead
chunk_task <- function(chunk) sapply(chunk, function(x) x^2)
chunks <- split(1:1000, cut(1:1000, 10, labels = FALSE))
clusterExport(cl, 'chunk_task')

t2 <- system.time(parLapply(cl, chunks, chunk_task))[['elapsed']]

cat('Unchunked:', round(t1, 4), 's\n')
cat('Chunked:  ', round(t2, 4), 's\n')

stopCluster(cl)

Keine großen Objekte übertragen

Die Serialisierung großer Objekte wie Data Frames, Matrizen oder Modelle für Worker ist kostspielig. Übergeben Sie stattdessen nur die Indizes und lesen Sie die Daten aus einer gemeinsam genutzten Quelle wie einer Datei oder Datenbank oder aus Daten, die im Worker vorab geladen wurden. Halten Sie die an Worker übertragenen Daten möglichst klein.

library(parallel)
cl <- makeCluster(2)

# BAD: sends the entire large data frame to every worker
big_df <- data.frame(x = rnorm(1e5), y = rnorm(1e5))
clusterExport(cl, 'big_df')  # 800 KB shipped to each worker

# BETTER: workers generate/read their own data slice
clusterEvalQ(cl, {
  set.seed(Sys.getpid())  # unique per worker
  local_data <- data.frame(x = rnorm(500), y = rnorm(500))
})

# Each worker uses its own local_data without receiving it from master
result <- parLapply(cl, 1:2, function(i) {
  coef(lm(y ~ x, data = local_data))
})
print(result)

stopCluster(cl)

Protokollierung aus Workern

Da Worker nicht in die Konsole des Masters schreiben können, leiten Sie die Ausgabe der Worker mit makeCluster(outfile = '/path/to/log') in separate Logdateien pro Worker um. Unter Unix können Sie die Ausgabe nach /dev/null oder in eine eigene Logdatei pro Worker umleiten.

library(parallel)

# Route all worker stdout/stderr to a log file
log_file <- tempfile(fileext = '.log')
cl <- makeCluster(2, outfile = log_file)

clusterEvalQ(cl, {
  cat('[Worker', Sys.getpid(), '] started\n')
})

parLapply(cl, 1:4, function(i) {
  cat('[Worker] processing item', i, '\n')  # goes to log file
  i * 10
})

stopCluster(cl)

# Read the log
log_content <- readLines(log_file)
cat(head(log_content, 8), sep = '\n')

Parallelen Code profilieren

Verwenden Sie system.time(), um die gesamte verstrichene Zeit zu messen, und schlüsseln Sie den Overhead der Worker sowie die Berechnungszeit manuell auf. Für weitere Details führen Sie die Zielfunktion zunächst sequenziell mit profvis::profvis() aus und parallelisieren Sie anschließend den Engpass.

library(parallel)

# Profile the task sequentially first
target_fn <- function(n) {
  x <- rnorm(n)
  list(
    mean = mean(x),
    sd   = sd(x),
    q95  = quantile(x, 0.95)
  )
}

# Measure sequential baseline
t_seq <- system.time(lapply(rep(1000, 20), target_fn))[['elapsed']]

# Measure parallel speedup
cl <- makeCluster(2)
clusterExport(cl, 'target_fn')
t_par <- system.time(parLapply(cl, rep(1000, 20), target_fn))[['elapsed']]
stopCluster(cl)

cat('Sequential:', round(t_seq, 4), 's\n')
cat('Parallel:  ', round(t_par, 4), 's\n')
cat('Efficiency:', round(t_seq / (t_par * 2) * 100, 1), '%\n')

Vollständiges robustes Muster

Kombinieren Sie alle bewährten Vorgehensweisen: Teilen Sie die Arbeit in Blöcke auf, verwenden Sie eine lastausgeglichene Zuweisung, behandeln Sie Fehler für jedes Element einzeln, protokollieren Sie in eine Datei und stellen Sie mit on.exit() die Bereinigung des Clusters sicher.

library(parallel)

robust_parallel <- function(items, fn, workers = 2) {
  cl <- makeCluster(workers, outfile = tempfile())
  on.exit(stopCluster(cl), add = TRUE)

  clusterExport(cl, 'fn', envir = environment())

  safe_fn <- function(x) {
    tryCatch(
      fn(x),
      error = function(e) list(error = e$message, value = NA)
    )
  }
  clusterExport(cl, 'safe_fn', envir = environment())

  # Load-balanced assignment for unequal task times
  clusterApplyLB(cl, items, safe_fn)
}

results <- robust_parallel(1:8, function(x) {
  if (x == 5) stop('deliberate error')
  x^3
})

cat('Results:\n')
for (i in seq_along(results)) {
  cat(' [', i, ']', ifelse(is.na(results[[i]]$value),
    paste('ERROR:', results[[i]]$error),
    results[[i]]
  ), '\n')
}

Kurze Überprüfung

Sie haben 100 parallele Aufgaben mit stark variierenden Ausführungszeiten (einige dauern 10 ms, andere 500 ms). Welche Planungsstrategie minimiert die gesamte verstrichene Zeit?

Zusammenfassung: Parallelen Code debuggen

Wichtigste Erkenntnisse:

  • Testen Sie Funktionen vor der Parallelisierung sequenziell – verwenden Sie zunächst lapply()
  • Umschließen Sie Worker-Körper mit tryCatch(), um Fehler einzelner Elemente abzufangen, ohne den Auftrag zum Absturz zu bringen
  • future::value() mit tryCatch() behandelt einzelne Fehler von Futures zuverlässig
  • foreach %dopar% mit .errorhandling = 'pass' gibt Fehlerobjekte in der Ergebnisliste zurück
  • Verwenden Sie clusterApplyLB() für dynamischen Lastausgleich bei unterschiedlich langen Aufgaben
  • Fassen Sie kleine Aufgaben zu Blöcken zusammen, um den Kommunikations-Overhead zu reduzieren
  • Halten Sie die an Worker übertragenen Daten möglichst klein – übergeben Sie Indizes statt großer Datenobjekte
  • Protokollieren Sie die Ausgabe der Worker über makeCluster(outfile=) in Dateien
# Canonical debugging workflow
# 1. Test sequentially
# lapply(inputs, my_fn)

# 2. Wrap in tryCatch
# safe_fn <- function(x) tryCatch(my_fn(x), error = function(e) NA)

# 3. Parallelise the safe version
# cl <- makeCluster(2); on.exit(stopCluster(cl))
# parLapply(cl, inputs, safe_fn)

cat('Debugging workflow: sequential -> tryCatch -> parallel\n')

Häufig gestellte Fragen

Ist die Lektion „Parallelen Code debuggen und Lasten verteilen“ kostenlos?

Ja — der vollständige Text von „Parallelen Code debuggen und Lasten verteilen“ ist hier im Web kostenlos zu lesen. Um sie interaktiv zu üben (integrierter Code-Editor und 24/7 KI-Tutor) und den Rest des R Academy-Kurses freizuschalten, upgrade auf CoddyKit PRO. Der R Academy-Kurs umfasst insgesamt 4 Lektionen.

Was lerne ich in „Parallelen Code debuggen und Lasten verteilen“?

Behandeln Sie Fehler in parallelen Workern und verteilen Sie ungleichmäßige Arbeitslasten effektiv Du übst R Academy mit praktischem Code, den du direkt im Browser ausführst, und ein 24/7 KI-Tutor beantwortet deine Fragen während du die Lektion bearbeitest.

Brauche ich Erfahrung, um R Academy zu starten?

Keine Vorkenntnisse erforderlich. R Academy auf CoddyKit ist für Anfänger bis fortgeschrittene Lernende strukturiert, sodass du hier starten oder von Anfang an beginnen und in deinem eigenen Tempo voranschreiten kannst. Dies ist Lektion 4 von 4.

Wie lange dauert die Lektion „Parallelen Code debuggen und Lasten verteilen“?

Die meisten CoddyKit-Lektionen dauern etwa 5–10 Minuten. Jede ist kompakt und interaktiv, sodass du stetig Fortschritte machst und genau dort weitermachst, wo du aufgehört hast – im Web und in der App.

Kann ich in dieser R Academy-Lektion Code schreiben und ausführen?

Ja. Jede R Academy-Lektion enthält einen integrierten Code-Editor, sodass du echten Code direkt in deinem Browser schreibst und ausführst und sofort KI-Feedback erhältst — ohne lokale Einrichtung erforderlich.

Alle Lektionen in diesem Kurs

  1. Paket parallel und detectCores()
  2. Das Framework future
  3. furrr: Parallele purrr-Operationen
  4. Parallelen Code debuggen und Lasten verteilen
← Zurück zu R Academy