thread

2023-12-22 12:12:37

c++ 11中异步操作与锁的简单学习(lock_guard、unique_lock、condition_variable、recursive_mutex)


在多线程操作中,锁用来保证数据的一致性访问,即各个线程有条不紊的使用某些数据,避免同时操作或同时取值导致出现问题。
多线程本来就是为了提高效率和响应速度,但锁的使用又限制了多线程的并行执行,这会降低效率,因此设计合理的锁能保证程序效率。

lock_guard
lock_guard是一个互斥量包装程序,它提供了一种方便的RAII(Resource acquisition is
initialization )风格的机制来在作用域块的持续时间内拥有一个互斥量。

创建lock_guard对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量。

它的特点如下:
1、创建即加锁,作用域结束自动析构并解锁,无需手工解锁
2、不能中途解锁,必须等作用域结束才解锁
3、不能复制

#include <thread>
#include <mutex>
#include <iostream>
?
int g_i = 0;
std::mutex g_i_mutex;?
?
void safe_increment()
{
? ? const std::lock_guard<std::mutex> lock(g_i_mutex);
? ? ++g_i;
? ? std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
? ? // g_i_mutex is automatically released when lock
? ? // goes out of scope
}
?
int main()
{
? ? std::cout << "main: " << g_i << '\n';
? ? std::thread t1(safe_increment);
? ? std::thread t2(safe_increment);
? ? t1.join();
? ? t2.join();
?
? ? std::cout << "main: " << g_i << '\n';
}
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
unique_lock
unique_lock是一个通用的互斥量锁定包装器,它允许延迟锁定,限时深度锁定,递归锁定,锁定所有权的转移以及与条件变量一起使用。

简单地讲,unique_lock 是 lock_guard 的升级加强版,它具有 lock_guard 的所有功能,同时又具有其他很多方法,使用起来更强灵活方便,能够应对更复杂的锁定需要。unique_lock是个类模板,灵活很多,但效率上差一点,内存占用多一点。工作中一般推荐使用lock_guard;lock_guard取代了mutex的lock()和unlock();

unique_lock特点如下:

1、创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
2、可以随时加锁解锁
3、作用域规则同 lock_grard,析构时自动释放锁
4、不可复制,可移动
5、条件变量需要该类型的锁作为参数(此时必须使用unique_lock)

第二个参数:

std::lock_guard<std::mutex> lk1(my_mutex1, std::adopt_lock);// std::adopt_lock标记作用;
std::unique_lock<std::mutex> lk2(my_mutex2, std::adopt_lock);// std::adopt_lock标记作用;
1
2
std::adopt_lock
表示这个互斥量已经被lock了(你必须要把互斥量提前lock了 ,否者会报异常);

std::adopt_lock标记的效果就是假设调用一方已经拥有了互斥量的所有权(已经lock成功了);则通知lock_guard或unique_lock不需要再构造函数中lock这个互斥量了。
用std::adopt_lock的前提是,自己需要先把mutex lock上。

std::try_to_lock
我们会尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,我也会立即返回,并不会阻塞在那里;用这个try_to_lock的前提是你自己不能先lock。

std::defer_lock
std::defer_lock的意思就是并没有给mutex加锁:初始化了一个没有加锁的mutex。
用std::defer_lock的前提是,你不能自己先lock,否则会报异常。

条件变量condition variable
说完unique_lock此时不得不再总结一些相关用法——condition variable

在多线程编程中,当多个线程之间需要进行某些同步机制时,如某个线程的执行需要另一个线程完成后才能进行,可以使用条件变量。

c++11提供的 condition_variable 类是一个同步原语,它能够阻塞一个或者多个线程,直到另一线程修改共享变量并通知 condition_variable。

也可以把它理解为信号通知机制,一个线程负责发送信号,其他线程等待该信号的触发。

condition_variable 存在一些问题,如虚假唤醒,这可以通知增加额外的共享变量来避免。

针对增加额外的变量这一点,为什么不在另一线程循环检测这个变量,从而达到相同目的而不需要再使用条件变量?

循环检测时,程序在高速运行,占用过高的cpu,而条件变量的等待是阻塞,休眠状态下cpu使用率为0,省电!
对于运行中的线程,可能会被操作系统调度,切换cpu核心,这样一来,所有的缓存可能失效,而条件变量不会,省时!
对于只需要通知一次的情况,如初始化完成、登录成功等,建议不要使用 condition_variable,使用std::future更好。

对于共享变量的刷新,发出通知的一方流程如下:
1、可以通过 std::lock_guard来获取 std::mutex;
修改共享变量(即使共享变量是原子变量,也需要在互斥对象内进行修改,以保证正确地将修改发布到等待线程)
在 condition_variable 上执行 notify_one/notify_all 通知条件变量(该操作不需要锁)

对于锁的等待一方,流程如下:
1、先获取 std::mutex, 此时只能使用 std::unique_lock
2、执行 wait,wait_for或wait_until(该操作会自动释放锁并阻塞)。
3、接收到条件变量通知、超时或者发生虚假唤醒时,线程被唤醒,并自动获取锁。唤醒的线程负责检查共享变量,如果是虚假唤醒,则应继续等待。

std :: condition_variable仅适用于 std::unique_lock,此限制允许在某些平台上获得最大效率。

std:: condition_variable_any提供可与任何BasicLockable对象一起使用的条件变量,例如std ::
shared_lock。

使用注意事项:
1、notify_one() 只唤醒一个线程,如果有多个线程,具体唤醒哪一个不确定,如果需要唤醒其他所有线程,使用 notify_all()
2、执行 notify_one() 时不需要锁。
3、修改共享变量 ready/processed 时需要锁,共享变量用于避免虚假唤醒。
4、cv.wait 第一个参数必须是 unique_lock,因为它内部会执行 unlock和lock,如果需要设置超时,使用 wait_for/wait_until。

5、需要共享变量来避免虚假唤醒,接收线程需要判断是否为虚假唤醒。如果不使用共享变量,当通知线程在接收线程准备接收之前发送通知,接收线程将要永远阻塞了。
6、共享变量的修改需要在锁内进行。
7、通知线程在发出通知时不需要加锁。

std::condition_variable 提供了两种 wait() 函数。当前线程调用 wait()后将被阻塞(此时当前线程应该获得了锁(mutex),假设获得了锁 lck),直到另外某个线程调用 notify_one/all唤醒了当前线程。

1、在线程被阻塞时,该函数会自动调用 lck.unlock()释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_one/all 唤醒了当前线程),wait() 函数也是自动调用 lck.lock(),使得 lck 的状态和 wait 函数被调用时相同。

