[笔记]C++并发编程实战 《二》线程管理(二)

简介: [笔记]C++并发编程实战 《二》线程管理(二)

2.2 向线程函数传递参数

清单2.4中,向 std::thread 构造函数中的可调用对象,或函数传递一个参数很简单。需要注意的是,默认参数要拷贝到线程独立内存中,即使参数是引用的形式,也可以在新线程中进行访问。

再来看一个例子:

void f(int i, std::string const& s);
std::thread t(f, 3, "hello");

代码创建了一个调用f(3, "hello")的线程。

注意,函数f需要一个 std::string 对象作为第二个参数,但这里使用的是字符串的字面值,也就是 char const * 类型。之后,在线程的上下文中完成字面值向 std::string 对象的转化。

需要特别要注意,当指向动态变量的指针作为参数传递给线程的情况,代码如下:

void f(int i, std::string const &s);
void oops(int some_param)
{
    char buffer[1024]; // 1
    sprintf(buffer, "%i", some_param);
    std::thread t(f, 3, buffer); // 2
    t.detach();
}

这种情况下,buffer①是一个指针变量,指向本地变量,然后本地变量通过buffer传递到新线程中②。并且,函数有很有可能会在字面值转化成 std::string 对象之前崩溃(oops),从而导致一些未定义的行为。

内核崩溃(oops)

并且想要依赖隐式转换将字面值转换为函数期待的 std::string 对象,但因 std::thread 的构造函数会复制提供的变量,就只复制了没有转换成期望类型的字符串字面值。

解决方案就是:

  • 在传递到 std::thread 构造函数之前就将字面值转化为 std::string 对象:
void f(int i, std::string const &s);
void not_oops(int some_param)
{
    char buffer[1024];
    sprintf(buffer, "%i", some_param);
    std::thread t(f, 3, std::string(buffer)); // 使用std::string,避免悬垂指针
    t.detach();
}

还可能遇到相反的情况:

  • 期望传递一个非常量引用(但这不会被编译),但整个对象被复制了。

你可能会尝试使用线程更新一个引用传递的数据结构,比如:

void update_data_for_widget(widget_id w, widget_data &data); // 1
void oops_again(widget_id w)
{
    widget_data data;
    std::thread t(update_data_for_widget, w, data); // 2
    display_status();
    t.join();
    process_widget_data(data);
}

虽然update_data_for_widget①的第二个参数期待传入一个引用,但是 std::thread 的构造函数②并不知晓;构造函数无视函数期待的参数类型,并盲目的拷贝已提供的变量。

不过,在代码会将参数以右值的方式进行拷贝传递,这是为了照顾到那些只能进行移动的类型,而后会以右值为参数调用update_data_for_widget。因为函数期望的是一个非常量引用作为参数,而非一个右值作为参数,所以会在编译时出错。

对于熟悉std::bind 的开发者来说,问题的解决办法是显而易见的:

  • 可以使用 std::ref 将参数转换成引用的形式,从而可将线程的调用改为以下形式:
std::thread t(update_data_for_widget,w,std::ref(data));

在这之后,update_data_for_widget就会接收到一个data变量的引用,而非一个data变量拷贝的引用,这样代码就能顺利的通过编译。

如果你熟悉std::bind ,就应该不会对以上述传参的形式感到奇怪,因为 std::thread 构造函数和 std::bind 的操作都在标准库中定义好了,可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数:

class X
{
public:
  void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work,&my_x); // 1

这段代码中,新线程将my_x.do_lengthy_work()作为线程函数;my_x的地址①作为指针对象提供给函数。也可以为成员函数提供参数: std::thread 构造函数的第三个参数就是成员函数的第一个参数,以此类推(代码如下,译者自加)。

class X
{
public:
void do_lengthy_work(int);
};
X my_x;
int num(0);
std::thread t(&X::do_lengthy_work, &my_x, num);

有趣的是,提供的参数可以移动,但不能拷贝。

