Taskflow : Limit the Maximum Concurrency 【官网CookBook】

【文档链接】https://taskflow.github.io/taskflow/LimitTheMaximumConcurrency.html

本章讨论如何在 Taskflow 应用程序中限制并发性或工作线程的最大数量。

定义信号量

     Taskflow 提供了一种机制 tf::Semaphore,用于限制某段任务中的最大并发数。您可以让一个任务在执行其工作之前/之后获取或释放一个或多个信号量。任务可以同时获取和释放信号量,或者仅获取,或者仅释放。

 tf::Semaphore 对象以一个初始值开始。只要该值大于 0,任务就可以获取信号量并执行其工作。如果该值为 0 或更小,尝试获取信号量的任务将不会运行,而是进入该信号量的等待队列。当另一个任务释放信号量时,它会重新调度等待队列中的所有任务。

tf::Executor executor(8);   // create an executor of 8 workers
tf::Taskflow taskflow;

tf::Semaphore semaphore(1); // create a semaphore with initial value of 1

std::vector<tf::Task> tasks {
  taskflow.emplace([](){ std::cout << "A" << std::endl; }),
  taskflow.emplace([](){ std::cout << "B" << std::endl; }),
  taskflow.emplace([](){ std::cout << "C" << std::endl; }),
  taskflow.emplace([](){ std::cout << "D" << std::endl; }),
  taskflow.emplace([](){ std::cout << "E" << std::endl; })
};

for(auto & task : tasks) {  // each task acquires and release the semaphore
  task.acquire(semaphore);
  task.release(semaphore);
}

executor.run(taskflow).wait();

        上面的示例创建了五个彼此之间没有依赖关系的任务。在正常情况下,这五个任务会并发执行。然而,本示例中使用了一个初始值为 1 的信号量,并且所有任务在运行前都需要获取该信号量,在完成后需要释放该信号量。这种组织方式将同时运行的任务数量限制为仅一个。一个可能的输出如下所示:

# the output is a sequential chain of five tasks
A
B
E
D
C

【注意】  
您有责任确保信号量在获取和释放它的任务执行期间保持有效。执行器和任务流不会管理任何信号量的生命周期。

        对于上面的同一个示例,我们可以将信号量的并发限制设置为另一个不同于 1 的值,例如 3。这将会限制只有三个工作线程可以运行五个任务 A、B、C、D 和 E。

tf::Executor executor(8);   // create an executor of 8 workers
tf::Taskflow taskflow;

tf::Semaphore semaphore(3); // create a semaphore with initial value of 3

std::vector<tf::Task> tasks {
  taskflow.emplace([](){ std::cout << "A" << std::endl; }),
  taskflow.emplace([](){ std::cout << "B" << std::endl; }),
  taskflow.emplace([](){ std::cout << "C" << std::endl; }),
  taskflow.emplace([](){ std::cout << "D" << std::endl; }),
  taskflow.emplace([](){ std::cout << "E" << std::endl; })
};

for(auto & task : tasks) {  // each task acquires and release the semaphore
  task.acquire(semaphore);
  task.release(semaphore);
}

executor.run(taskflow).wait();
# One possible output: A, B, and C run concurrently, D and E run concurrently
ABC
ED

        信号量不仅能够限制某段任务的最大并发数,还可以用于限制不同任务段的并发性,因此功能非常强大。具体来说,可以让一个任务获取信号量,并让另一个任务释放该信号量,从而对任务子图施加并发控制。以下示例展示了如何使用信号量(而非显式的依赖关系)来序列化五对任务的执行。

tf::Executor executor(4);  // creates an executor of 4 workers
tf::Taskflow taskflow;
tf::Semaphore semaphore(1);

int N = 5;
int counter = 0;  // non-atomic integer counter

for(int i=0; i<N; i++) {
  tf::Task f = taskflow.emplace([&](){ counter++; })
                       .name("from-"s + std::to_string(i));
  tf::Task t = taskflow.emplace([&](){ counter++; })
                       .name("to-"s + std::to_string(i));
  f.precede(t);
  f.acquire(semaphore);
  t.release(semaphore);
}

executor.run(taskflow).wait();

assert(counter == 2*N);

       如果没有信号量,每对任务(例如,from-0 -> to-0)将会独立且并发地运行。然而,该程序强制每个 `from` 任务在运行其工作之前获取信号量,并在其配对的 `to` 任务完成之后才释放信号量。这种约束使得每对任务按顺序运行,而哪一对任务先运行则取决于调度器

在不同任务间使用信号量  

        您可以使用信号量来限制不同任务流图部分之间的并发性。当您向执行器提交多个任务流时,执行器会将它们视为一组相互依赖的任务。无论哪个任务流中的任务获取或释放信号量都无关紧要。

tf::Executor executor(8);   // create an executor of 8 workers
tf::Taskflow taskflow1;
tf::Taskflow taskflow2;

tf::Semaphore semaphore(1); // create a semaphore with initial value of 1

taskflow1.emplace([](){std::cout << "task in taskflow1"; })
         .acquire(semaphore)
         .release(semaphore);

taskflow2.emplace([](){std::cout << "task in taskflow2"; })
         .acquire(semaphore)
         .release(semaphore);

executor.run(taskflow1);
executor.run(taskflow2);
executor.wait_for_all();

        上述示例从每个任务流中创建一个任务,并将这两个任务流提交给执行器。同样,在正常情况下,这两个任务可以并发运行,但信号量限制了只能由一个工作线程按任意顺序依次运行这两个任务。

定义冲突图  

        tf::Semaphore 的一个重要应用是通过冲突图实现冲突感知调度。冲突图是一种无向图,其中每个顶点代表一个任务,每条边表示一对任务之间的冲突。当一个任务与另一个任务冲突时,它们不能同时运行。以下面的冲突图为例,任务 A 与任务 B 和任务 C 冲突(反之亦然),这意味着 A 不能与 B 或 C 同时运行,而 B 和 C 可以同时运行。

        我们可以为冲突图中的每条边创建一个并发量为 1 的信号量,并让该边所连接的两个任务获取该信号量。这种组织方式强制这两个任务不能同时运行。

tf::Executor executor;
tf::Taskflow taskflow;

tf::Semaphore conflict_AB(1);
tf::Semaphore conflict_AC(1);

tf::Task A = taskflow.emplace([](){ std::cout << "A" << std::endl; });
tf::Task B = taskflow.emplace([](){ std::cout << "B" << std::endl; });
tf::Task C = taskflow.emplace([](){ std::cout << "C" << std::endl; });

// describe the conflict between A and B
A.acquire(conflict_AB).release(conflict_AB);
B.acquire(conflict_AB).release(conflict_AB);

// describe the conflict between A and C
A.acquire(conflict_AC).release(conflict_AC);
C.acquire(conflict_AC).release(conflict_AC);

executor.run(taskflow).wait();
# One possible output: B and C run concurrently after A
A
BC

【注意】  
一个任务可以获取和释放多个信号量。当执行器运行某个任务时,它会尝试获取该任务所需的所有信号量。当执行器完成该任务时,它会释放该任务所获取的所有信号量。

重置信号量  

您可以使用 tf::Semaphore::reset() 将信号量重置为其初始状态,或者通过 tf::Semaphore::reset(size_t new_max_value) 设置一个新的最大值。方法 tf::Semaphore::value()` 允许您查询信号量的当前值,该值表示可用的获取次数。

tf::Semaphore semaphore(4);
assert(semaphore.value() == 4 && semaphore.max_value() == 4);

// reset the semaphore to a new value
semaphore.reset(11);
assert(semaphore.value() == 11 && semaphore.max_value() == 11);

【注意】  
当一个信号量被获取的次数超过其最大值时,将抛出异常。

理解信号量的局限性  

        目前,tf::Semaphore 对异常处理和任务流取消的支持有限。如果某个任务抛出异常,或者任务流被取消,信号量上的后续获取和释放操作可能会导致未定义行为。为确保正确的行为,您应该在下一次运行之前调用 tf::Semaphore::reset 来重置信号量。例如,在以下代码中,当任务 B 抛出异常时,执行器将取消任务流的执行。也就是说,任务 C 和 D 将不会运行,因此没有任何任务会释放已获取的信号量。为了解决这种情况,我们必须将信号量重置为干净状态以供下次运行使用。

tf::Executor executor;
tf::Taskflow taskflow;
tf::Semaphore semaphore(1);

tf::Task A = taskflow.emplace([](){});
tf::Task B = taskflow.emplace([](){ throw std::runtime_error("exception"); });
tf::Task C = taskflow.emplace([](){});
tf::Task D = taskflow.emplace([](){});
A.precede(B);
B.precede(C);
C.precede(D);

A.acquire(semaphore);
D.release(semaphore);

// current semaphore has a value of 1
assert(semaphore.value() == 1);

// when B throws the exception, D will not run and thus semaphore is not released
try {
  executor.run(taskflow).get();
}
catch(std::runtime_error& e) {
  std::cout << e.what() << std::endl;
}

// since A acquired the semaphore, its value is 0
assert(semaphore.value() == 0);

// reset the semaphore to a clean state before running the taskflow again
semaphore.reset();
assert(semaphore.value() == 1);

executor.run(taskflow).get();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值