2、某些情况下(即设置了 Predicate),只有当 pred 条件为 false 时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred 为 true 时才会被解除阻塞。

上代码:

#include <iostream> ? ? ? ? ? ? ? ?// std::cout
#include <thread> ? ? ? ? ? ? ? ?// std::thread
#include <mutex> ? ? ? ? ? ? ? ?// std::mutex, std::unique_lock
#include <condition_variable> ? ?// std::condition_variable

std::mutex mtx; // 全局互斥锁.
std::condition_variable cv; // 全局条件变量.
bool ready = false; // 全局标志位.

void do_print_id(int id)
{
? ? std::unique_lock <std::mutex> lck(mtx);
? ? while (!ready) // 如果标志位不为 true, 则等待...
? ? ? ? cv.wait(lck); // 当前线程被阻塞, 当全局标志位变为 true 之后,
? ? // 线程被唤醒, 继续往下执行打印线程编号id.
? ? std::cout << "thread " << id << '\n';
}

void go()
{
? ? std::unique_lock <std::mutex> lck(mtx);
? ? ready = true; // 设置全局标志位为 true.
? ? cv.notify_all(); // 唤醒所有线程.
}

int main()
{
? ? std::thread threads[10];
? ? // spawn 10 threads:
? ? for (int i = 0; i < 10; ++i)
? ? ? ? threads[i] = std::thread(do_print_id, i);

? ? std::cout << "10 threads ready to race...\n";
? ? go(); // go!
? ??
? ?? ?for (auto & th:threads)
? ? ?? ?th.join();
? ? ? ??
? ? return 0;
}

wait()另一种情况:

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

(1) std::this_thread::yield(); 是将当前线程所抢到的CPU”时间片A”让渡给其他线程(其他线程会争抢”时间片A”,
注意: 此时”当前线程”不参与争抢).
等到其他线程使用完”时间片A”后, 再由操作系统调度, 当前线程再和其他线程一起开始抢CPU时间片.

(2) 如果将 std::this_thread::yield();上述语句修改为: return; ,则将未使用完的CPU”时间片A”还给操作系统, 再由操作系统调度, 当前线程和其他线程一起开始抢CPU时间片.

std::this_thread::yield(): 适合应用在 — “that should be used in a case where you are in a busy waiting state”, 如果存在此情况, 线程实际的执行代码类似如下:
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

std::this_thread::yield() 的目的是避免一个线程(that should be used in a case where you are in a busy waiting state)频繁与其他线程争抢CPU时间片, 从而导致多线程处理性能下降.

std::this_thread::yield() 是让当前线程让渡出自己的CPU时间片(给其他线程使用)
std::this_thread::sleep_for() 是让当前休眠”指定的一段”时间.

sleep_for()也可以起到 std::this_thread::yield()相似的作用, (即:当前线程在休眠期间, 自然不会与其他线程争抢CPU时间片)但两者的使用目的是大不相同的:
std::this_thread::yield() 是让线程让渡出自己的CPU时间片(给其他线程使用)
sleep_for() 是线程根据某种需要, 需要等待若干时间.
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
std::thread::yield : 通常来说,用在线程中,等待一个状态的完成,而且不卡顿。
std::thread::yield : 通常来说,用在线程中,等待一个状态的完成,而且不卡顿。
std::thread::yield : 通常来说,用在线程中,等待一个状态的完成,而且不卡顿。

部分代码如下:

std::atomic<int> data(0);

void SetData(int x)
{
????std::this_thread::sleep_for(std::chrono::seconds(3));
????std::cout << "SetData: " << x << std::endl;
????data = x;
}

void Print_Data( )
{
????while (data == 0)
????{
????????std::this_thread::yield();
????}
????std::cout << "x: " << data << std::endl;
}

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

#include <iostream> ? ? ? ? ? ? ? ?// std::cout
#include <thread> ? ? ? ? ? ? ? ?// std::thread, std::this_thread::yield
#include <mutex> ? ? ? ? ? ? ? ?// std::mutex, std::unique_lock
#include <condition_variable> ? ?// std::condition_variable

std::mutex mtx;
std::condition_variable cv;
int cargo = 0;
bool shipment_available()
{
? ? return cargo != 0;
}
void consume(int n)
{
? ? for (int i = 0; i < n; ++i) {
? ? ? ? std::unique_lock <std::mutex> lck(mtx);
? ? ? ? cv.wait(lck, shipment_available);
? ? ? ? std::cout << cargo << '\n';
? ? ? ? cargo = 0;
? ? }
}
int main()
{
? ? std::thread consumer_thread(consume, 10);
? ? for (int i = 0; i < 10; ++i) {
? ? ? ? while (shipment_available())
? ? ? ? ? ? std::this_thread::yield();
? ? ? ? std::unique_lock <std::mutex> lck(mtx);
? ? ? ? cargo = i + 1;
? ? ? ? cv.notify_one();
? ? }

? ? consumer_thread.join();
? ? return 0;
}

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
基于多线程并发-STL之递归锁recursive_mutex

大胡子的艾娃

已于 2023-05-12 17:33:18 修改

阅读量140
?收藏 1

点赞数
分类专栏: 并发编程 文章标签: C++ recursive_mutex 递归 锁 标准库
版权

并发编程
专栏收录该内容
19 篇文章1 订阅
订阅专栏
一:说明
1、recursive_mutex 类是同步原语,能用于保护共享数据免受从个多线程同时访问。
2、recursive_mutex 提供排他性递归所有权语义:
3、recursive_mutex 的使用场景之一是保护类中的共享状态,而类的成员函数可能相互调用

二:用法:
recursive_mutex的用处和mutex差不多,用于限制多线程同时访问同一个变量,用来加锁,保证多个线程,同一时刻只能有一个线程在修改变量;和mutex不同的时,recursive_mutex可以允许同一个线程递归的去加锁,线程只有加锁次数和释放次数相同时,才会释放变量的控制权;例如下面的fun2中调用了fun1,但是fun1和fun2中都加了锁,如果使用mutex,在fun1加锁,在fun2中再次加锁,就会造成死锁;所以recursive_mutex可以避免递归嵌套调用时,造成的死锁问题;递归调用不会死锁,同一线程使用recursive_mutex加锁次数和解锁次数相等时释放控制权;

#include <iostream>
#include <thread>
#include <mutex>

class X {
? ? std::recursive_mutex m;
? ? std::string shared;
public:
? ? void fun1() {
? ? ? ? std::lock_guard<std::recursive_mutex> lk(m);
? ? ? ? shared = "fun1";
? ? ? ? std::cout << "in fun1, shared variable is now " << shared << '\n';
? ? }
? ? void fun2() {
? ? ? ? std::lock_guard<std::recursive_mutex> lk(m);
? ? ? ? shared = "fun2";
? ? ? ? std::cout << "in fun2, shared variable is now " << shared << '\n';
? ? ? ? fun1(); // 递归锁在此处变得有用
? ? ? ? std::cout << "back in fun2, shared variable is " << shared << '\n';
? ? };
};

int main()
{
? ? X x;
? ? std::thread t1(&X::fun1, &x);
? ? std::thread t2(&X::fun2, &x);
? ? t1.join();
? ? t2.join();
}
————————————————
版权声明:本文为CSDN博主「大胡子的艾娃」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43148810/article/details/130645778
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

最后说说recursive_mutex
recursive_mutex 类是同步原语,能用于保护共享数据免受从个多线程同时访问。

recursive_mutex 提供排他性递归所有权语义:

调用方线程在从它成功调用 lock 或 try_lock 开始的时期里占有 recursive_mutex 。此时期间,调用方线程可以多次锁定/解锁互斥元。结束的时候lock与unlock次数匹配正确就行。

线程占有 recursive_mutex 时,若其他所有线程试图要求 recursive_mutex 的所有权,则它们将阻塞(对于调用 lock )或收到 false 返回值(对于调用 try_lock )。

可锁定 recursive_mutex 次数的最大值是未指定的,但抵达该数后,对 lock 的调用将抛出 std::system_error 而对 try_lock 的调用将返回 false 。

若 recursive_mutex 在仍为某线程占有时被销毁,则程序行为未定义。 recursive_mutex 类满足互斥体 (Mutex) 和标准布局类型 (StandardLayoutType) 的所有要求。

recursive_mutex与std::lock_guard一起使用有时也能达到某些场合的效果:

#include <iostream>
#include <thread>
#include <mutex>
?
class X {
? ? std::recursive_mutex m;
? ? std::string shared;
? public:
? ? void fun1() {
? ? ? std::lock_guard<std::recursive_mutex> lk(m);
? ? ? shared = "fun1";
? ? ? std::cout << "in fun1, shared variable is now " << shared << '\n';
? ? }
? ? void fun2() {
? ? ? std::lock_guard<std::recursive_mutex> lk(m);
? ? ? shared = "fun2";
? ? ? std::cout << "in fun2, shared variable is now " << shared << '\n';
? ? ? fun1(); // ① 递归锁在此处变得有用
? ? ? std::cout << "back in fun2, shared variable is " << shared << '\n';
? ? };
};
?
int main()?
{
? ? X x;
? ? std::thread t1(&X::fun1, &x);
? ? std::thread t2(&X::fun2, &x);
? ? t1.join();
? ? t2.join();
}

共勉。
结果
结果
结果
in fun1,shared variable in now fun1
in fun2,shared variable in now fun2
in fun1,shared variable in now fun1
back in fun2, shared variable is fun1

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
7.std::async
我们已经有多线程thread了,为什么还要有async?
线程毕竟是属于比较低层次的东西,有时候使用有些不便,比如我希望获取线程函数的返回结果的时候,我就不能直接通过 thread.join()得到结果,这时就必须定义一个变量,在线程函数中去给这个变量赋值,然后join,最后得到结果,这个过程是比较繁琐的。
c++11还提供了异步接口std::async,通过这个异步接口可以很方便的获取线程函数的执行结果。std::async会自动创建一个线程去调用 线程函数,它返回一个std::future,这个future中存储了线程函数返回的结果,当我们需要线程函数的结果时,直接从future中获取非常方便。
std::async是更高层次上的异步操作,使我们不用关注线程创建内部细节,就能方便的获取异步执行状态和结果,还可以指定线程创建策略,std::async是为了 让用户的少费点脑子的,它让这三个对象默契的工作。大概的工作过程是这样的:std::async先将异步操作用std::packaged_task包装起来,然后将异步操作的结果放到std::promise中,
7.1 理解
1. std::async是用来创建异步任务的。

2. std::async有两个参数:std::launch::deferred和 std::launch::async

7.2 异同
1,std::async()与std::thread()最明显的不同,就是async并不一定创建新的线程

2,std::thread() 如果系统资源紧张,那么可能创建线程失败,整个程序可能崩溃。

3,std::thread()创建线程的方式,如果线程返回值,你想拿到这个值也不容易;

4,std::async()创建异步任务,可能创建也可能不创建线程;并且async调用方式很容易拿到线程入口函数的返回值。

7.3 参数
1,参数std::launch::deferred 延迟调用;参数std::launch::async 强制创建一个新线程

2,如果你用std::launch::deferred来调用async会怎么样?std::launch::deferred延迟调用,延迟到future对象调用get()或者wait()的时候才执行mythread();如果不调用get()或者wait(),mythread()不会执行。

3,std::launch::async:强制这个异步任务在新线程上执行,这意味着,系统必须要给我创建出新线程来运行mythread();

4,std::launch::async |std::launch::deferred 这里这个 |:以为这调用async的行为可能是 创建新线程并立即执行,或者没有创建新线程并且延迟调用result.get()才开始执行任务入口函数,两者居其一。

5,不带额外参数;只给一个入口函数名;默认是应该是std::launch::async |std::launch::deferred,和c)效果一致,换句话说,系统会自行决定是异步(创建新线程)还是同步(不创建新线程)方式运行

7.4 注意
由于系统资源限制:

(1)如果使用std::thread()创建的线程太多,则可能创建线程失败,系统报告异常,崩溃;
(2)如果用std::async,一般就不会报异常崩溃,因为如果系统资源紧张导致无法创建新线程的时候,std::async这种不加额外参数的调用就不会创建新线程,而是后续谁调用了future::get()来请求结果,那么这个异步任务就运行在执行这条get()语句所在的线程上。
(3)如果你强制std::async创建新线程,那么就必须使用std::launch::async,承受的代价就是系统资源紧张时,可能程序崩溃。经验:一个程序里,线程的数量不易超过100-200,与时间片有关,详情参考操作系统。
7.5 async不确定性问题的解决
不加额外参数的std::async调用问题,让系统自行决定是否创建新的线程。

问题的焦点在于 std::future<int> result = std::async(mythread)写法,这个异步任务到底 有没有被推迟执行。