"移动"是指:原始对象中的数据转移给另一对象,而转移的这些数据就不再在原始对象中保存了(译者:比较像在文本编辑的"剪切"操作)。 std::unique_ptr 就是这样一种类型(译者:C++11中的智能指针),这种类型为动态分配的对象提供内存自动管理机制(译者:类似垃圾回收)。同一时间内,只允许一个 std::unique_ptr 实现指向一个给定对象,并且当这个实现销毁时,指向的对象也将被删除。

移动构造函数(move constructor)移动赋值操作符(move assignment operator)允许一个对象在多个std::unique_ptr实现中传递(有关"移动"的更多内容,请参考附录A的A.1.1节)。

使用"移动"转移原对象后,就会留下一个空指针(NULL)

移动操作可以将对象转换成可接受的类型,例如:函数参数或函数返回的类型。

当原对象是一个临时变量时,自动进行移动操作,但当原对象是一个命名变量,那么转移的时候就需要使用 std::move() 进行显示移动。

下面的代码展示了 std::move 的用法,展示了 std::move 是如何转移一个动态对象到一个线程中去的:

void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));

std::thread 的构造函数中指定 std::move§ ,big_object对象的所有权就被首先转移到新创建线程的的内部存储中,之后传递给process_big_object函数。

c++标准线程库中和 std::unique_ptr 在所属权上有相似语义类型的类有好几种,

std::thread为其中之一。

虽然, std::thread 实例不像 std::unique_ptr 那样能占有一

个动态对象的所有权,但是它能占有其他资源:

  • 每个实例都负责管理一个执行线程。执行线程的所有权可以在多个 std::thread 实例中互相转移,这是依赖于 std::thread 实例的可移动且不可复制性。
  • 不可复制保性证了在同一时间点,一个 std::thread 实例只能关联一个执行线程;

可移动性使得开发者可以自己决定,哪个实例拥有实际执行线程的所有权。

2.3 转移线程所有权

假设要写一个在后台启动线程的函数,并想通过新线程返回的所有权去调用这个函数,而不是等待线程结束再去调用;或完全与之相反的想法:创建一个线程,并在函数中转移所有权,都必须要等待线程结束。所以,新线程的所有权都需要转移。

这就是将移动操作引入 std::thread 的原因,C++标准库中有很多资源占有(resource-owning)类型,比如 std::ifstream , std::unique_ptr 还有 std::thread 都是可移动,但不可拷贝。

这就说明执行线程的所有权可以在 std::thread 实例中移动,下面将展示一个例子。

例子中,创建了两个执行线程,并且在 std::thread 实例之间(t1,t2和t3)转移所有权:

void some_function();
void some_other_function();
std::thread t1(some_function); // 1
std::thread t2=std::move(t1); // 2
t1=std::thread(some_other_function); // 3
std::thread t3; // 4
t3=std::move(t2); // 5
t1=std::move(t3); // 6 赋值操作将使程序崩溃

首先,新线程开始与t1相关联①。当显式使用 std::move() 创建t2后②,t1的所有权就转移给了t2。之后,t1和执行线程已经没有关联了,执行some_function的函数线程与t2关联。

然后,一个临时 std::thread 对象相关的线程启动了③。

为什么不显式调用 std::move() 转移所有权呢?

因为,所有者是一个临时对象——移动操作将会隐式的调用。

t3使用默认构造方式创建④,与任何执行线程都没有关联。调用 std::move() 将与t2关联线程的所有权转移到t3中⑤。因为t2是一个命名对象,需要显式的调用 std::move()

移动操作完成后,t1与执行some_other_function的线程相关联,t2与任何线程都无关联,t3与执行some_function的线程相关联。

最后一个移动操作,将some_function线程的所有权转移⑥给t1。不过,t1已经有了一个关联的线程(执行some_other_function的线程),所以这里系统直接调用 std::terminate()终止程序继续运行。这样做(不抛出异常, std::terminate() 是noexcept函数)是为了保证

与 std::thread 的析构函数的行为一致。2.1.1节中,需要在线程对象被析构前,显式的等待线程完成,或者分离它;进行赋值时也需要满足这些条件(说明:不能通过赋一个新值

给 std::thread 对象的方式来"丢弃"一个线程)。

std::thread 支持移动,就意味着线程的所有权可以在函数外进行转移,就如下面程序一样。

清单2.5 函数返回 std::thread 对象

std::thread f()
{
void some_function();
return std::thread(some_function);
}
std::thread g()
{
void some_other_function(int);
std::thread t(some_other_function,42);
return t;
}

当所有权可以在函数内部传递,就允许 std::thread 实例可作为参数进行传递,代码如下:

void f(std::thread t);
void g()
{
void some_function();
f(std::thread(some_function));
std::thread t(some_function);
f(std::move(t));
}

std::thread 支持移动的好处是可以创建thread_guard类的实例(定义见清单2.3),并且拥有其线程所有权。当thread_guard对象所持有的线程被引用时,移动操作就可以避免很多不必要的麻烦;这意味着,当某个对象转移了线程的所有权后,它就不能对线程进行加入或分离。

为了确保线程程序退出前完成,下面的代码里定义了scoped_thread类。现在,我们来看一下这段代码:

清单2.6 scoped_thread的用法

class scoped_thread
{
std::thread t;
public:
  explicit scoped_thread(std::thread t_): // 1
  t(std::move(t_))
  {
    if(!t.joinable()) // 2
    throw std::logic_error(“No thread”);
  }
  ~scoped_thread()
  {
    t.join(); // 3
  }
  scoped_thread(scoped_thread const&)=delete;
  scoped_thread& operator=(scoped_thread const&)=delete;
};
struct func; // 定义在清单2.1中
void f()
{
  int some_local_state;
  scoped_thread t(std::thread(func(some_local_state))); // 4
  do_something_in_current_thread();
}

与清单2.3相似,不过新线程直接传递到scoped_thread中④,而非创建一个独立变量。当主线程到达f()函数末尾时⑤,scoped_thread对象就会销毁,然后加入③到的构造函数①创建的线程对象中去。在清单2.3中的thread_guard类,需要在析构中检查线程是否"可加入"。这里把检查放在了构造函数中②,并且当线程不可加入时,抛出异常。

这里对C++17标准给出一个建议,就是添加一个joining_thread的类型,这个类型

与 std::thread 类似;不同是的添加了析构函数,就类似于scoped_thread。委员会成员们对此并没有达成统一共识,所以这个类没有添加入C++17标准中(C++20仍旧对这种方式进行探讨,不过名称为 std::jthread ),不过这个类实现起来也不是很困难。

下面就来对这个类进行实现:

清单2.7 joining_thread类的实现

class joining_thread
{
    std::thread t;
public:
    joining_thread() noexcept = default;
    template <typename Callable, typename... Args>
    explicit joining_thread(Callable &&func, Args &&...args) : t(std::forward<Callable>(func), std::forward<Args>(args)...)
    {
    }
    explicit joining_thread(std::thread t_) noexcept : t(std::move(t_))
    {
    }
    joining_thread(joining_thread &&other) noexcept : t(std::move(other.t))
    {
    }
    joining_thread &operator=(joining_thread &&other) noexcept
    {
        if(joinable())
        {
            join();
        }
        t = std::move(other.t);
        return *this;
    }
    joining_thread &operator=(std::thread other) noexcept
    {
        if (joinable())
            join();
        t = std::move(other);
        return *this;
    }
    ~joining_thread() noexcept
    {
        if (joinable())
            join();
    }
    void swap(joining_thread &other) noexcept
    {
        t.swap(other.t);
    }
    std::thread::id get_id() const noexcept
    {
        return t.get_id();
    }
    bool joinable() const noexcept
    {
        return t.joinable();
    }
    void join()
    {
        t.join();
    }
    void detach()
    {
        t.detach();
    }
    std::thread &as_thread() noexcept
    {
        return t;
    }
    const std::thread &as_thread() const noexcept
    {
        return t;
    }
};

std::thread 对象的容器,如果这个容器是移动敏感的(比如,标准中的 std::vector<> ),那

么移动操作同样适用于这些容器。了解这些后,就可以写出类似清单2.7中的代码,代码量产了一些线程,并且等待它们结束。

清单2.8 量产线程,等待它们结束

void do_work(unsigned id);
void f()
{
  std::vector<std::thread> threads;
  for(unsigned i=0; i < 20; ++i)
  {
    threads.push_back(std::thread(do_work,i)); // 产生线程
  }
  std::for_each(threads.begin(),threads.end(),
  std::mem_fn(&std::thread::join)); // 对每个线程调用join()
}

我们经常需要线程去分割一个算法的工作总量,所以在算法结束的之前,所有的线程必须结束。清单2.8中线程所做的工作都是独立的,并且结果仅会受到共享数据的影响。如果f()有返回值,这个返回值就依赖于线程得到的结果。在写入返回值之前,程序会检查使用共享数据的线程是否终止。结果在不同线程中转移的替代方案,我们会在第4章中再次讨论。

将 std::thread 放入 std::vector 是向线程自动化管理迈出的第一步:并非为这些线程创建独立的变量,并且直接加入,而是把它们当做一个组。创建一组线程(数量在运行时确定),可使得这一步迈的更大,而非像清单2.8那样创建固定数量的线程。

2.4 运行时决定线程数量

std::thread::hardware_concurrency() 在新版C++标准库中是一个很有用的函数。这个函数会返回能并发在一个程序中的线程数量。例如,多核系统中,返回值可以是CPU核芯的数量。

返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。但是,这也无法掩盖这个函数对启动线程数量的帮助。

清单2.9实现了一个并行版的 std::accumulate 。代码中将整体工作拆分成小任务交给每个线程去做,其中设置最小任务数,是为了避免产生太多的线程。程序可能会在操作数量为0的时候抛出异常。比如, std::thread 构造函数无法启动一个执行线程,就会抛出一个异常。在这个算法中讨论异常处理,已经超出现阶段的讨论范围,这个问题我们将在第8章中再来讨论。

清单2.9 原生并行版的 std::accumulate

template <typename Iterator, typename T>
struct accumulate_block
{
    void operator()(Iterator first, Iterator last, T &result)
    {
        result = std::accumulate(first, last, result);
    }
};
template <typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init)
{
    unsigned long const length = std::distance(first, last);
    if (!length) // 1
        return init;
    unsigned long const min_per_thread = 25;
    unsigned long const max_threads =
        (length + min_per_thread - 1) / min_per_thread; // 2
    unsigned long const hardware_threads =
        std::thread::hardware_concurrency();
    unsigned long const num_threads = // 3
        std::min(hardware_threads != 0 ? hardware_threads : 2,
                 max_threads);
    unsigned long const block_size = length / num_threads; // 4
    std::vector<T> results(num_threads);
    std::vector<std::thread> threads(num_threads - 1); // 5
    Iterator block_start = first;
    for (unsigned long i = 0; i < (num_threads - 1); ++i)
    {
        Iterator block_end = block_start;
        std::advance(block_end, block_size); // 6
        threads[i] = std::thread(            // 7
            accumulate_block<Iterator, T>(),
            block_start, block_end, std::ref(results[i]));
        block_start = block_end; // #8
    }
    accumulate_block<Iterator, T>()(
        block_start, last, results[num_threads - 1]); // 9
    std::for_each(threads.begin(), threads.end(),
                  std::mem_fn(&std::thread::join));               // 10
    return std::accumulate(results.begin(), results.end(), init); // 11
}

