Linux的多线程

Linux的子线程实际上也是个进程,但是比传统的进程轻量化。

pthread

pthread是用于Linux系统下的线程库,头文件是<pthread.h>。C++11 之前的多线程开发高度依赖平台原生 API,Windows 以 CreateThread 和内核对象为核心,Linux 则遵循 POSIX 标准的 pthread 库。二者在接口设计、同步机制及资源管理上的差异显著增加了跨平台开发成本,这也是 std::thread 标准库被引入的重要动力。

创建线程pthread_create()

线程ID:

ID类型为 pthread_t,是给usigned long int,

查看当前线程ID,调用如下函数:

pthread_t pthread_self(void)        //返回当前线程的线程ID

创建线程函数pthread_create()

pthread_create 是 POSIX 线程库(pthread)中用于创建线程的函数,其语法如下:

#include <pthread.h>int pthread_create(pthread_t *thread,           // 指向线程标识符的指针(输出参数)const pthread_attr_t *attr,  // 线程属性(NULL表示默认属性)void *(*start_routine)(void*), // 线程入口函数(返回void*,参数为void*)void *arg                   // 传递给入口函数的参数
);
// Compile and link with -pthread, 线程库的名字叫pthread, 全名: libpthread.so libptread.a
//注意:pthread的源代码是个动态库,在编译的时候要链接动态库

 代码:

#include <iostream>
#include <pthread.h>
#include <string>
void *callback(void *arg)
{std::cout << "Hello from thread!" <<pthread_self()<< std::endl;for(int i=0; i < 5; ++i){std::cout << "Thread iteration: " << i << std::endl;}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,callback,nullptr );std::cout<<"主线程:"<<pthread_self()<<std::endl;for (int i = 0; i < 5; i++){std::cout<<i<<std::endl;}system("pause");return 0;
}

编译可执行文件:

g++ mythread.cpp -lpthread -o mythread
线程库文件(动态库),需要在编译的时候通过参数指定出来,动态库名为 libpthread.so需要使用的参数为 -l,根据规则掐头去尾最终形态应该写成:-lpthread(参数和参数值中间可以有空格)

线程退出pthread_exit()

在编写多线程程序的时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对于主线程),我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。

#include <pthread.h>
void pthread_exit(void *retval);

参数: 线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为NULL

#include <iostream>
#include <pthread.h>
#include <string>
void *callback(void *arg)
{std::cout << "Hello from thread!" <<pthread_self()<< std::endl;for(int i=0; i < 5; ++i){std::cout << "Thread iteration: " << i << std::endl;}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,callback,nullptr );std::cout<<"主线程:"<<pthread_self()<<std::endl;pthread_exit(NULL);return 0;
}

线程回收pthread_join()阻塞等待

线程函数
线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。

另外通过线程回收函数还可以获取到子线程退出时传递出来的数据,函数原型如下:

#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);

参数:
thread: 要被回收的子线程的线程ID

retval: 二级指针, 指向一级指针的地址, 是一个传出参数, 这个地址中存储了pthread_exit() 传递出的数据,如果不需要这个参数,可以指定为NULL

返回值:线程回收成功返回0,回收失败返回错误号。

代码:

#include <iostream>
#include <pthread.h>
#include <string>
struct Test
{int num;int age;
};
void *callback(void *arg)
{std::cout << "Hello from thread!" <<pthread_self()<< std::endl;for(int i=0; i < 5; ++i){std::cout << "Thread iteration: " << i << std::endl;}struct Test *t = static_cast<struct Test *>(arg); //将void*转换为Test*t->num=100;t->age=6;pthread_exit(t);        //注意不要返回局部变量,不然有内存问题,这里的t指向的地址是主线程传入的地址//如果t是局部变量,主线程无法访问到return nullptr;
}
int main()
{pthread_t tid;struct Test t;pthread_create(&tid,nullptr,callback,&t );std::cout<<"主线程:"<<pthread_self()<<std::endl;void *ptr;pthread_join(tid,&ptr);     //第二个参数,传入的是指针ptr的地址,是个二级指针,ptr最终指向callback函数的tstruct Test * pt=static_cast<struct Test *>(ptr);std::cout << "Thread returned: num = " << pt->num << ", age="<<pt->age<<std::endl;    return 0;
}

注意pthread_exit(t)不要返回局部变量,不然有内存问题。这里的t指向的地址是主线程传入的地址;如果t是局部变量,子线程退出后,子线程栈区数据会被释放,主线程将无法访问到。

线程分离pthread_detach()

在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用pthread_join()只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。

在线程库函数中为我们提供了线程分离函数pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用pthread_join()就回收不到子线程资源了。

#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);

下面的代码中,在主线程中创建子线程,并调用线程分离函数,实现了主线程和子线程的分离:

#include <iostream>
#include <pthread.h>
#include <string>
struct Test
{int num;int age;
};
void *callback(void *arg)
{std::cout << "Hello from thread!" <<pthread_self()<< std::endl;for(int i=0; i < 5; ++i){std::cout << "Thread iteration: " << i << std::endl;}struct Test t;    //在子线程中创建结构体t,因为主线程栈区数据在主线程结束后将访问不到t.num=100;            t.age=6;pthread_exit(&t);    return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,callback,nullptr); //创建线程,传入回调函数和参数std::cout<<"主线程:"<<pthread_self()<<std::endl;pthread_detach(tid); //分离线程,主线程不需要等待子线程结束return 0;
}

注意,线程分离后,如果callback参数是主线程栈区数据,将出现内存异常,子线程访问不到。

主线程正常退出(return/exit)
若主线程通过returnexit结束,整个进程会立即终止,所有子线程(包括已分离的线程)将被强制终止,无论子线程是否完成

主线程调用pthread_exit退出(std::thread中没有与这个对应的函数)
若主线程调用pthread_exit退出,进程会继续运行直到所有非分离线程结束。此时已分离的子线程可继续独立执行,但其生命周期受限于进程存活状态

资源回收机制
分离后的子线程终止时,系统会自动回收其资源(栈空间、寄存器状态等),无需其他线程调用pthread_join。但若主线程导致进程终止,分离线程的资源也会随进程一起被操作系统

风险提示
分离线程若访问主线程已释放的资源(如栈变量),会导致未定义行为。
在守护进程(daemon)中,主线程退出后分离线程可能继续运行,但需注意僵尸线程风险

线程取消pthread_cancel()

线程取消的意思就是在某些特定情况下在一个线程中杀死另一个线程。使用这个函数杀死一个线程需要分两步:
1、在线程A中调用线程取消函数pthread_cancel,指定杀死线程B,这时候线程B是死不了的
2、在线程B中进程一次系统调用(从用户区切换到内核区),否则线程B可以一直运行。

#include <pthread.h>
// 参数是子线程的线程ID
int pthread_cancel(pthread_t thread);

 参数:要杀死的线程的线程ID
返回值:函数调用成功返回0,调用失败返回非0错误号。
在下面的示例代码中,主线程调用线程取消函数,只要在子线程中进行了系统调用,当子线程执行到这个位置就挂掉了。

#include <iostream>
#include <pthread.h>
#include <string>
struct Test
{int num;int age;
};
void *callback(void *arg)
{std::cout << "Hello from thread!" <<pthread_self()<< std::endl;for(int i=0; i < 5; ++i){std::cout << "Thread iteration: " << i << std::endl;}struct Test t;t.num=100;t.age=6;return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,callback,nullptr); //创建线程,传入回调函数和参数std::cout<<"主线程:"<<pthread_self()<<std::endl;for(int i=0; i < 100; ++i){std::cout << "主线程: " << i << std::endl;}pthread_cancel(tid);return 0;
}

关于系统调用有两种方式:
1、直接调用Linux系统函数
2、调用标准C库函数,为了实现某些功能,在Linux平台下标准C库函数会调用相关的系统函数

线程ID比较pthread_equal()

在Linux中线程ID本质就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程的ID,但是线程库是可以跨平台使用的,在某些平台上 pthread_t可能不是一个单纯的整形,这中情况下比较两个线程的ID必须要使用比较函数,函数原型如下:

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

参数:t1 和 t2 是要比较的线程的线程ID
返回值:如果两个线程ID相等返回非0值,如果不相等返回0

C++线程类 

++11之前,C++语言没有对并发编程提供语言级别的支持,这使得我们在编写可移植的并发程序时,存在诸多的不便。现在C++11中增加了线程以及线程相关的类,很方便地支持了并发编程,使得编写的多线程程序的可移植性得到了很大的提高。

C++11中提供的线程类叫做std::thread,基于这个类创建一个新的线程非常的简单,只需要提供线程函数或者函数对象即可,并且可以同时指定线程函数的参数。

C++语言级别的多线程编程=》代码可以跨平台windows/linux/mac

本节主要内容:
thread/
mutex/
condition_variable/
lock_guard/
unique_lock/
atomic 原子类型 基于CAS操作的原子类型 线程安全的
sleep_for

本质上相当于在语言层面加了层封装,在底层仍然调用操作系统各自的API
C++语言层面:                                   thread
底层:                      windows                                          linux(调用stace ./a.out可以看过程)
                            createThread                                pthread_create

构造函数

// ①
thread() noexcept;
// ②
thread( thread&& other ) noexcept;
// ③
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
// ④
thread( const thread& ) = delete;

构造函数①:默认构造函,构造一个线程对象,在这个线程中不执行任何处理动作

构造函数②:移动构造函数,将 other 的线程所有权转移给新的thread 对象。之后 other 不再表示执行线程。

构造函数③:创建线程对象,并在该线程中执行函数f中的业务逻辑,args是要传递给函数f的参数
任务函数f的可选类型有很多,具体如下:
        普通函数,类成员函数,匿名函数,仿函数(这些都是可调用对象类型)
        可以是可调用对象包装器类型,也可以是使用绑定器绑定之后得到的类型(仿函数)

构造函数④:使用=delete显示删除拷贝构造, 不允许线程对象之间的拷贝

公共成员函数

get_id()

应用程序启动之后默认只有一个线程,这个线程一般称之为主线程或父线程,通过线程类创建出的线程一般称之为子线程,每个被创建出的线程实例都对应一个线程ID,这个ID是唯一的,可以通过这个ID来区分和识别各个已经存在的线程实例,这个获取线程ID的函数叫做get_id(),函数原型如下:

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

获取子线程t1的线程id:t1.get_id();
获取当前线程的线程id:this_thread::get_id()

线程回收

在C++标准中,‌必须‌对std::thread对象显式调用join()detach(),否则会导致程序终止(触发std::terminate)。这是由C++11标准严格规定的线程生命周期管理机制。

因此,必须二选一:
加入式(join())                        ——同步等待
分离式(detach())                   ——异步分离

join()

在某个线程中通过子线程对象调用join()函数,调用这个函数的线程被阻塞,但是子线程对象中的任务函数会继续执行,当任务执行完毕之后join()会清理当前子线程中的相关资源然后返回,同时,调用该函数的线程解除阻塞继续向下执行。
调用方法:t1.join();

detach()

detach()函数的作用是进行线程分离,分离主线程和创建出的子线程。在线程分离之后,主线程退出也会一并销毁创建出的所有子线程,在主线程退出之前,它可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源。
调用方法:t1.detach()

detach()应用场景适配性

  • 适用于‌后台任务‌(如日志记录、心跳检测),主线程无需等待其完成
  • 允许主线程快速响应新请求,而分离线程持续处理耗时操作
  • 避免join()导致的线程嵌套阻塞问题(如线程A等待线程B,线程B又等待线程A)

joinable()

joinable()函数用于判断主线程和子线程是否处理关联(连接)状态,一般情况下,二者之间的关系处于关联状态,该函数返回一个布尔类型:

返回值为true:主线程和子线程之间有关联(连接)关系
返回值为false:主线程和子线程之间没有关联(连接)关系

bool joinable() const noexcept;
#include<iostream>
#include<thread>
#include <chrono> 
void threadFunction() {std::this_thread::sleep_for(std::chrono::seconds(2));
}
int main() {std::thread t;std::cout << "before starting, joinable: " << t.joinable() << std::endl;t = std::thread(threadFunction);std::cout << "after starting, joinable: " << t.joinable() << std::endl;t.join();std::cout << "after joining, joinable: " << t.joinable() << std::endl;std::thread t1(threadFunction);std::cout << "after starting, joinable: " << t1.joinable() << std::endl;t1.detach();std::cout << "after detaching, joinable: " << t1.joinable() << std::endl;return 0;
}

 结果如下

结论:
1、在创建的子线程对象的时候,如果没有指定任务函数,那么子线程不会启动,主线程和这个子线程也不会进行连接
2、在创建的子线程对象的时候,如果指定了任务函数,子线程启动并执行任务,主线程和这个子线程自动连接成功
3、子线程调用了detach()函数之后,父子线程分离,同时二者的连接断开,调用joinable()返回false
4、在子线程调用了join()函数,子线程中的任务函数继续执行,直到任务处理完毕,这时join()会清理(回收)当前子线程的相关资源,所以这个子线程和主线程的连接也就断开了,因此,调用join()之后再调用joinable()会返回false。

静态函数 hardware_concurrency()

thread线程类还提供了一个静态方法,用于获取当前计算机的CPU核心数,根据这个结果在程序中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的。

static unsigned hardware_concurrency() noexcept;

 代码:

int main() {int num=std::thread::hardware_concurrency();std::cout<<"CPU number:"<<num<<std::endl;return 0;
}

命名空间 - this_thread

get_id()

调用命名空间std::this_thread中的get_id()方法可以得到当前线程的线程ID,函数原型如下:

thread::id get_id() noexcept;

sleep_for() 

线程被创建后有这五种状态:创建态,就绪态,运行态,阻塞态(挂起态),退出态(终止态)

线程和进程的执行有很多相似之处,在计算机中启动的多个线程都需要占用CPU资源,但是CPU的个数是有限的并且每个CPU在同一时间点不能同时处理多个任务。为了能够实现并发处理,多个线程都是分时复用CPU时间片,快速的交替处理各个线程中的任务。因此多个线程之间需要争抢CPU时间片,抢到了就执行,抢不到则无法执行(因为默认所有的线程优先级都相同,内核也会从中调度,不会出现某个线程永远抢不到CPU时间片的情况)。

命名空间this_thread中提供了一个休眠函数sleep_for(),调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长,因为阻塞态的线程已经让出了CPU资源,代码也不会被执行,所以线程休眠过程中对CPU来说没有任何负担。这个函数是函数原型如下,参数需要指定一个休眠时长,是一个时间段:

template <class Rep, class Period>void sleep_for (const chrono::duration<Rep,Period>& rel_time);
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;void func()
{for (int i = 0; i < 10; ++i){this_thread::sleep_for(chrono::seconds(1));cout << "子线程: " << this_thread::get_id() << ", i = " << i << endl;}
}int main()
{thread t(func);t.join();
}

在func()函数的for循环中使用了this_thread::sleep_for(chrono::seconds(1));之后,每循环一次程序都会阻塞1秒钟,也就是说每隔1秒才会进行一次输出。需要注意的是:程序休眠完成之后,会从阻塞态重新变成就绪态,就绪态的线程需要再次争抢CPU时间片,抢到之后才会变成运行态,这时候程序才会继续向下运行。

sleep_until()

命名空间this_thread中提供了另一个休眠函数sleep_until(),和sleep_for()不同的是它的参数类型不一样

sleep_until():指定线程阻塞到某一个指定的时间点time_point类型,之后解除阻塞
sleep_for():指定线程阻塞一定的时间长度duration 类型,之后解除阻塞

该函数的函数原型如下: 

template <class Clock, class Duration>void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;void func()
{for (int i = 0; i < 10; ++i){// 获取当前系统时间点auto now = chrono::system_clock::now();// 时间间隔为2schrono::seconds sec(2);// 当前时间点之后休眠两秒this_thread::sleep_until(now + sec);cout << "子线程: " << this_thread::get_id() << ", i = " << i << endl;}
}int main()
{thread t(func);t.join();
}

yield()

命名空间this_thread中提供了一个非常绅士的函数yield(),在线程中调用这个函数之后,处于运行态的线程会主动让出自己已经抢到的CPU时间片,最终变为就绪态,这样其它的线程就有更大的概率能够抢到CPU时间片了。使用这个函数的时候需要注意一点,线程调用了yield()之后会主动放弃CPU资源,但是这个变为就绪态的线程会马上参与到下一轮CPU的抢夺战中,不排除它能继续抢到CPU时间片的情况,这是概率问题。

void yield() noexcept;
#include <iostream>
#include <thread>
using namespace std;void func()
{for (int i = 0; i < 100000000000; ++i){cout << "子线程: " << this_thread::get_id() << ", i = " << i << endl;this_thread::yield();}
}int main()
{thread t(func);thread t1(func);t.join();t1.join();
}

结论:
1、std::this_thread::yield() 的目的是避免一个线程长时间占用CPU资源,从而导致多线程处理性能下降。
2、std::this_thread::yield() 是让当前线程主动放弃了当前自己抢到的CPU资源,但是在下一轮还会继续抢。

互斥锁

解决多线程数据混乱的方案就是进行线程同步,最常用的就是互斥锁,在C++11中一共提供了四种互斥锁:

std::mutex:独占的互斥锁,不能递归使用
std::timed_mutex:带超时的独占互斥锁,不能递归使用
std::recursive_mutex:递归互斥锁,不带超时功能
std::recursive_timed_mutex:带超时的递归互斥锁
互斥锁在有些资料中也被称之为互斥量,二者是一个东西。

 std::mutex

成员函数
lock()函数用于给临界区加锁,并且只能有一个线程获得锁的所有权,它有阻塞线程的作用,函数原型如下

void lock();

独占互斥锁对象有两种状态:锁定和未锁定。如果互斥锁是打开的,调用lock()函数的线程会得到互斥锁的所有权,并将其上锁,其它线程再调用该函数的时候由于得不到互斥锁的所有权,就会被lock()函数阻塞。当拥有互斥锁所有权的线程将互斥锁解锁,此时被lock()阻塞的线程解除阻塞,抢到互斥锁所有权的线程加锁并继续运行,没抢到互斥锁所有权的线程继续阻塞。

除了使用lock()还可以使用try_lock()获取互斥锁的所有权并对互斥锁加锁,函数原型如下:

bool try_lock();

二者的区别在于try_lock()不会阻塞线程,lock()会阻塞线程:

如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回true
如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回false
当互斥锁被锁定之后可以通过unlock()进行解锁,但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。该函数的函数原型如下:

void unlock();

通过介绍以上三个函数,使用互斥锁进行线程同步的大致思路差不多就能搞清楚了,主要分为以下几步:

1、找到多个线程操作的共享资源(全局变量、堆内存、类成员变量等),也可以称之为临界资源
2、找到和共享资源有关的上下文代码,也就是临界区(下图中的黄色代码部分)
3、在临界区的上边调用互斥锁类的lock()方法
4、在临界区的下边调用互斥锁的unlock()方法
线程同步的目的是让多线程按照顺序依次执行临界区代码,这样做线程对共享资源的访问就从并行访问变为了线性访问,访问效率降低了,但是保证了数据的正确性。

当线程对互斥锁对象加锁,并且执行完临界区代码之后,一定要使用这个线程对互斥锁解锁,否则最终会造成线程的死锁。死锁之后当前应用程序中的所有线程都会被阻塞,并且阻塞无法解除,应用程序也无法继续运行。

注意:

1、在所有线程的任务函数执行完毕之前,互斥锁对象是不能被析构的,一定要在程序中保证这个对象的可用性。
2、互斥锁的个数和共享资源的个数相等,也就是说每一个共享资源都应该对应一个互斥锁对象。互斥锁对象的个数和线程的个数没有关系。

代码示例:

#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<chrono>
std::mutex mtx; // Global mutex for thread safety
int ticketCount = 50;
void sellTicket(int i) 
{while(ticketCount > 0)  //ticketCount=1 锁+双重判断,以防ticketCount出现-1的情况{mtx.lock();if(ticketCount > 0) {std::cout<<"窗口"<<i<<"售出一张票,剩余票数:"<<ticketCount<<std::endl;ticketCount--;}mtx.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulate}
}int main() {std::list<std::thread> tList;for(int i=0;i<3;i++){tList.push_back(std::thread(sellTicket,i));}for(auto &t : tList){if(t.joinable()){t.join();}}return 0;
}

std::lock_guard

lock_guard是C++11新增的一个模板类,使用这个类,可以简化互斥锁lock()和unlock()的写法,同时也更安全(防止ulock()调用不到)。这个模板类的定义和常用的构造函数原型如下:

// 类的定义,定义于头文件 <mutex>
template< class Mutex >
class lock_guard;// 常用构造函数
explicit lock_guard( mutex_type& m );

lock_guard在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记unlock()操作而导致线程死锁。lock_guard使用了RAII技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。

#include<iostream>
#include<thread>
#include<list>
#include<mutex>
#include<chrono>
std::mutex mtx; // Global mutex for thread safety
int ticketCount = 50;
void sellTicket(int i) 
{while(ticketCount > 0)  //ticketCount=1 锁+双重判断,以防ticketCount出现-1的情况{//mtx.lock();{// 保证所有线程都能释放锁,防止死锁问题发生std::lock_guard<std::mutex> lock(mtx);if(ticketCount > 0) {std::cout<<"窗口"<<i<<"售出一张票,剩余票数:"<<ticketCount<<std::endl;ticketCount--;}}//mtx.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulate}
}int main() {std::list<std::thread> tList;for(int i=0;i<3;i++){tList.push_back(std::thread(sellTicket,i));}for(auto &t : tList){if(t.joinable()){t.join();}}return 0;
}

缺陷:
锁粒度控制不灵活

  • 固定作用域锁定‌:在构造时立即加锁,析构时自动解锁,无法在作用域内手动释放锁。
  • 性能影响‌:若临界区范围过大(如循环内部存在耗时操作),会导致锁持有时间过长,降低并发效率

功能局限性

  • 不支持延迟加锁‌:创建时必须立即锁定互斥量,无法实现“先构造后加锁”。
  • 不可手动解锁‌:无法在作用域结束前主动释放锁(如条件判断后提前解锁)。
  • 所有权不可转移‌:不支持移动语义,无法在函数间传递锁所有权。

lock_guard无法在函数间传递所有权,错误代码示例:

#include <iostream>
#include <mutex>
#include <thread>std::mutex mtx;// 尝试传递lock_guard作为参数(错误示例)
void process_data(std::lock_guard<std::mutex> lock) {  // 编译错误:lock_guard不可拷贝std::cout << "Processing data with lock held\n";
}int main() {std::lock_guard<std::mutex> lock(mtx);  // 正确用法:局部作用域锁定// process_data(lock);  // 错误:lock_guard不能拷贝传递return 0;
}

可以改用unique_lock配合std::move实现所有权转移

#include <iostream>
#include <mutex>
#include <thread>std::mutex mtx;// 使用unique_lock的正确方案
void process_data(std::unique_lock<std::mutex> lock) {  // 支持移动语义std::cout << "Processing data with lock held\n";
}int main() {std::unique_lock<std::mutex> lock(mtx);  // 先获取锁process_data(std::move(lock));  // 通过移动语义传递所有权// 此时lock不再拥有互斥量if (!lock.owns_lock()) {std::cout << "Lock ownership transferred\n";}return 0;
}

可以理解为lock_guard相当于scoped_ptr,unique_lock相当于unique_ptr。

std::unique_lock

unique_lock与lock_guard区别: 
1、lock_guard只能用在简单的临界区代码段的互斥操作中,不能用在函数参数传递或者返回过程中
2、unique_lock不仅能用在简单的临界区代码段的互斥操作中,还能用在函数调用过程中
3、unique_lock有lock、unlock、try_lock等构造方法,支持延迟加锁

// lock_guard用法(无法手动解锁)
std::mutex mtx;
{std::lock_guard<std::mutex> lk(mtx); // 自动加锁// 临界区代码
} // 自动解锁// unique_lock用法(支持手动控制)
std::unique_lock<std::mutex> ulk(mtx, std::defer_lock);
ulk.lock();   // 显式加锁
// 临界区代码
ulk.unlock(); // 显式解锁

 

多线程编程的两个核心问题:互斥与同步通信

1. 线程间互斥:解决竞态条件问题

问题本质

当多个线程同时访问‌共享资源‌(内存、文件、设备等)时,如果至少有一个线程执行‌写操作‌,就会产生‌竞态条件‌。最终结果取决于线程执行的随机顺序,导致程序行为不可预测。

关键概念

  • 临界区‌:访问共享资源的代码段(需要保护的区域)
  • 原子操作‌:不可分割的操作(要么完全执行,要么完全不执行)
  • 数据竞争‌:多个线程同时访问同一内存位置,且至少有一个是写操作

类比解释

想象一个公共卫生间:

  • 共享资源‌:卫生间(只能一个人使用)
  • 临界区‌:卫生间内部
  • 线程‌:需要如厕的人
  • 竞态条件‌:多人同时尝试进入卫生间导致混乱

解决方案与技术

// 共享资源
int shared_counter = 0;// 解决方案1:互斥锁(Mutex)
std::mutex mtx;
void safe_increment() {std::lock_guard<std::mutex> lock(mtx); // 进入临界区前加锁shared_counter++; // 临界区操作// 离开作用域自动解锁
}// 解决方案2:原子操作(Atomic)
std::atomic<int> atomic_counter(0);
void atomic_increment() {atomic_counter++; // 无锁原子操作
}

关键要点

  1. 互斥确保‌同一时间只有一个线程‌访问临界区
  2. 临界区应尽可能‌短小‌(减少锁持有时间)
  3. 优先考虑‌无锁设计‌(原子操作、线程本地存储)
  4. 警惕‌死锁‌(多个锁使用固定顺序)

2. 线程间同步通信:协调执行顺序

问题本质

当线程之间存在‌依赖关系‌时(例如生产者-消费者),需要协调它们的执行顺序。线程同步通信解决的是"‌何时执行‌"的问题,而非"是否冲突"。

关键概念

  • 执行顺序依赖‌:一个线程需要等待另一个线程完成特定操作
  • 事件通知‌:线程间发送信号通知状态变化
  • 阻塞/唤醒机制‌:让线程在条件不满足时休眠,条件满足时唤醒

类比解释

餐厅厨房工作流程:

  • 生产者‌:厨师(制作菜肴)
  • 消费者‌:服务员(上菜)
  • 共享资源‌:出菜台
  • 同步需求‌:服务员必须等待厨师完成菜肴才能上菜

生产者-消费者模型实现

同步机制详解

机制作用特点
std::condition_variable线程等待/通知必须与std::mutex配合使用
wait(lock, predicate)条件等待自动释放锁,被唤醒后重新加锁
notify_one()通知一个等待线程精确唤醒,避免惊群效应
notify_all()通知所有等待线程适用于多消费者场景

互斥与同步的关系与区别

特性线程互斥线程同步通信
核心问题防止并发访问冲突协调执行顺序
关注点"能否访问"资源"何时访问"资源
类比卫生间门锁厨师-服务员协作
典型场景计数器更新生产者-消费者
主要机制互斥锁、原子操作条件变量、信号量
性能重点减少锁竞争精确唤醒、避免忙等待

最佳实践总结

  1. 分层设计‌:先解决互斥(数据安全),再处理同步(执行顺序)
  2. RAII原则‌:使用lock_guard/unique_lock管理锁资源
  3. 精确通知‌:使用notify_one()替代notify_all()减少不必要唤醒
  4. 避免虚假唤醒‌:条件变量等待始终使用谓词检查
  5. 无锁设计‌:对于高性能场景,考虑原子操作或无锁队列
  6. 线程退出管理‌:确保所有线程能安全结束(避免死等)

通过理解这两个问题的本质区别与内在联系,可以设计出正确高效的多线程程序,既保证数据安全,又实现线程间高效协作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/86963.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/86963.shtml
英文地址,请注明出处:http://en.pswp.cn/web/86963.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Windows 环境下 NVM 命令详解:多版本 Node.js 管理利器

“一个 Node.js 版本走天下&#xff1f;太局限了&#xff01;试试 nvm&#xff0c;版本切换如丝般顺滑。” 什么是 NVM NVM&#xff08;Node Version Manager&#xff09;是一个命令行工具&#xff0c;允许你安装并在多个 Node.js 版本之间自由切换。 在 Linux/macOS 下常用的…

一二级路由之间的传参方式以及高亮问题

实现如下图所示的一二级路由的高亮情况&#xff1a; 在一级路由APP.vue下设置&#xff1a; .head a.router-link-active {background-color: rgb(235, 221, 204); }在二级路由Mycenter.vue下设置&#xff1a; /* 要求在点击跳转到mycenter_lianxi页面时候父路由保持高亮…

前端JavaScript力扣HOT100刷题【51-100】

注&#xff1a;纯手打&#xff0c;如有错误欢迎评论区交流&#xff01; 转载请注明出处&#xff1a;https://blog.csdn.net/testleaf/article/details/148953015 编写此文是为了更好地学习前端知识&#xff0c;如果损害了有关人的利益&#xff0c;请联系删除&#xff01; 本文章…

智能制造数字孪生集成交付生态链:智慧产线极速克隆,孪生重构生产周期

在智能制造的浪潮中&#xff0c;数字孪生技术正以前所未有的速度重塑制造业的生产模式。从产品设计到生产制造&#xff0c;再到运维管理&#xff0c;数字孪生通过构建物理世界的虚拟镜像&#xff0c;实现了生产全流程的数字化映射与优化。 山东融谷信息以“智能制造数字孪生集成…

非常详细版: dd.device.geolocation 钉钉微应用获取定位,移动端 PC端都操作,Vue实现钉钉微应用获取精准定位并渲染在地图组件上

dd.device.geolocation 钉钉微应用获取定位,钉钉微应用获取精准定位并渲染在地图组件上 ,手机端 PC端要都可用 【dd.device.geolocation是需要鉴权的哦】 想要的数据和效果图 想要的数据格式 代码 <template><div class="dialogStyles"

鸿蒙5:组件状态共享

目录 1. 组件状态共享 1.1 状态共享-父子传值&#xff1a;Local、Param、Event 1.2 状态共享-父子双向绑定!! 1.3 跨代共享&#xff1a;Provider和Consumer 1.3.1 aliasName和属性名 1.3.2 实现跨代共享 1.3.3 装饰复杂类型&#xff0c;配合Trace一起使用 1.3.4 支持共…

【MySQL】12. C语言与数据库的连接

1. 下载MySQL的连接库 sudo apt install -y libmysqlclient-dev 2. MySQL连接库的常用接口介绍 通过下面的样例了解MYSQL的常用接口&#xff1a; #include <iostream> #include <mysql/mysql.h> using namespace std;const char *host "localhost";…

[springboot系列] 探秘JUnit 5: Java单元测试利器

介绍 JUnit 5 是一个用于 Java 编程语言的单元测试框架&#xff0c;它是 JUnit 框架的第五个版本&#xff0c;与 JUnit 4 相比&#xff0c;JUnit 5 提供了许多改进和新特性&#xff0c;包括更好的扩展性、灵活性和对现代 Java 特性的支持。 JUnit 5 由三个主要的子模块组成&a…

开源 java android app 开发(十三)绘图定义控件、摇杆控件的制作

文章的目的为了记录使用java 进行android app 开发学习的经历。本职为嵌入式软件开发&#xff0c;公司安排开发app&#xff0c;临时学习&#xff0c;完成app的开发。开发流程和要点有些记忆模糊&#xff0c;赶紧记录&#xff0c;防止忘记。 相关链接&#xff1a; 开源 java an…

Python 库 包 sentence-transformers

sentence-transformers 是一个非常流行的 Python 库&#xff0c;专门用于将文本&#xff08;句子、段落、文档&#xff09;转换为高质量的语义向量&#xff08;嵌入&#xff09;。它基于 Transformer 架构&#xff08;如 BERT、RoBERTa、DistilBERT 等&#xff09; 的预训练模型…

《聚类算法》入门--大白话篇:像整理房间一样给数据分类

一、什么是聚类算法&#xff1f; 想象一下你的衣柜里堆满了衣服&#xff0c;但你不想一件件整理。聚类算法就像一个聪明的助手&#xff0c;它能自动帮你把衣服分成几堆&#xff1a;T恤放一堆、裤子放一堆、外套放一堆。它通过观察衣服的颜色、大小、款式这些特征&#xff0c;把…

AutoGen(五) Human-in-the-Loop(人类在环)实战与进阶:多智能体协作与Web交互全流程(附代码)

AutoGen Human-in-the-Loop&#xff08;人类在环&#xff09;实战与进阶&#xff1a;多智能体协作与Web交互全流程&#xff08;附代码&#xff09; 引言&#xff1a;AI自动化的极限与人类参与的价值 在大模型&#xff08;LLM&#xff09;驱动的AI应用开发中&#xff0c;完全自…

并查集 Union-Find

目录 引言 简单介绍 浅浅总结 算法图解 初始化 根节点查找 集合合并 连通性检查 例题 大概思路 完整代码&#xff1a; 引言 一个小小的并查集让我们在ccpc卡了那么久(还有unordered_map,如果不是忘了map自动排序这么一回事也不至于试那么多发)&#xff0c;至今仍然心有…

书籍在行列都排好序的矩阵中找数(8)0626

题目&#xff1a; 给定一个有N*M的整型矩阵matrix和一个整数K&#xff0c;matrix的每一行和每一列都是排好序的。实现一个函数&#xff0c;判断K是否在matrix中。 0 1 2 5 2 3 4 7 4 4 4 8 5 …

深度学习04 卷积神经网络CNN

卷积神经网络与人工神经网络关系与区别 概念 卷积神经网络&#xff08;Convolutional Neural Network, CNN&#xff09;是人工神经网络&#xff08;Artificial Neural Network, ANN&#xff09;的一种特殊形式&#xff0c;两者在核心思想和基础结构上存在关联&#xff0c;但在…

vue基础之组件通信(VUE3)

文章目录 前言一、父子组件通信1.父组件向子组件通信2.子组件向父组件通信3.ref父组件直接操作子组件通信。 二、跨代通信1. 跨层级通信2.事件总线通信 总结 前言 vue3的组件通信和vue2相比在语法上会有些差距&#xff0c;且vue3有的通信方式也在功能上比vue2更加完善&#xf…

【RidgeUI AI+系列】中文重复统计器

中文重复统计器 文字重复统计是一个使用文本处理工具&#xff0c; 输入文本内容并指定最小词长度后&#xff0c; 就能自动高亮显示重复的词。 本教程将会借助AI实现这个应用的开发 页面脚本编写 该工具的基础流程较为清晰&#xff1a;用户输入一段文字后&#xff0c;调用提取…

代码随想录|图论|05岛屿数量(深搜DFS)

leetcode:99. 岛屿数量 题目 题目描述&#xff1a; 给定一个由 1&#xff08;陆地&#xff09;和 0&#xff08;水&#xff09;组成的矩阵&#xff0c;你需要计算岛屿的数量。岛屿由水平方向或垂直方向上相邻的陆地连接而成&#xff0c;并且四周都是水域。你可以假设矩阵外均…

数据结构-第二节-堆栈与队列

一、概念&#xff1a; 堆栈与队列也是线性表&#xff0c;但是&#xff1a; 堆栈&#xff1a;只能在一个端进行插入删除&#xff0c;此端称为栈顶。&#xff08;特点&#xff1a;后来居上&#xff09; 队列&#xff1a;在一端进行插入&#xff08;队尾&#xff09;&#xff0…

HarmonyNext动画大全02-显式动画

HarmonyOS NEXT显式动画详解 1. 核心接口 显式动画通过animateTo接口实现&#xff0c;主要特点包括&#xff1a; 触发方式&#xff1a;需主动调用接口触发动画 参数配置 &#xff1a; animateTo({duration: 1000, // 动画时长(ms)curve: Curve.Ease, // 动画曲线delay: 200…