解决代码:

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>
#include<future>
using namespace std;
?
int mythread() //线程入口函数
{
?? ?cout << "mythread start" << "threadid= " << std::this_thread::get_id() << endl; //打印线程id
?
?? ?std::chrono::milliseconds dura(5000); //定一个5秒的时间
?? ?std::this_thread::sleep_for(dura); ?//休息一定时常
?
?? ?cout << "mythread end" << "threadid= " << std::this_thread::get_id() << endl; //打印线程id
?
?? ?return 5;
}
int main()
{
?? ?cout << "main" << "threadid= " << std::this_thread::get_id() << endl;
?? ?std::future<int> result = std::async(mythread);//流程并不卡在这里
?? ?cout << "continue....." << endl;
?
?? ?//枚举类型
?? ?std::future_status status = result.wait_for(std::chrono::seconds(0));//等待一秒
?? ?
?? ?if (status == std::future_status::deferred)
?? ?{
?? ??? ?//线程被延迟执行了,系统资源紧张
?? ??? ?cout << result.get() << endl; //此时采取调用mythread()
?? ?}
?? ?else if (status == std::future_status::timeout)//
?? ?{
?? ??? ?//超时:表示线程还没执行完;我想等待你1秒,希望你返回,你没有返回,那么 status = timeout
?? ??? ?//线程还没执行完
?? ??? ?cout << "超时:表示线程还没执行完!" << endl;
?? ?}
?? ?else if (status == std::future_status::ready)
?? ?{
?? ??? ?//表示线程成功返回
?? ??? ?cout << "线程成功执行完毕,返回!" << endl;
?? ??? ?cout << result.get() << endl;
?? ?}
?
?? ?cout << "I love China!" << endl;
?? ?return 0;
}
7.6使用
// C++ Standard: C++17
#include <iostream>
#include <thread>
#include <future>
using namespace std;
int main() {
?? ?async(launch::async, [](const char *message){
?? ??? ?cout << message << flush;
?? ?}, "Hello, ");
?? ?cout << "World!" << endl;
?? ?return 0;
}

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
启动线程
线程在 std::thread 对象创建(为线程指定任务)时启动。最简单的情况下,任务也会很简单,通常是无参数无返回(void-returning)的函数。这种函数在其所属线程上运行,直到函数执行完毕,线程也就结束了。在一些极端情况下,线程运行时,任务中的函数对象需要通过某种通讯机制进行参数的传递,或者执行一系列独立操作;可以通过通讯机制传递信号,让线程停止。线程要做什么,以及什么时候启动,其实都无关紧要。总之,使用C++线程库启动线程,可以归结为构造 std::thread 对象:

? ? void do_some_work();
? ? std::thread my_thread(do_some_work);
如同大多数C++标准库一样, std::thread 可以用可调用(callable)类型构造,将带有函数调用符类型的实例传入 std::thread 类中,替换默认的构造函数。

启动了线程,你需要明确是要等待线程结束,还是让其自主运行。如果 std::thread 对象销毁之前还没有做出决定,程序就会终止( std::thread 的析构函数会调用 std::terminate() )。因此,即便是有异常存在,也需要确保线程能够正确的加入(joined)或分离(detached)。需要注意的是,必须在 std::thread 对象销毁之前做出决定——加入或分离线程。
如果不等待线程,就必须保证线程结束之前,可访问的数据得有效性。这不是一个新问题——单线程代码中,对象销毁之后再去访问,也会产生未定义行为——不过,线程的生命周期增加了这个问题发生的几率。
创建线程的几种方式:

全局函数
? ? std::thread tFunction(background_func);
? ? tFunction.join();
函数对象
? ? background_task task;
? ? std::thread tFunctionObject(task);
成员函数

? ? background_wraper oBackgroundWraper;
? ? std::thread tMemberFunction(std::bind(&background_wraper::background, &oBackgroundWraper));
静态成员函数
std::thread tStaticMemberFunction(background_wraper ::staticBackground);
Lambda表达式
? ? std::thread tLambda([]()
? ? {
? ? ? ? cout << "lambda" << endl
? ? ? ? ? ? << "子线程ID为:" << this_thread::get_id() << endl << endl;
? ? });

作者:龙翱天际
链接:https://www.jianshu.com/p/64136806f171
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
void ThreadPool::threadFunction() {
? ? while (true) {
? ? ? ? Task task;
? ? ? ? {
? ? ? ? ? ? unique_lock<mutex> lock(queueMutex);
? ? ? ? ? ? condition.wait(lock, [this]() { return !taskQueue.empty() || terminate; });

? ? ? ? ? ? if (terminate && taskQueue.empty()) {
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }

? ? ? ? ? ? task = taskQueue.front();
? ? ? ? ? ? taskQueue.pop();
? ? ? ? }
? ? ? ? task(); // Execute the task.
? ? }
}

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""


>
C/C++开发基础——原子操作与多线程编程
C/C++开发基础——原子操作与多线程编程
发布于 2023-11-13 14:02:09
2160
举报
一,线程的创建与终止

线程是CPU最小的执行和调度单位。多个线程共享进程的资源。

创建线程比创建进程更快,开销更小。

创建线程的方法:pthread_create、std::thread。

pthread_create:传入的线程函数只有一个参数。

std::thread:传入的线程函数可以有任意数量的参数。

因为,thread类的构造函数是一个可变参数模板,可接收任意数目的参数,其中第一个参数是线程对应的函数名称。

std::thread调用以后返回一个线程类,每创建一个线程类,就会在系统中启动一个线程,并利用这个线程类来管理线程。

线程类可以被移动,但是不可以被复制,可以调用move()来改变线程的所有权。

线程的标识符是线程id,线程类可以调用this_thread::get_id()来获得当前线程的id。

创建线程以后,可以调用join()或者detach()来等待线程结束,join()会等启动的线程运行结束以后再继续执行当前代码,detach()会直接往后继续执行当前代码,而不需要等待启动的线程运行结束。如果调用detach()分离线程,该线程结束后,线程资源会自动被系统回收。

std::thread常用的创建线程类的方式有:

通过函数指针创建线程

通过函数对象创建线程

通过lambda表达式创建线程

通过成员函数创建线程

1.通过函数指针创建线程

代码样例:

函数

void counter(int id, int numIterations)
{
? ? for(int i=0; i<numIterations; ++i){
? ? ? ? cout << "Counter " << id << " has value " << i << endl;
? ? }
}
复制
利用函数创建线程:

