简体   繁体   English

每个条件唤醒多个线程工作一次

[英]Waking up multiple threads to work once per condition

I have a situation where one thread needs to occasionally wake up a number of worker threads and each worker thread needs to do it's work (only) once and then go back to sleep to wait for the next notification.我有一种情况,一个线程需要偶尔唤醒多个工作线程,每个工作线程需要(仅)一次完成它的工作,然后 go 回到睡眠状态以等待下一个通知。 I'm using a condition_variable to wake everything up, but the problem I'm having is the "only once" part.我正在使用 condition_variable 唤醒一切,但我遇到的问题是“只有一次”部分。 Assume that each thread is heavy to create, so I don't want to just be creating and joining them each time.假设每个线程的创建都很繁重,所以我不想每次都创建和加入它们。

// g++ -Wall -o threadtest -pthread threadtest.cpp
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <chrono>

std::mutex condMutex;
std::condition_variable condVar;
bool dataReady = false;

void state_change_worker(int id)
{
    while (1)
    {
        {
            std::unique_lock<std::mutex> lck(condMutex);
            condVar.wait(lck, [] { return dataReady; });
            // Do work only once.
            std::cout << "thread " << id << " working\n";
        }
    }
}

int main()
{
    // Create some worker threads.
    std::thread threads[5];
    for (int i = 0; i < 5; ++i)
        threads[i] = std::thread(state_change_worker, i);

    while (1)
    {
        // Signal to the worker threads to work.
        {
            std::cout << "Notifying threads.\n";
            std::unique_lock<std::mutex> lck(condMutex);
            dataReady = true;
            condVar.notify_all();
        }
        // It would be really great if I could wait() on all of the 
        // worker threads being done with their work here, but it's 
        // not strictly necessary.
        std::cout << "Sleep for a bit.\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }
}

Update: Here is a version implementing an almost-but-not-quite working version of a squad lock.更新:这是一个实现小队锁的几乎但不完全工作版本的版本。 The problem is that I can't guarantee that each thread will have a chance to wake up and derement count in waitForLeader() before one runs through again.问题是我不能保证每个线程在再次运行之前都有机会唤醒并减少 waitForLeader() 中的计数。

// g++ -Wall -o threadtest -pthread threadtest.cpp
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <chrono>

class SquadLock
{
public:
    void waitForLeader()
    {
        {
            // Increment count to show that we are waiting in queue.
            // Also, if we are the thread that reached the target, signal
            // to the leader that everything is ready.
            std::unique_lock<std::mutex> count_lock(count_mutex_);
            std::unique_lock<std::mutex> target_lock(target_mutex_);
            if (++count_ >= target_)
                count_cond_.notify_one();
        }
        // Wait for leader to signal done.
        std::unique_lock<std::mutex> lck(done_mutex_);
        done_cond_.wait(lck, [&] { return done_; });
        {
            // Decrement count to show that we are no longer waiting.
            // If we are the last thread set done to false.
            std::unique_lock<std::mutex> lck(count_mutex_);
            if (--count_ == 0)
            {
                done_ = false;
            }
        }
    }

    void waitForHerd()
    {
        std::unique_lock<std::mutex> lck(count_mutex_);
        count_cond_.wait(lck, [&] { return count_ >= target_; });
    }
    void leaderDone()
    {
        std::unique_lock<std::mutex> lck(done_mutex_);
        done_ = true;
        done_cond_.notify_all();
    }
    void incrementTarget()
    {
        std::unique_lock<std::mutex> lck(target_mutex_);
        ++target_;
    }
    void decrementTarget()
    {
        std::unique_lock<std::mutex> lck(target_mutex_);
        --target_;
    }
    void setTarget(int target)
    {
        std::unique_lock<std::mutex> lck(target_mutex_);
        target_ = target;
    }

private:
    // Condition variable to indicate that the leader is done.
    std::mutex done_mutex_;
    std::condition_variable done_cond_;
    bool done_ = false;

    // Count of currently waiting tasks.
    std::mutex count_mutex_;
    std::condition_variable count_cond_;
    int count_ = 0;

    // Target number of tasks ready for the leader.
    std::mutex target_mutex_;
    int target_ = 0;
};

SquadLock squad_lock;
std::mutex print_mutex;
void state_change_worker(int id)
{
    while (1)
    {
        // Wait for the leader to signal that we are ready to work.
        squad_lock.waitForLeader();
        {
            // Adding just a bit of sleep here makes it so that every thread wakes up, but that isn't the right way.
            // std::this_thread::sleep_for(std::chrono::milliseconds(100));
            std::unique_lock<std::mutex> lck(print_mutex);
            std::cout << "thread " << id << " working\n";
        }
    }
}

int main()
{

    // Create some worker threads and increment target for each one
    // since we want to wait until all threads are finished.
    std::thread threads[5];
    for (int i = 0; i < 5; ++i)
    {
        squad_lock.incrementTarget();
        threads[i] = std::thread(state_change_worker, i);
    }
    while (1)
    {
        // Signal to the worker threads to work.
        std::cout << "Starting threads.\n";
        squad_lock.leaderDone();
        // Wait for the worked threads to be done.
        squad_lock.waitForHerd();
        // Wait until next time, processing results.
        std::cout << "Tasks done, waiting for next time.\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }
}

Following is an excerpt from a blog I created concerning concurrent design patterns.以下是我创建的有关并发设计模式的博客的摘录。 The patterns are expressed using the Ada language, but the concepts are translatable to C++.模式使用 Ada 语言表达,但概念可翻译为 C++。

Summary概括

Many applications are constructed of groups of cooperating threads of execution.许多应用程序是由多组协作的执行线程构成的。 Historically this has frequently been accomplished by creating a group of cooperating processes.从历史上看,这经常通过创建一组合作进程来实现。 Those processes would cooperate by sharing data.这些过程将通过共享数据进行合作。 At first, only files were used to share data.起初,只使用文件来共享数据。 File sharing presents some interesting problems.文件共享提出了一些有趣的问题。 If one process is writing to the file while another process reads from the file you will frequently encounter data corruption because the reading process may attempt to read data before the writing process has completely written the information.如果一个进程正在写入文件,而另一个进程从文件中读取,您将经常遇到数据损坏,因为读取进程可能会在写入进程完全写入信息之前尝试读取数据。 The solution used for this was to create file locks, so that only one process at a time could open the file.用于此的解决方案是创建文件锁,以便一次只有一个进程可以打开文件。 Unix introduced the concept of a Pipe, which is effectively a queue of data. Unix 引入了 Pipe 的概念,它实际上是一个数据队列。 One process can write to a pipe while another reads from the pipe.一个进程可以写入 pipe,而另一个进程可以从 pipe 读取。 The operating system treats data in a pipe as a series of bytes.操作系统将 pipe 中的数据视为一系列字节。 It does not let the reading process access a particular byte of data until the writing process has completed its operation on the data.在写入进程完成对数据的操作之前,它不会让读取进程访问特定字节的数据。 Various operating systems also introduced other mechanisms allowing processes to share data.各种操作系统还引入了允许进程共享数据的其他机制。 Examples include message queues, sockets, and shared memory.示例包括消息队列、sockets 和共享 memory。 There were also special features to help programmers control access to data, such as semaphores.还有一些特殊功能可以帮助程序员控制对数据的访问,例如信号量。 When operating systems introduced the ability for a single process to operate multiple threads of execution, also known as lightweight threads, or just threads, they also had to provide corresponding locking mechanisms for shared data.当操作系统引入单个进程操作多个执行线程的能力时,也称为轻量级线程,或者只是线程,它们还必须为共享数据提供相应的锁定机制。 Experience shows that, while the variety of possible designs for shared data is quite large, there are a few very common design patterns that frequently emerge.经验表明,虽然共享数据的可能设计种类繁多,但经常出现一些非常常见的设计模式。 Specifically, there are a few variations on a lock or semaphore, as well as a few variations on data buffering.具体来说,锁或信号量有一些变化,数据缓冲也有一些变化。 This paper explores the locking and buffering design patterns for threads in the context of a monitor.本文探讨了监视器上下文中线程的锁定和缓冲设计模式。 Although monitors can be implemented in many languages, all examples in this paper are presented using Ada protected types.尽管监视器可以用多种语言实现,但本文中的所有示例都使用 Ada 保护类型。 Ada protected types are a very thorough implementation of a monitor. Ada 保护类型是一个非常彻底的监视器实现。

Monitors监视器

There are several theoretical approaches to creating and controlling shared memory.有几种理论方法可以创建和控制共享 memory。 One of the most flexible and robust is the monitor as first described by C.A.R. C.A.R 首次描述的监视器是最灵活和最强大的监视器之一。 Hoare.霍尔。 A monitor is a data object with three different kinds of operations.监视器是具有三种不同操作的数据 object。

Procedures are used to change the state or values contained by the monitor.程序用于更改 state 或监视器包含的值。 When a thread calls a monitor procedure that thread must have exclusive access to the monitor to prevent other threads from encountering corrupted or partially written data.当线程调用监视器过程时,该线程必须对监视器具有独占访问权限,以防止其他线程遇到损坏或部分写入的数据。

Entries, like procedures, are used to change the state or values contained by the monitor, but an entry also specifies a boundary condition.条目与过程类似,用于更改 state 或监视器包含的值,但条目也指定边界条件。 The entry may only be executed when the boundary condition is true.该条目只能在边界条件为真时执行。 Threads that call an entry when the boundary condition is false are placed in a queue until the boundary condition becomes true.当边界条件为假时调用条目的线程被放入队列中,直到边界条件变为真。 Entries are used, for example, to allow a thread to read from a shared buffer.例如,条目用于允许线程从共享缓冲区中读取。 The reading thread is not allowed to read the data until the buffer actually contains some data.在缓冲区实际包含一些数据之前,不允许读取线程读取数据。 The boundary condition would be that the buffer must not be empty.边界条件是缓冲区不能为空。 Entries, like procedures, must have exclusive access to the monitor's data.条目和过程一样,必须具有对监视器数据的独占访问权。

Functions are used to report the state of a monitor.函数用于报告监视器的 state。 Since functions only report state, and do not change state, they do not need exclusive access to the monitor's data.由于函数只报告 state,而不更改 state,因此它们不需要独占访问监视器的数据。 Many threads may simultaneously access the same monitor through functions without danger of data corruption.许多线程可以通过函数同时访问同一个监视器,而没有数据损坏的危险。

The concept of a monitor is extremely powerful.监视器的概念非常强大。 It can also be extremely efficient.它也可以非常有效。 Monitors provide all the capabilities needed to design efficient and robust shared data structures for threaded systems.监视器提供了为线程系统设计高效和健壮的共享数据结构所需的所有功能。 Although monitors are powerful, they do have some limitations.尽管监视器功能强大,但它们确实有一些局限性。 The operations performed on a monitor should be very fast, with no chance of making a thread block.在监视器上执行的操作应该非常快,不会造成线程阻塞。 If those operations should block, the monitor will become a road block instead of a communication tool.如果这些操作被阻止,监视器将成为路障而不是通信工具。 All the threads awaiting access to the monitor will be blocked as long as the monitor operation is blocked.只要监视器操作被阻塞,所有等待访问监视器的线程都会被阻塞。 For this reason, some people choose not to use monitors.出于这个原因,有些人选择不使用显示器。 There are design patterns for monitors that can actually be used to work around these problems.有一些监视器设计模式实际上可以用来解决这些问题。 Those design patterns are grouped together as locking patterns.这些设计模式被组合在一起作为锁定模式。

Squad Locks小队锁

A squad lock allows a special task (the squad leader) to monitor the progress of a herd or group of worker tasks.班锁允许特殊任务(班长)监控一群或一组工人任务的进度。 When all (or a sufficient number) of the worker tasks are done with some aspect of their work, and the leader is ready to proceed, the entire set of tasks is allowed to pass a barrier and continue with the next sequence of their activities.当所有(或足够数量)的工作任务完成了他们工作的某个方面,并且领导者准备好继续进行时,整个任务集被允许通过障碍并继续他们的下一个活动序列。 The purpose is to allow tasks to execute asynchronously, yet coordinate their progress through a complex set of activities.目的是允许任务异步执行,但通过一组复杂的活动协调它们的进度。

package Barriers is
   protected type Barrier(Trigger : Positive) is
      entry Wait_For_Leader; 
      entry Wait_For_Herd; 
      procedure Leader_Done; 
   private
      Done : Boolean := False;
   end Barrier;

   protected type Autobarrier(Trigger : Positive) is
      entry Wait_For_Leader; 
      entry Wait_For_Herd; 
   private
      Done : Boolean := False;
   end Autobarrier;
end Barriers;

This package shows two kinds of squad lock.这个package展示了两种小队锁。 The Barrier protected type demonstrates a basic squad lock.屏障保护类型展示了基本的小队锁。 The herd calls Wait_For_Leader and the leader calls Wait_For_Herd and then Leader_Done.牛群调用 Wait_For_Leader,领导者调用 Wait_For_Herd,然后是 Leader_Done。 The Autobarrier demonstrates a simpler interface. Autobarrier 展示了一个更简单的界面。 The herd calls Wait_For_Leader and the leader calls Wait_For_Herd.牛群调用 Wait_For_Leader,领导者调用 Wait_For_Herd。 The Trigger parameter is used when creating an instance of either type of barrier.在创建任一类型屏障的实例时使用 Trigger 参数。 It sets the minimum number of herd tasks the leader must wait for before it can proceed.它设置领导者在继续之前必须等待的最小群体任务数。

package body Barriers is
   protected body Barrier is
      entry Wait_For_Herd when Wait_For_Leader'Count >= Trigger is
      begin
         null;
      end Wait_For_Herd;

      entry Wait_For_Leader when Done is
      begin
         if Wait_For_Leader'Count = 0 then
            Done := False;
         end if;
      end Wait_For_Leader;

      procedure Leader_Done is
      begin
         Done := True;
      end Leader_Done;
   end Barrier;

   protected body Autobarrier is
      entry Wait_For_Herd when Wait_For_Leader'Count >= Trigger is
      begin
         Done := True;
      end Wait_For_Herd;

      entry Wait_For_Leader when Done is
      begin
         if Wait_For_Leader'Count = 0 then
            Done := False;
         end if;
      end Wait_For_Leader;
   end Autobarrier;
end Barriers;

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM