C++ std::thread 和 std::jthread 使用详解 (含C++20新特性)

目录

std::thread

std::thread 构造函数 

观察器

操作

std::jthread

std::jthread 构造函数

观察器

操作

停止记号处理

管理当前线程的函数

yield()

get_id()

sleep_for()

sleep_until()


C++ 11之前,官方并没有支持线程库。在Linux下完成多线程编程时,多数情况下是使用#include <pthread.h>的函数。C++ 11通过标准库引入了对 thread 类的支持,大大方便了完成多线程开发的工作。在C++20中,引入的 jthread 类是 thread 自动合并和取消的实现版本。接下来将先从线程函数和 thread 类开始介绍,分析它们的不同,然后再介绍 jthread。

 

 

std::thread

 

std::thread 构造函数 

(1)thread() noexcept;
(2)thread( thread&& other ) noexcept;
(3)template< class Function, class... Args > 
   explicit thread( Function&& f, Args&&... args );
(4)thread(const thread&) = delete;

(1) 构造新 thread 对象,但由于没有传入函数,所以thread对象还没有关联到线程

(2) 移动构造函数。构造表示曾为 other 所表示的执行线程的 thread 对象。此调用后 other 不再表示执行线程。

(3) 构造新的 std::thread 对象并将它与执行线程关联。新的执行线程开始执行

(4) 复制构造函数被删除 thread 不可复制。

#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
 