thread t1(counter, 1, 6);
thread t2(counter, 2, 4);
t1.join();
t2.join();
复制
注意,线程中的函数,比如counter(),在创建线程的时候,默认的传参方式是值拷贝,比如id,numIterations会被拷贝以后再传递到线程空间中。

2.通过函数对象创建线程

代码样例:

函数对象Counter:

class Counter
{
? ? public:
? ? Counter(int id, int numIterations)
? ? :mId(id), mNumIterations(numIterations)
? ? {


? ? }

? ? //重载运算符operator()
? ? void operator()() const
? ? {
? ? ? ? for(int i=0; i < mNumIterations; ++i){
? ? ? ? ? ? cout << "Counter " << mId << " has value " << i << endl;
? ? ? ? }
? ? }

? ? private:
? ? ? ? int mId;
? ? ? ? int mNumIterations;
};
复制
利用函数对象创建线程:

方法1:通过构造函数创建Counter类的一个实例,将实例传递给thread类

thread t1{Counter{1, 4}};
复制
方法2:创建Counter类的一个实例c,将实例传递给thread类

Counter c(2, 5);
thread t2(c);
复制
完整代码实现:?

#include <thread>
#include <iostream>

using namespace std;

class Counter
{
? ? public:
? ? Counter(int id, int numIterations)
? ? :mId(id), mNumIterations(numIterations)
? ? {

? ? }

? ? //重载运算符operator()
? ? void operator()() const
? ? {
? ? ? ? for(int i=0; i < mNumIterations; ++i){
? ? ? ? ? ? cout << "Counter " << mId << " has value " << i << endl;
? ? ? ? }
? ? }

? ? private:
? ? ? ? int mId;
? ? ? ? int mNumIterations;
};

int main(){
? ? thread t1{Counter{1, 4}};
? ? Counter c(2, 5);
? ? thread t2(c);

? ? t1.join();
? ? t2.join();

? ? cout << "Main thread end." << endl;
? ? return 0;
}
复制
运行结果:

Counter 1 has value 0
Counter 1 has value 1
Counter 1 has value 2
Counter 1 has value 3
Counter 2 has value 0
Counter 2 has value 1
Counter 2 has value 2
Counter 2 has value 3
Counter 2 has value 4
Main thread end.
复制
3.通过lambda表达式创建线程

代码样例:

#include <thread>
#include <iostream>

using namespace std;

int main(){
? ? int id = 1;
? ? int numIterations = 5;
? ? thread t1(
? ? ? ? [id, numIterations]{
? ? ? ? ? ? for(int i=0; i<numIterations; ++i){
? ? ? ? ? ? ? ? cout << "Counter " << id << " has value " << i << endl;
? ? ? ? ? ? }
? ? ? ? }
? ? );

? ? t1.join();
? ? return 0;
}
复制
运行结果:

Counter 1 has value 0
Counter 1 has value 1
Counter 1 has value 2
Counter 1 has value 3
Counter 1 has value 4
复制
4.通过成员函数创建线程

代码样例:

在线程中指定要执行该类的哪个成员函数。

#include <thread>
#include <iostream>

using namespace std;

class Request
{
? ? public:
? ? ? ? Request(int id): mId(id){ }

? ? ? ? void process()
? ? ? ? {
? ? ? ? ? ? cout << "Processing request." << mId << endl;
? ? ? ? }
? ? private:
? ? ? ? int mId;
};

int main(){
? ? Request req_obj(100);
? ? thread t{ &Request::process, &req_obj };

? ? t.join();
? ? return 0;
}
复制
运行结果:

Processing request.100
复制
我们也可用采用RAII写法,封装一个新的线程类,在线程类析构的时候自动调用join()来等待线程执行结束,写法如下:

class RaiiThread {
private:
? ? std::thread& t;
public:
? ? RaiiThread(std::thread& _t ) : t(_t) {}

? ? ~RaiiThread() {
? ? ? ? if(t.joinable())
? ? ? ? ? ? t.join();
? ? } ? ? ? ?

? ? //线程类不能被拷贝
? ? RaiiThread(const RaiiThread &)= delete;
? ? RaiiThread& operator=(const RaiiThread &)= delete ;
};
复制
5.线程的终止

线程终止的方式有:

1.线程函数运行完返回,该子线程终止。

2.同一进程中的其他线程调用pthread_cancel()取消该线程,该子线程终止。

3.线程函数中调用pthread_exit()主动退出,该子线程终止。

4.主线程(main函数中)退出,所有子线程全部被终止。

5.子线程调用exit()函数,整个进程被终止。

二,thread_local变量

thread_local关键字可以实现线程的本地存储。

thread_local变量在多线程中只初始化一次,而且每个线程都有这个变量的独立副本, 每个线程都可以独立访问和修改自己的变量副本,而不会干扰其他线程。

thread_local变量的生命周期从初始化时开始,到线程运行完毕时结束。

int m; ?//所有线程共享m
thread_local int n; //每个线程都有自己的n副本
复制
代码样例:

#include <iostream>
#include <thread>
using namespace std;
void thread_func()
{
? ? ? ?thread_local int stls_variable = 0;
? ? ? ?stls_variable += 1;
? ? ? ?cout << "Thread ID: " << this_thread::get_id()
? ? ? ? ? ? ? << ", Variable: " << stls_variable
? ? ? ? ? ? ? << endl;
}
int main()
{
? ? ? ?thread t1(thread_func);
? ? ? ?thread t2(thread_func);
? ? ? ?t1.join();
? ? ? ?t2.join();
? ? ? ?return 0;
}
复制
运行结果:

Thread ID: 16312, Variable: 1
Thread ID: 14848, Variable: 1
复制
三,原子类型与原子操作

1.原子操作与数据安全

对于一个变量,编译器首先将值从内存加载到寄存器中,在寄存器中进行处理,然后再把结果保存回内存。由于多个线程共享进程中的内存空间,因此,这段内存可以被多个线程同时访问,导致数据争用。原子操作可以解决数据争用问题,保证数据安全。

如果对一个共享内存资源的操作是原子操作,当多个线程访问该共享资源时,在同一时刻,有且仅有一个线程可以对这个资源进行操作。

实现原子操作的方式:

1,使用互斥锁等同步机制

2,使用原子类型

2.常见的原子类型

图源自《深入理解C++11》


除了使用内置类型,开发者可以通过atomic类模板来自定义原子类型。

例如,定义一个T类型的原子类型变量t

std::atomic<T> t;
复制
3.代码样例

使用原子类型之前的多线程代码:

#include <atomic>

using namespace std;