函数看起来很长,但不复杂。如果输入的范围为空①,就会得到init的值。反之,如果范围内多于一个元素时,都需要用范围内元素的总数量除以线程(块)中最小任务数,从而确定启动线程的最大数量②,这样能避免无谓的计算资源的浪费。比如,一台32芯的机器上,只有5个数需要计算,却启动了32个线程。

计算量的最大值和硬件支持线程数中,较小的值为启动线程的数量③。因为上下文频繁的切换会降低线程的性能,所以你肯定不想启动的线程数多于硬件支持的线程数量。

当 std::thread::hardware_concurrency() 返回0,你可以选择一个合适的数作为你的选择;在本例中,我选择了"2"。你也不想在一台单核机器上启动太多的线程,因为这样反而会降低性能,有可能最终让你放弃使用并发。

每个线程中处理的元素数量,是范围中元素的总量除以线程的个数得出的④。对于分配是否得当,我们会在后面讨论。

现在,确定了线程个数,通过创建一个 std::vector 容器存放中间结果,并为线程创建一个 std::vectorstd::thread 容器 #5。这里需要注意的是,启动的线程数必须比num_threads少1个,因为在启动之前已经有了一个线程(主线程)。

使用简单的循环来启动线程:block_end迭代器指向当前块的末尾⑥,并启动一个新线程为当前块累加结果⑦。当迭代器指向当前块的末尾时,启动下一个块⑧。

启动所有线程后,⑨中的线程会处理最终块的结果。对于分配不均,因为知道最终块是哪一个,那么这个块中有多少个元素就无所谓了。

当累加最终块的结果后,可以等待 std::for_each ⑩创建线程的完成(如同在清单2.8中做的那样),之后使用 std::accumulate 将所有结果进行累加⑪。

结束这个例子之前,需要明确:T类型的加法运算不满足结合律(比如,对于float型或double型,在进行加法操作时,系统很可能会做截断操作),因为对范围中元素的分组,会导致parallel_accumulate得到的结果可能与 std::accumulate 得到的结果不同。同样的,这里对迭代器的要求更加严格:必须都是向前迭代器,而 std::accumulate 可以在只传入迭代器的情况下工作。对于创建出results容器,需要保证T有默认构造函数。对于算法并行,通常都要这样的修改;不过,需要根据算法本身的特性,选择不同的并行方式。算法并行会在第8章有更加深入的讨论,并在第10章中会介绍一些C++17中支持的并行算法(其中 std::reduce 操作等价于这里的parallel_accumulate)。需要注意的:因为不能直接从一个线程中返回一个值,所以需要传递results容器的引用到线程中去。另一个办法,通过地址来获取线程执行的结果;第4章中,我们将使用期望(futures)完成这种方案。

当线程运行时,所有必要的信息都需要传入到线程中去,包括存储计算结果的位置。不过,并非总需如此:有时候这是识别线程的可行方案,可以传递一个标识数,例如清单2.8中的i。

不过,当需要标识的函数在调用栈的深层,同时其他线程也可调用该函数,那么标识数就会变的捉襟见肘。好消息是在设计C++的线程库时,就有预见了这种情况,在之后的实现中就给每个线程附加了唯一标识符。

2.5 标识线程

线程标识类型为 std::thread::id ,并可以通过两种方式进行检索。

第一种,可以通过调用 std::thread 对象的成员函数 get_id() 来直接获取。如果 std::thread 对象没有与任何执行线程相关联, get_id() 将返回 std::thread::type 默认构造值,这个值表示“无线程”。

第二种,当前线程中调用 std::this_thread::get_id() (这个函数定义在 头文件中)也可以获得线程标识。

std::thread::id 对象可以自由的拷贝和对比,因为标识符就可以复用。如果两个对象

的 std::thread::id 相等,那它们就是同一个线程,或者都“无线程”。如果不等,那么就代表了两个不同线程,或者一个有线程,另一没有线程。