void f1(int n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread 1 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
void f2(int& n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread 2 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
class foo
{
public:
    void bar()
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 3 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};
 
class baz
{
public:
    void operator()()
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 4 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};
 
int main()
{
    int n = 0;
    foo f;
    baz b;
    std::thread t1; // t1 不是线程
    std::thread t2(f1, n + 1); // 按值传递
    std::thread t3(f2, std::ref(n)); // 按引用传递
    std::thread t4(std::move(t3)); // t4 现在运行 f2() 。 t3 不再是线程
    std::thread t5(&foo::bar, &f); // t5 在对象 f 上运行 foo::bar()
    std::thread t6(std::ref(b)); // t6 在对象 b 上运行 baz::operator()
    t2.join();
    t4.join();
    t5.join();
    t6.join();
    std::cout << "Final value of n is " << n << '\n';
    std::cout << "Final value of foo::n is " << f.n << '\n';
    std::cout << "Final value of baz::n is " << b.n << '\n';
}

注意:若需要传递引用参数给线程函数,则必须包装它 (例如用 std::ref 或 std::cref)。忽略来自函数的任何返回值。若函数抛异常,则调用 std::terminate。

 

pthread_create 线程创建函数

int pthread_create (pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

功能:创建一个具有指定参数的线程。

形参:thread 是要创建的线程的线程ID指针。

pthread_t 类型的定义是 typedef unsigned long int pthread_t;(打印时要使用%lu或%u方式)。

attr:创建线程时的线程属性(设置NULL表示使用默认线程属性)。

start_routine:指向的是新线程将运行的函数。线程一旦被创建好,内核就可以调度内核线程来执行 start_routine 函数指针所指向的函数了。

arg:指向的是运行函数的形参。

返回值:若是成功建立线程返回0,否则返回错误的编号。

相对而言,使用线程创建函数更为复杂。但这并不意味这就不使用 pthread 提供的函数了,因为 pthread 中的函数可以设置线程的属性。这在某些情况下是非常有用的,所以有时会将两者结合起来使用。

 

 

观察器

joinable

bool joinable() const noexcept;

用于判断 thread 对象是否关联到某一线程,若 thread 对象执行线程关联,返回 true ,反之为 false 。

#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::thread t;
    std::cout << "before starting, joinable: " << std::boolalpha << t.joinable()
              << '\n';
 
    t = std::thread(foo);
    std::cout << "after starting, joinable: " << t.joinable() 
              << '\n';
 
    t.join();
    std::cout << "after joining, joinable: " << t.joinable() 
              << '\n';
}

before starting, joinable: false
after starting, joinable: true
after joining, joinable: false

 

get_id

std::thread::id get_id() const noexcept;

返回标识与 *this 关联的线程的 std::thread::id 

#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::thread t1(foo);
    std::thread::id t1_id = t1.get_id();
 
    std::thread t2(foo);
    std::thread::id t2_id = t2.get_id();
 
    std::cout << "t1's id: " << t1_id << '\n';
    std::cout << "t2's id: " << t2_id << '\n';
 
    t1.join();
    t2.join();
}

t1's id: 0x35a7210f
t2's id: 0x35a311c4

 

 

操作

join

void join();

阻塞当前线程直至 *this 所标识的线程结束其执行。*this 所标识的线程的完成同步于对应的从 join() 成功返回。*this 自身上不进行同步。同时从多个线程在同一 thread 对象上调用 join() 构成数据竞争,导致未定义行为。

#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    // 模拟昂贵操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
void bar()
{
    // 模拟昂贵操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::cout << "starting first helper...\n";
    std::thread helper1(foo);
 
    std::cout << "starting second helper...\n";
    std::thread helper2(bar);
 
    std::cout << "waiting for helpers to finish..." << std::endl;
    helper1.join();
    helper2.join();
 
    std::cout << "done!\n";
}

starting first helper...
starting second helper...
waiting for helpers to finish...
done!

 

pthread_join 等待线程结束函数

int pthread_join(pthread_t thread, void **retval);

功能:这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。

形参:thread是被等待的线程标识符。

retval:一个用户定义的指针,它可以用来存储被等待线程的返回值。

返回值:成功返回0,否则返回错误的编号。

错误码:

①EDEADLK:可能引起死锁,比如两个线程互相针对对方调用pthread_join,或者线程对自身调用pthread_join。

②EINVAL:目标线程是不可回收的,或者已经有其他线程在回收目标线程。

③ESRCH:目标线程不存在。

 

detach

void detach();

从 thread 对象分离执行线程,允许执行独立地持续。一旦该线程退出,则释放任何分配的资源。调用 detach 后 *this 不再占有任何线程。

#include <iostream>
#include <chrono>
#include <thread>
 
void independentThread() 
{
    std::cout << "Starting concurrent thread.\n";
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Exiting concurrent thread.\n";
}
 
void threadCaller() 
{
    std::cout << "Starting thread caller.\n";
    std::thread t(independentThread);
    t.detach();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Exiting thread caller.\n";
}
 
int main() 
{
    threadCaller();
    std::this_thread::sleep_for(std::chrono::seconds(5));
}

Starting thread caller.
Starting concurrent thread.
Exiting thread caller.
Exiting concurrent thread.

 

pthread_detach 分离释放线程函数

int pthread_detach (pthread_t thread);

功能:允许分离执行线程,即独立的执行。也是线程资源释放的一种方式,一旦线程退出,则释放分配的资源。

形参:thread 是要释放线程的标识符 Id

返回值:若是成功返回 0,否则返回错误的编号。

其他说明:linux 线程执行和 windows 不同,pthread 有两种状态 joinable 状态和 unjoinable 状态。一个线程默认的状态是 joinable,如果线程是 joinable 状态,当线程函数自己返回退出时或 pthread_exit 时,都不会释放线程所占用堆栈和线程描述符(总计8K多),只有当调用了 pthread_join 之后这些资源才会被释放。若是 unjoinable 状态的线程,这些资源在线程函数退出时或 pthread_exit 时自动会被释放。unjoinable 属性可以在 pthread_create 时指定,或在线程创建后在线程中 pthread_detach 自己设置,如:pthread_detach( pthread_self() ),将状态改为 unjoinable 状态,确保资源的释放。如果线程状态为 joinable ,需要在之后适时调用 pthread_join。

 

swap

void swap( std::thread& other ) noexcept;

交换二个 thread 对象的底层柄。

#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
void bar()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::thread t1(foo);
    std::thread t2(bar);
 
    std::cout << "thread 1 id: " << t1.get_id() << '\n'
              << "thread 2 id: " << t2.get_id() << '\n';
 
    std::swap(t1, t2);
 
    std::cout << "after std::swap(t1, t2):" << '\n'
              << "thread 1 id: " << t1.get_id() << '\n'
              << "thread 2 id: " << t2.get_id() << '\n';
 
    t1.swap(t2);
 
    std::cout << "after t1.swap(t2):" << '\n'
              << "thread 1 id: " << t1.get_id() << '\n'
              << "thread 2 id: " << t2.get_id() << '\n';
 
    t1.join();
    t2.join();
}

thread 1 id: 140185268262656
thread 2 id: 140185259869952
after std::swap(t1, t2):
thread 1 id: 140185259869952
thread 2 id: 140185268262656
after t1.swap(t2):
thread 1 id: 140185268262656
thread 2 id: 140185259869952

 

总结:

(0x01) std::thread 类创建线程非常方便,构造 thread 对象时传入一个需要运行的函数及其参数。构造完成后,新的线程马上被创建,同时执行该对象。注意:若需要传递引用参数给线程函数,则必须包装它(例如用 std::ref 或 std::cref)。

(0x02) 使用 std::thread 默认的构造函数构造对象时,该对象是不关联任何线程的。可以在之后的使用过程中再关联到某一线程。可以通过使用 joinable() 接口,判断一个 thread 对象是否关联某个线程。

(0x03) 关联到线程的 thread 对象析构前,必须调用 join() 接口等待线程结束。或者 thread 对象调用 detach() 接口解除与线程的关联,否则会抛异常。

(0x04) thread 对象 detach() 后会独立执行直至结束,而对应的 thread 对象变成不关联任何线程的对象,joinable() 将返回 false。

(0x05) std::thread 没有拷贝构造函数和拷贝赋值操作符,因此不支持复制操作(但从构造函数的示例代码可以看出 std::thread 可以 move )。这也说明了没有两个 thread 对象可以表示同一执行线程。

 

 

std::jthread

它拥有同 std::thread 的行为主要增加了以下两个功能:

(1) std::jthread 对象被 destruct 时,会自动调用 join,等待其所表示的执行流结束。

(2) 支持外部请求中止(通过 get_stop_source、get_stop_token 和 request_stop )。

 

为什么不是选择往 std::thread 添加新接口,而是引入了一个新的标准库?

因为 std::jthread 为了实现上述新功能,带来了额外的性能开销 (主要是多了一个成员变量)。而根据 C++ 一直以来 "不为不使用的功能付费" 的设计哲学,他们自然就把这些新功能拆出来新做了一个类。

 

std::jthread 构造函数

(1)jthread() noexcept;
(2)jthread( jthread&& other ) noexcept;
(3)template< class Function, class... Args > 
   explicit jthread( Function&& f, Args&&... args );
(4)jthread( const jthread& ) = delete;

(1) 构造新 jthread 对象,但由于没有传入函数,所以 jthread 对象还没有关联到线程

(2) 移动构造函数。构造的 jthread 对象表示之前由 other 表示的执行线程。此调用后 other 不再表示执行线程。

(3) 创建与执行线程关联的新 std::jthread 对象。若函数 f 接受 std::stop_token 作为其首参数,则新线程开始执行

(4) 复制构造函数被删除;线程不可复制。

#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
 
void f1(int n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread 1 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
void f2(int& n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread 2 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
class foo
{
public:
    void bar()
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 3 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};
 
class baz
{
public:
    void operator()()
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 4 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};
 
int main()
{
    int n = 0;
    foo f;
    baz b;
    std::jthread t0; // t0 不是线程
    std::jthread t1(f1, n + 1); // 按值传递
    std::jthread t2a(f2, std::ref(n)); // 按引用传递
    std::jthread t2b(std::move(t2a)); // t2b 现在运行 f2() 。 t2a 不再是线程
    std::jthread t3(&foo::bar, &f); // t3 在对象 f 上运行 foo::bar()
    std::jthread t4(b); // t4 在对象 b 上运行 baz::operator()
    t1.join();
    t2b.join();
    t3.join();
    std::cout << "Final value of n is " << n << '\n';
    std::cout << "Final value of foo::n is " << f.n << '\n';
    // t4 在析构时结合
}

 

 

观察器

joinable

[[nodiscard]] bool joinable() const noexcept;
#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::jthread t;
    std::cout << "before starting, joinable: " << std::boolalpha << t.joinable()
              << '\n';
 
    t = std::thread(foo);
    std::cout << "after starting, joinable: " << t.joinable() 
              << '\n';
 
    t.join();
    std::cout << "after joining, joinable: " << t.joinable() 
              << '\n';
}

before starting, joinable: false
after starting, joinable: true
after joining, joinable: false

 

get_id

[[nodiscard]] std::jthread::id get_id() const noexcept;

返回标识与 *this 关联的线程的 std::jthread::id 。

#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::jthread t1(foo);
    std::jthread::id t1_id = t1.get_id();
 
    std::jthread t2(foo);
    std::jthread::id t2_id = t2.get_id();
 
    std::cout << "t1's id: " << t1_id << '\n';
    std::cout << "t2's id: " << t2_id << '\n';
 
 
}

t1's id: 0x35a7210f
t2's id: 0x35a311c4

 

 

操作

join

void join();

阻塞当前线程直至 *this 所标识的线程结束其执行。*this 所标识的线程的完成同步于对应的从 join() 成功返回。*this 自身上不进行同步。同时从多个线程在同一 jthread 对象上调用 join() 构成数据竞争,导致未定义行为。

#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    // 模拟昂贵操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
void bar()
{
    // 模拟昂贵操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::cout << "starting first helper...\n";
    std::jthread helper1(foo);
 
    std::cout << "starting second helper...\n";
    std::jthread helper2(bar);
 
    std::cout << "waiting for helpers to finish..." << std::endl;
    helper1.join();
    helper2.join();
 
    std::cout << "done!\n";
}

starting first helper...
starting second helper...
waiting for helpers to finish...
done!

 

detach

void detach();

从 jthread 对象分离执行线程,允许线程独立地运行。一旦该线程退出,则释放任何分配的资源。调用 detach 后 *this 不再占有任何线程。

#include <iostream>
#include <chrono>
#include <thread>
 
void independentThread() 
{
    std::cout << "Starting concurrent thread.\n";
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Exiting concurrent thread.\n";
}
 
void threadCaller() 
{
    std::cout << "Starting thread caller.\n";
    std::jthread t(independentThread);
    t.detach();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Exiting thread caller.\n";
}
 
int main() 
{
    threadCaller();
    std::this_thread::sleep_for(std::chrono::seconds(5));
}

Starting thread caller.
Starting concurrent thread.
Exiting thread caller.
Exiting concurrent thread.

 

swap

void swap( std::jthread& other ) noexcept;

交换二个 jthread 对象的底层柄。

#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
void bar()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::jthread t1(foo);
    std::jthread t2(bar);
 
    std::cout << "thread 1 id: " << t1.get_id() << '\n'
              << "thread 2 id: " << t2.get_id() << '\n';
 
    std::swap(t1, t2);
 
    std::cout << "after std::swap(t1, t2):" << '\n'
              << "thread 1 id: " << t1.get_id() << '\n'
              << "thread 2 id: " << t2.get_id() << '\n';
 
    t1.swap(t2);
 
    std::cout << "after t1.swap(t2):" << '\n'
              << "thread 1 id: " << t1.get_id() << '\n'
              << "thread 2 id: " << t2.get_id() << '\n';
 
 
}

thread 1 id: 140185268262656
thread 2 id: 140185259869952
after std::swap(t1, t2):
thread 1 id: 140185259869952
thread 2 id: 140185268262656
after t1.swap(t2):
thread 1 id: 140185268262656
thread 2 id: 140185259869952

 

 

停止记号处理

get_stop_source

std::stop_source get_stop_source() const noexcept;

返回 std::stop_source ,拥有与 jthread 对象内部所保有者相同的共享停止状态。

 

get_stop_token

std::stop_token get_stop_token() const noexcept;

返回 std::stop_token ,与 jthread 对象内部保有的同一共享停止状态关联。

 

request_stop

bool request_stop() noexcept;

若内部停止状态尚未被请求停止,则对它发出停止请求。原子地作出确定,而若请求了停止,则原子地更新共享状态以避免竞争条件,使得:能在同一共享状态的 std::stop_token 与 std::stop_source 上同时调用 stop_requested() 与 stop_possible() 能从多个线程在同一 jthread 对象或与同一停止状态关联的其他 std::stop_source 对象上并发调用 request_stop() ,而将只有一个线程实际进行停止请求。

注意:若 request_stop() 发出停止请求(即返回 true ),则将在发出 request_stop() 的同一线程上同步调用对同一共享停止状态注册的任何 std::stop_callbacks 。若任何回调的调用经由异常退出,则调用 std::terminate 。

若已作出停止请求,则此函数返回 false 。然而不保证正好对同一停止状态(成功)请求停止的另一线程或 std::stop_source 对象不仍然在调用 std::stop_callback 函数的中间。

若 request_stop() 发出停止请求(即返回 true ),则提醒所有用与 jthread 的内部停止状态关联的 stop_token 的可中断等待注册的、基类型为 std::condition_variable_any 的条件变量。

 

 

管理当前线程的函数

yield()

void yield() noexcept;

提供提示给实现,以重调度线程的执行,允许其他线程运行。

#include <iostream>
#include <chrono>
#include <thread>
 
// 建议其他线程运行一小段时间的“忙睡眠”
void little_sleep(std::chrono::microseconds us)
{
    auto start = std::chrono::high_resolution_clock::now();
    auto end = start + us;
    do {
        std::this_thread::yield();
    } while (std::chrono::high_resolution_clock::now() < end);
}
 
int main()
{
    auto start = std::chrono::high_resolution_clock::now();
 
    little_sleep(std::chrono::microseconds(100));
 
    auto elapsed = std::chrono::high_resolution_clock::now() - start;
    std::cout << "waited for "
              << std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count()
              << " microseconds\n";
}

waited for 128 microseconds

 

get_id()

std::thread::id get_id() noexcept;
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
 
std::mutex g_display_mutex;
 
void foo()
{
    std::thread::id this_id = std::this_thread::get_id();
 
    g_display_mutex.lock();
    std::cout << "thread " << this_id << " sleeping...\n";
    g_display_mutex.unlock();
 
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::thread t1(foo);
    std::thread t2(foo);
 
    t1.join();
    t2.join();
}

thread 0x2384b312 sleeping...
thread 0x228a10fc sleeping...

 

sleep_for()

template< class Rep, class Period >
void sleep_for( const std::chrono::duration<Rep, Period>& sleep_duration );

阻塞当前线程执行,至少经过指定的 sleep_duration 。此函数可能阻塞长于 sleep_duration ,因为调度或资源争议延迟。标准库建议用稳定时钟度量时长。若实现用系统时间代替,则等待时间亦可能对时钟调节敏感。

#include <iostream>
#include <chrono>
#include <thread>
 
int main()
{
    using namespace std::chrono_literals; // C++14
    std::cout << "Hello waiter" << std::endl; // 有意冲入
    auto start = std::chrono::high_resolution_clock::now();
    std::this_thread::sleep_for(2s);
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double, std::milli> elapsed = end-start;
    std::cout << "Waited " << elapsed.count() << " ms\n";
}

Hello waiter
Waited 2000.12 ms

 

sleep_until()

template< class Clock, class Duration >
void sleep_until( const std::chrono::time_point<Clock,Duration>& sleep_time );

阻塞当前线程,直至抵达指定的 sleep_time 。使用联倾向于 sleep_time 的时钟,这表示时钟调节有影响。从而在调用时间点后,阻塞的时长可能小于,但不会多于 sleep_time - Clock::now() 。函数亦可能阻塞长于抵达 sleep_time 之后,由于调度或资源争议延迟。

 

 

参考:https://zh.cppreference.com/w/cpp/thread

https://www.zhihu.com/question/364140779

https://blog.csdn.net/qq_38289815/article/details/82950320

  • 13
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tyler_Zx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值