void increment(int& counter)
{
? ? for (int i = 0; i < 100; ++i){
? ? ? ? ++counter;
? ? ? ? this_thread::sleep_for(1ms);
? ? }
}

int main()
{
? ? int counter = 0;
? ? vector<thread> threads;
? ? for(int i = 0; i < 10; ++i){
? ? ? ? threads.push_back(thread{ increment, ref(counter) });
? ? }

? ? for (auto& t : threads){
? ? ? ? t.join();
? ? }

? ? cout << "Result = " << counter << endl;
}
复制
使用原子类型之后的多线程代码:

#include <atomic>

using namespace std;

void increment(atomic<int>& counter)
{
? ? for(int i=0; i<100; ++i){
? ? ? ? ++counter;
? ? ? ? this_thread::sleep_for(1ms);
? ? }
}

int main()
{
? ? atomic<int> counter(0);
? ? vector<thread> threads;
? ? for(int i = 0; i < 10; ++i){
? ? ? ? threads.push_back(thread{increment, ref(counter)});
? ? }

? ? for (auto& t : threads){
? ? ? ? t.join();
? ? }
? ? cout << "Result = " << counter << endl;
}
复制
原子类型可以省去在线程的函数中进行加锁和解锁的操作,下面两段代码实现的效果一样:

Demo1:

#include <thread>
#include <iostream>

using namespace std;

static long total = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;

void* func(void *){
? ? long i;
? ? for (i = 0; i < 999; i++)
? ? {
? ? ? ? pthread_mutex_lock(&m);
? ? ? ? total += 1;
? ? ? ? pthread_mutex_unlock(&m);
? ? }
}

int main(){
? ? pthread_t thread1, thread2;
? ? if (pthread_create(&thread1, NULL, &func, NULL)){
? ? ? ? throw;
? ? }
? ? if (pthread_create(&thread2, NULL, &func, NULL)){
? ? ? ? throw;
? ? }

? ? pthread_join(thread1, NULL);
? ? pthread_join(thread2, NULL);

? ? cout << total << endl;
? ? return 0;
}
复制
运行结果:

1998
复制
Demo2:

#include <thread>
#include <iostream>
#include <atomic>

using namespace std;

atomic_long total {0}; ?//原子数据类型

void* func(void *){
? ? long i;
? ? for (i = 0; i < 999; i++)
? ? {
? ? ? ? total += 1;
? ? }
}

int main(){
? ? pthread_t thread1, thread2;
? ? if (pthread_create(&thread1, NULL, &func, NULL)){
? ? ? ? throw;
? ? }
? ? if (pthread_create(&thread2, NULL, &func, NULL)){
? ? ? ? throw;
? ? }

? ? pthread_join(thread1, NULL);
? ? pthread_join(thread2, NULL);

? ? cout << total << endl;
? ? return 0;
}
复制
运行结果:

1998
复制
四,互斥体与锁

对于数组类型或者布尔类型等简单的数据类型可以使用原子操作来同步,如果当数据类型变得很复杂的时候,需要采用显式的同步机制来保证线程之间的同步,常用的同步机制有互斥体类和锁类。

锁类的对象可以用来管理互斥体类的对象,比如unique_lock对象可以管理mutex对象。

互斥体的主要操作是加锁(lock)和解锁(unlock)。

互斥体还分定时互斥体和非定时互斥体。

1.非定时互斥体

头文件 :<mutex>

互斥体名:std::mutex、std::recursive_mutex

头文件:<shared_mutex>

互斥体名:std::shared_mutex

std::mutex互斥体的常用方法:

lock():调用该方法的线程将尝试获取锁,获取不到锁就会一直阻塞。

try_lock():调用该方法的线程将尝试获取锁,获取不到锁就会立即返回,获得锁时返回true,未获得锁时返回false。

unlock():释放由该线程持有的锁。

shared_mutex类功能和读写锁类似,写文件的时候加锁,然后独占所有权,读文件的时候加锁,但是会和其他线程共享所有权。

shared_mutex类除了支持lock()、try_lock()、unlock()等方法获取和释放锁,还支持lock_shared()、try_lock_shared()、unlock_shared()等方法获取和释放共享所有权。

注意:已经获取到锁的线程不能再次调用lock()和try_lock(),否则可能导致死锁。

2.定时互斥体

头文件:<mutex>

互斥体名:std::timed_mutex ? ?

头文件:<shared_mutex>

互斥体名:std::shared_timed_mutex

shared_timed_mutex类除了支持lock()、try_lock()、unlock()等方法获取和释放锁,还支持lock_shared()、try_lock_shared()、unlock_shared()等方法获取和释放共享所有权。

std::timed_mutex定时互斥体还支持以下方法:?

try_lock_for():调用该方法的线程在给定时间间隔内尝试获取锁,在超时之前获取锁失败,返回false,在超时之前获取锁成功,返回true。

try_lock_until():调用该方法的线程在到达指定时间点之前尝试获取锁,在超时之前获取锁失败,返回false,在超时之前获取锁成功,返回true。

3.互斥锁

锁类是RAII写法,不需要手动释放和获取锁,比如lock_guard锁的构造函数里调用了锁的lock成员函数,析构函数里调用了锁的unlock成员函数。

因此,在生命周期结束或离开作用域时,锁类的析构函数会自动释放所关联的互斥体等资源。不需要手动调用unlock()方法,这样可以避免使用锁的时候出现问题,还可以防止死锁的发生。

锁类在标准库中都是模板类,用来生成锁的对象。

C++中常用的锁类:

头文件:<mutex>

常见的锁类:

std::lock_guard

std::unique_lock

std::scoped_lock?

std::scoped_lock与std::lock_guard类似,它接收数量可变的互斥体,可以获取多个锁。

std::lock_guard比较轻量级,执行速度比std::unique_lock更快,但是std::unique_lock用法更灵活。std::lock_guard创建对象即加锁,不能显式的调用lock()和unlock(),而std::unique_lock可以在任意时候调用它们。

std::unique_lock类包含以下方法:

lock():加锁。

unlock():解锁。

try_lock():尝试获取锁,获取失败返回false,获取成功返回true。

try_lock_for():尝试在指定时间段内获取锁,获取失败返回false,获取成功返回true。

try_lock_until():尝试在指定时间点之前获取锁,获取失败返回false,获取成功返回true。

release():释放所有权。

4.代码样例

