تصحيح أخطاء التعليمات البرمجية المتوازية وموازنة الأحمال
تعامل مع الأخطاء في العمال المتوازيين ووازن أعباء العمل غير المتساوية بفعالية
تصحيح أخطاء التعليمات البرمجية المتوازية وموازنة الأحمال درس مجاني في R Academy على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في R Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة R Academy 4 دروس في المجموع.
لماذا يصعب تصحيح الشيفرة المتوازية؟
يُعد تصحيح الشيفرة المتوازية أمرًا صعبًا لأن العاملين يعملون في عمليات منفصلة: فعبارات print() لا تظهر في العملية الرئيسية، وأدوات التصحيح التفاعلية مثل browser() لا تعمل داخل العاملين، كما تُسلسل الأخطاء ثم يُعاد إطلاقها في العملية الرئيسية، فتفقد أثر المكدس الأصلي.
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)انتشار الأخطاء في parLapply
عندما يطلق أحد العاملين خطأً، تقوم parLapply() بتغليفه ثم تعيد إطلاقه في العملية الرئيسية. وتُحفظ رسالة الخطأ، لكن أثر المكدس البعيد لا يُحفظ. اختبر دالتك دائمًا بالتتابع أولًا قبل تشغيلها بالتوازي.
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 داخل دوال العاملين
يتيح تغليف جسم دالة العامل بـ tryCatch() التقاط الأخطاء وتسجيلها لكل عنصر من دون تعطيل المهمة المتوازية بأكملها. أرجع قيمة مميِّزة للفشل (مثل NA) عند حدوث خطأ، حتى تتمكن المعالجة اللاحقة من تحديد المدخلات التي تسبب مشكلات.
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() مع tryCatch
في حزمة future، تعيد value(f) إطلاق الخطأ البعيد. ويتيح إحاطتها بـ tryCatch() معالجة حالات فشل القيم المستقبلية الفردية مع مواصلة جمع النتائج من القيم المستقبلية الأخرى.
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 مع %dopar% و.errorhandling
يوزّع معامل %dopar% في حزمة foreach التكرارات عبر واجهة خلفية مسجّلة. وتتحكم الوسيطة .errorhandling فيما يحدث عند وقوع الأخطاء: 'stop' (الافتراضي)، أو 'remove' (التخطي)، أو 'pass' (تضمين كائن الشرط).
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)موازنة الحمل: ثابتة مقابل ديناميكية
يُسنِد الجدولة الثابتة مسبقًا أجزاءً متساوية الحجم إلى العاملين. أما الجدولة الديناميكية فتعطي كل عامل مهمة واحدة في كل مرة، ولذلك يتولى العاملون الأسرع مهامًا إضافية. وتكون الديناميكية أفضل عندما تختلف مدد المهام اختلافًا كبيرًا.
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)التجميع: تقليل الحمل الزائد
ينطوي الاتصال بين العمليات على حمل زائد ثابت لكل مهمة. وعند معالجة عناصر صغيرة كثيرة، اجمعها في أجزاء أكبر، حتى ينفذ كل استدعاء للعامل عملًا أكبر لكل رسالة، مما يقلل نسبة الحمل الزائد إلى وقت الحساب.
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)تجنّب إرسال الكائنات الكبيرة
تُعدّ عملية تسلسل الكائنات الكبيرة (إطارات البيانات والمصفوفات والنماذج) وإرسالها إلى العاملين مكلفة. وبدلًا من ذلك، مرّر الفهارس فقط واقرأ البيانات من مصدر مشترك (ملف أو قاعدة بيانات أو بيانات محمّلة مسبقًا داخل العامل). حافظ على صِغر البيانات المرسلة إلى العاملين.
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)التسجيل من العاملين
بما أن العاملين لا يستطيعون الكتابة إلى وحدة تحكم العملية الرئيسية، فأعد توجيه مخرجات العاملين إلى ملفات سجل منفصلة لكل عامل باستخدام makeCluster(outfile = '/path/to/log'). وفي Unix، يمكنك إعادة التوجيه إلى /dev/null أو إلى ملف سجل مخصص لكل عامل.
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')تحليل أداء الشيفرة المتوازية
استخدم system.time() لقياس الزمن الفعلي الإجمالي، وحلّل يدويًا الحمل الزائد للعاملين مقابل زمن الحساب. وللحصول على تفاصيل أكثر، شغّل الدالة المستهدفة بالتتابع أولًا باستخدام profvis::profvis()، ثم شغّل عنق الزجاجة بالتوازي.
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')نمط متين متكامل
يجمع هذا النمط جميع أفضل الممارسات: تقسيم العمل إلى أجزاء، واستخدام إسناد متوازن الحمل، ومعالجة الأخطاء لكل عنصر، والتسجيل في ملف، وضمان تنظيف العنقود باستخدام on.exit().
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')
}اختبار سريع
لديك 100 مهمة متوازية ذات أزمنة تنفيذ متفاوتة جدًا (تستغرق بعضُها 10ms، بينما تستغرق أخرى 500ms). أي استراتيجية جدولة ستقلل الزمن الفعلي الإجمالي إلى أدنى حد؟
مراجعة: تصحيح الشيفرة المتوازية
أهم النقاط:
- اختبر الدوال بالتتابع قبل تشغيلها بالتوازي، واستخدم
lapply()أولًا - أحط أجسام دوال العاملين بـ
tryCatch()لالتقاط الأخطاء لكل عنصر من دون تعطيل المهمة - تتعامل
future::value()معtryCatch()مع حالات فشل القيم المستقبلية الفردية بسلاسة - يعيد
foreach %dopar%مع.errorhandling = 'pass'كائنات الأخطاء ضمن قائمة النتائج - استخدم
clusterApplyLB()لموازنة الحمل الديناميكية عند اختلاف مدد المهام - قسّم المهام الصغيرة إلى أجزاء لتقليل حمل الاتصال الزائد
- أبقِ البيانات المرسلة إلى العاملين في الحد الأدنى؛ مرّر الفهارس بدلًا من كائنات البيانات الكبيرة
- سجّل مخرجات العاملين في ملفات عبر
makeCluster(outfile=)
# 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')الأسئلة الشائعة
هل درس «تصحيح أخطاء التعليمات البرمجية المتوازية وموازنة الأحمال» مجاني؟
نعم — نص درس «تصحيح أخطاء التعليمات البرمجية المتوازية وموازنة الأحمال» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة R Academy، انتقل إلى CoddyKit PRO. تتضمن دورة R Academy 4 دروس في المجموع.
ماذا ستتعلم في «تصحيح أخطاء التعليمات البرمجية المتوازية وموازنة الأحمال»؟
تعامل مع الأخطاء في العمال المتوازيين ووازن أعباء العمل غير المتساوية بفعالية تتمرن على R Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ R Academy؟
لا تُشترط خبرة سابقة. R Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «تصحيح أخطاء التعليمات البرمجية المتوازية وموازنة الأحمال»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس R Academy هذا؟
نعم. كل درس في R Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- حزمة parallel وdetectCores()
- إطار عمل future
- furrr: عمليات purrr المتوازية
- تصحيح أخطاء التعليمات البرمجية المتوازية وموازنة الأحمال