C++线程库不会限制你去检查线程标识是否一样, std::thread::id 类型对象提供相当丰富的对比操作;比如,提供为不同的值进行排序。这意味着允许程序员将其当做为容器的键值,做排序,或做其他方式的比较。按默认顺序比较不同值的 std::thread::id ,所以这个行为可预见的:当 a<b , b<c 时,得 a<c ,等等。标准库也提供 std::hashstd::thread::id 容器,所以 std::thread::id 也可以作为无序容器的键值。

std::thread::id 实例常用作检测线程是否需要进行一些操作,比如:当用线程来分割一项工作(如清单2.9),主线程可能要做一些与其他线程不同的工作。这种情况下,启动其他线程前,它可以将自己的线程ID通过 std::this_thread::get_id() 得到,并进行存储。就是算法核心部分(所有线程都一样的),每个线程都要检查一下,其拥有的线程ID是否与初始线程的ID相同。

std::thread::id master_thread;
void some_core_part_of_algorithm()
{
  if(std::this_thread::get_id()==master_thread)
  {
  do_master_thread_work();
  }
  do_common_work();
}

另外,当前线程的 std::thread::id 将存储到一个数据结构中。之后在这个结构体中对当前线程的ID与存储的线程ID做对比,来决定操作是被“允许”,还是需要(permitted/required)

同样,作为线程和本地存储不适配的替代方案,线程ID在容器中可作为键值。例如,容器可以存储其掌控下每个线程的信息,或在多个线程中互传信息。

std::thread::id 可以作为一个线程的通用标识符,当标识符只与语义相关(比如,数组的索

引)时,就需要这个方案了。也可以使用输出流( std::cout )来记录一个 std::thread::id 对象

的值。

std::cout<<std::this_thread::get_id();

具体的输出结果是严格依赖于具体实现的,C++标准的唯一要求就是要保证ID比较结果相等的线程,必须有相同的输出。

总结

本章讨论了C++标准库中基本的线程管理方式:启动线程,等待结束和不等待结束(因为需要它们运行在后台)。并了解应该如何在线程启动前,向线程函数中传递参数,如何转移线程的所有权,如何使用线程组来分割任务。最后,讨论了使用线程标识来确定关联数据,以及特殊线程的特殊解决方案。虽然,现在已经可以纯粹的依赖线程,使用独立的数据,做独立的任务,但在某些情况下,线程间确实需要有共享数据。第3章会讨论共享数据和线程的直接关系。

第4章会讨论在有/没有共享数据情况下的线程同步操作。


相关文章
|
15天前
|
缓存 安全 C++
C++无锁队列:解锁多线程编程新境界
【10月更文挑战第27天】
30 7
|
15天前
|
消息中间件 存储 安全
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
43 1
C++ 多线程之初识多线程
|
22天前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
21天前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
|
30天前
|
安全 程序员 编译器
【实战经验】17个C++编程常见错误及其解决方案
想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。
88 5
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
45 6
|
1月前
|
缓存 负载均衡 Java
c++写高性能的任务流线程池(万字详解!)
本文介绍了一种高性能的任务流线程池设计,涵盖多种优化机制。首先介绍了Work Steal机制,通过任务偷窃提高资源利用率。接着讨论了优先级任务,使不同优先级的任务得到合理调度。然后提出了缓存机制,通过环形缓存队列提升程序负载能力。Local Thread机制则通过预先创建线程减少创建和销毁线程的开销。Lock Free机制进一步减少了锁的竞争。容量动态调整机制根据任务负载动态调整线程数量。批量处理机制提高了任务处理效率。此外,还介绍了负载均衡、避免等待、预测优化、减少复制等策略。最后,任务组的设计便于管理和复用多任务。整体设计旨在提升线程池的性能和稳定性。
74 5
|
1月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
23 0
C++ 多线程之线程管理函数
|
1月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
29 0
Linux C/C++之线程基础