#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
int main()
{
? ? int counter = 0;
? ? std::mutex counter_mutex;
? ? std::vector<std::thread> threads;
? ? auto worker_task = [&](int id){
? ? ? ? std::unique_lock<std::mutex> lock(counter_mutex);
? ? ? ? ++counter;
? ? ? ? std::cout << id << ", initial counter: " << counter << '\n';
? ? ? ? lock.unlock();
? ? ? ? std::this_thread::sleep_for(std::chrono::seconds(1));
? ? ? ? lock.lock();
? ? ? ? ++counter;
? ? ? ? std::cout << id << ", final counter: " << counter << '\n';
? ? };
? ? for (int i = 0; i < 5; ++i)
? ? {
? ? ? ? threads.emplace_back(worker_task, i);
? ? }

? ? for (auto& thread : threads)
? ? {
? ? ? ? thread.join();
? ? }
? ? return 0; ?
}
复制
运行结果:

0, initial counter: 1
1, initial counter: 2
2, initial counter: 3
3, initial counter: 4
4, initial counter: 5
0, final counter: 6
3, final counter: 7
1, final counter: 8
2, final counter: 9
4, final counter: 10
复制
五,条件变量

条件变量的使用在一定程度上可以避免线程的死锁。

条件变量可以让线程一直阻塞,直到某个条件成立,这个条件可以是另一个线程完成了某操作或者系统时间达到了指定时间。

条件变量允许显式的线程间通信。

条件变量所在的头文件:<condition_variable>

常用的两个条件变量:

std::condition_variable:只能等待unique_lock<mutex>上的条件变量。

std::condition_variable_any:可等待任何对象的条件变量,包括自定义的锁类型,自定义的锁类应提供lock()和unlock()方法。

两种条件变量都支持以下常用的方法:

notify_one():唤醒等待这个条件变量的线程之一。

notify_all():唤醒等待这个条件变量的所有线程。

wait():阻塞当前线程,直到条件变量被唤醒。

wait_for():阻塞当前线程,直到条件变量被唤醒,或到达指定时长。

wait_until():阻塞当前线程,直到条件变量被唤醒,或到达指定时间点。

六,多线程代码实战——线程安全的队列

1.具体设计

1.使用互斥锁来保护共享资源,这里的共享资源是队列。

2.互斥锁在push或者pop队列的时候加锁,在执行完毕后解锁。

3.使用条件变量来等待队列的更改。

4.当数据元素被添加到队列中时,条件变量会notify正在等待的线程,等待队列被更改的线程被唤醒并开始操作。

5.线程从队列中删除数据元素时,会先检查队列是否为空,如果为空,它会等待条件变量,直到有新元素被添加到队列中。

2.代码实现

#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>

template <typename T>
class TSQueue {
private:
? ? std::queue<T> m_queue;

? ? // mutex for thread synchronization
? ? std::mutex m_mutex;

? ? // Condition variable for signaling
? ? std::condition_variable m_cond;

public:
? ? void push(T item)
? ? {
? ? ? ? // acquire lock
? ? ? ? std::unique_lock<std::mutex> lock(m_mutex);

? ? ? ? m_queue.push(item);

? ? ? ? // Notify one thread that is waiting
? ? ? ? m_cond.notify_one();
? ? }

? ? T pop()
? ? {
? ? ? ? // acquire lock
? ? ? ? std::unique_lock<std::mutex> lock(m_mutex);

? ? ? ? // wait until queue is not empty
? ? ? ? m_cond.wait(lock,
? ? ? ? ? ? [this]() { return !m_queue.empty(); });

? ? ? ? T item = m_queue.front();
? ? ? ? m_queue.pop();

? ? ? ? return item;
? ? }
};

int main()
{
? ? TSQueue<int> q;

? ? // Push some data
? ? q.push(30);
? ? q.push(40);
? ? q.push(50);

? ? // Pop some data
? ? std::cout << q.pop() << std::endl;
? ? std::cout << q.pop() << std::endl;
? ? std::cout << q.pop() << std::endl;

? ? return 0;
}
复制
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
atomic<size_t> threadcount
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#include <iostream>
#include <ctime>
#include <vector>
#include <thread>
#include <atomic>
?
?
std::atomic<size_t> count(0);
?
void threadFun()
{
?? ?for (int i = 0; i < 10000; i++)
?? ??? ?count++;
}
?
int main(void)
{
?? ?clock_t start_time = clock();
?
?? ?// 启动多个线程
?? ?std::vector<std::thread> threads;
?? ?for (int i = 0; i < 10; i++)
?? ??? ?threads.push_back(std::thread(threadFun));
?? ?for (auto&thad : threads)
?? ??? ?thad.join();
?
?? ?// 检测count是否正确 10000*10 = 100000
?? ?std::cout << "count number:" << count << std::endl;
?
?? ?clock_t end_time = clock();
?? ?std::cout << "耗时:" << end_time - start_time << "ms" << std::endl;
?
?? ?return 0;
}

BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
?ThreadPool(int numThreads) : threads(numThreads) {
? ? ? ? // 创建numThreads个线程
? ? ? ? for (int i = 0; i < numThreads; ++i) {
? ? ? ? ? ? threadIds.emplace_back(0); // 先占个位
? ? ? ? ? ? threads[i] = std::thread([this, i]() {
? ? ? ? ? ? ? ? threadIds[i] = std::this_thread::get_id(); // 保存线程Id
? ? ? ? ? ? ? ? while (true) {
? ? ? ? ? ? ? ? ? ? // 线程池任务处理逻辑
? ? ? ? ? ? ? ? }
? ? ? ? ? ? });
? ? ? ? }
? ? }
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
#include <iostream>
#include <thread>

using namespace ?std;

void doit(){cout << "world !" << endl;}
int main(){
? ? // flush函数:刷新缓冲区。
? ? // endl函数:终止一行并刷新缓冲区。
? ? thread a([]{cout << "hello," << flush;}), b(doit);
? ? a.join();
? ? b.join();
? ? return 0;
}

dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd

? ? // 通过值传递
? ? std::thread threadByValue(threadFuncByValue, value);

? ? // 通过引用传递
? ? std::thread threadByReference(threadFuncByReference, std::ref(value));

? ? // 通过移动语义传递
? ? std::thread threadByMove(threadFuncByMove, std::move(greeting));

?? ?
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
void transfer(Box &from, Box &to, int num)
{
? ? // defer_lock表示暂时unlock,默认自动加锁
? ? std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
? ? std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);

? ? //两个同时加锁
? ? std::lock(lock1, lock2);//或者使用lock1.lock()

? ? from.num_things -= num;
? ? to.num_things += num;
? ? //作用域结束自动解锁,也可以使用lock1.unlock()手动解锁
}
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
//main.cpp
#include <stdio.h>
#include <pthread.h>
#include <assert.h>

void* Function_t(void* Param)
{
? ? printf("我是线程! ");
? ? pthread_t myid = pthread_self();
? ? printf("线程ID=%d ", myid);
? ? return NULL;
}

int main()
{
? ? pthread_t pid;
? ? pthread_attr_t attr;
? ? pthread_attr_init(&attr);
? ? pthread_attr_setscope(&attr, PTHREAD_SCOPE_PROCESS);
? ? pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
? ? pthread_create(&pid, &attr, Function_t, NULL);
? ? printf("======================================== ");
? ? getchar();
? ? pthread_attr_destroy(&attr);
? ? return 1;
}
/*******************************************
?pthread_attr_setdetachstate实现线程分离

同理,针对上述设置线程分离状态的方法,也可以在线程刚一创建时即进行分离(而非之后再调用pthread_detach()函数)。首先可以采用默认的方式对线程属性结构进行初始化,接着为创建分离线程而设置属性,最后再以此线程属性结构来创建新线程,线程一旦创建,就无须再保留该属性对象。最后将其摧毁。

初始化线程属性结构及摧毁函数如下。


设置线程分离状态的函数为pthread_attr_setdetachstate()。


参数detachstate用来设置线程的状态,设置PTHREAD_CREATE_DETACHED(分离态)与PTHREAD_CREATE_JOINABLE(结合态)。具体案例代码参加教材4.1.5节。


设置线程绑定状态的函数为pthread_attr_setscope,函数原型为:

int pthread_attr_setscope(pthread_attr_t * tattr, int scope);
函数说明:tattr参数为指向属性结构的指针,scope参数为绑定类型,通常有两个取值PTHREAD_SCOPE_SYSTEM(绑定)、PTHREAD_SCOPE_PROCESS(非绑定)。
********************************************/

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
  设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程。

#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/*初始化属性值,均设为默认值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) my_function, NULL);

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。设置线程分离状态的函数为pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
scope:PTHREAD_SCOPE_SYSTEM:绑定,此线程与系统中所有的线程竞争

  PTHREAD_SCOPE_PROCESS:非绑定,此线程与进程中的其他线程竞争

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
?线程管理
pthread_t:线程句柄.出于移植目的,不能把它作为整数处理,应使用函数pthread_equal()对两个线程ID进行比较。获取自身所在线程id使用函数pthread_self()。
pthread_attr_t:线程属性。主要包括scope属性、detach属性、堆栈地址、堆栈大小、优先级。主要属性的意义如下:
__detachstate,表示新线程是否与进程中其他线程脱离同步。如果设置为PTHREAD_CREATE_DETACHED,则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE状态。可以在线程创建并运行以后用pthread_detach()来设置。一旦设置为PTHREAD_CREATE_DETACHED状态,不论是创建时设置还是运行时设置,则不能再恢复到PTHREAD_CREATE_JOINABLE状态。
__schedpolicy,表示新线程的调度策略,包括SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)和SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用过pthread_setschedparam()来改变。
__schedparam,一个struct sched_param结构,目前仅有一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。系统支持的最大和最小的优先级值可以用函数sched_get_priority_max和sched_get_priority_min得到。
__inheritsched,有两种值可供选择:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED。
__scope,表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。目前LinuxThreads仅实现了PTHREAD_SCOPE_SYSTEM一值。
-----------------------------------
?著作权归作者所有:来自51CTO博客作者文火冰糖的硅基工坊的原创作品,请联系作者获取转载授权,否则将追究法律责任
[架构之路-40]:目标系统 - 系统软件 - Linux OS的线程库pthread简介
https://blog.51cto.com/u_11299290/5759804

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

自旋锁

自旋锁是一种用于保护多线程共享资源的锁,与一般互斥锁不同之处在于:当自旋锁尝试获取锁时以忙等待的形式不断地循环检查锁是否可用。在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

自旋锁和互斥锁的区别

从实现原理上来讲,互斥锁属于sleep-waiting类型的锁,而自旋锁属于busy-waiting类型的锁。也就是说:pthread_mutex_lock()操作,如果没有锁成功的话就会调用system_wait()的系统调用并将当前线程加入该互斥锁的等待队列里;而pthread_spin_lock()则可以理解为,在一个while(1)循环中用内嵌的汇编代码实现的锁操作(在linux内核中pthread_spin_lock()操作只需要两条CPU指令,unlock()解锁操作只用一条指令就可以完成)。

对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间;

对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。

因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
这里可以对thread进行封装(后边的lock也是用了这种封装方式),避免没有调用join或者detach可导致程序出错的情况出现:

#include <iostream>
#include <thread>
using namespace std;

class ThreadGuard
{
public:
? ?enum class DesAction
? ?{
? ? ? join,
? ? ? detach
? ?};

? ?ThreadGuard(std::thread &&t, DesAction a) : m_thread(std::move(t)), m_action(a){};

? ?~ThreadGuard()
? ?{
? ? ? if (m_thread.joinable())
? ? ? {
? ? ? ? ?if (m_action == DesAction::join)
? ? ? ? ?{
? ? ? ? ? ? m_thread.join();
? ? ? ? ?}
? ? ? ? ?else
? ? ? ? ?{
? ? ? ? ? ? m_thread.detach();
? ? ? ? ?}
? ? ? }
? ?}

? ?ThreadGuard(ThreadGuard &&) = default;
? ?ThreadGuard &operator=(ThreadGuard &&) = default;

? ?std::thread &get() { return m_thread; }

private:
? ?std::thread m_thread;
? ?DesAction m_action;
};

int main()
{
? ?ThreadGuard t(std::thread([]()
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?{ std::cout << "thread guard " << std::endl; }),
? ? ? ? ? ? ? ? ?ThreadGuard::DesAction::join);
? ?return 0;
}

c++11还提供了获取线程id,或者系统cpu个数,获取thread native_handle,使得线程休眠等功能

std::thread t(func);
cout << "当前线程ID " << t.get_id() << endl;
cout << "当前cpu个数 " << std::thread::hardware_concurrency() << endl;
auto handle = t.native_handle();// handle可用于pthread相关操作
std::this_thread::sleep_for(std::chrono::seconds(1));
————————————————
版权声明:本文为CSDN博主「不知所措的渣渣辉」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41950508/article/details/126678834
?

文章来源:https://blog.csdn.net/lusic01/article/details/135147897
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。