C++ 多线程和互斥量

发布于:2025-07-19 ⋅ 阅读:(18) ⋅ 点赞:(0)

1 线程与进程

进程,拥有资源并且独立运行的基本单位;进程间通信用管道或socket。
线程,程序执行的最小单元;线程间共享资源 互斥量。状态:就绪、运行、阻塞。
C++11中提供了多线程的标准库,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。

用到的头文件:

#include <atomic>   // C++11 原子操作,限制并发程序对共享数据的使用,避免数据竞争
#include <thread>   // 该头文件主要声明了std::thread类,另外std::this_thread命名空间也在该头文件中
#include <mutex>    // C++11 互斥量Mutex。在多线程环境中,有多个线程竞争同一个公共资源,就很容易引发线程安全的问题
#include <condition_variable>  // C++11 并发编程 主要包含了与条件变量相关的类和函数
#include <deque>//是双端队列(double-ended queue)是一种具有动态大小、可以在两端进行插入和删除操作的容器

编译时添加编译参数:

set(CMAKE_CXX_FLAGS -pthread)
message(STATUS "CMAKE_CXX_FLAGS = ${CMAKE_CXX_FLAGS}")

2. 使用举例

一个简单的多线程:

#include <iostream>
#include <thread>
 
void myfun() {
    std::cout << "I'm myfun()" << std::endl;
}
 
int main() {
    std::thread thread1(myfun);
    thread1.join();
    return 0;
}

解释:

1 首先,构建一个std::thread对象thread1,构造的时候传递了一个参数,这个参数是一个函数,这个函数就是这个线程的入口函数,函数执行完了,整个线程也就执行完了。
2 线程创建成功后,就会立即启动,并没有一个类似start的函数来显式的启动线程。
3 一旦线程开始运行, 就需要显式的决定是要等待它完成(join),或者分离它让它自行运行(detach)。
4 注意:只需要在std::thread对象被销毁之前做出这个决定。
这个例子中,对象thread1是栈上变量,在main函数执行结束后就会被销毁,所以需要在main函数结束之前做决定。
这个例子中选择了使用 thread1.join(),主线程会一直阻塞着,直到子线程完成,join()函数的另一个任务是回收该线程中使用的资源。

#include <iostream>
#include <thread>
 
void myfun() {
    //延时500ms 为了保证test()运行结束之后才打印
    std::this_thread::sleep_for(std::chrono::milliseconds(500)); 
    std::cout << "I'm myfun()" << std::endl;
}
int main() {
    std::thread thread1(myfun);

    //thread1.join();
    thread1.detach();
    //让主线程晚于子线程结束
    std::cout << "main() finished" << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(1000)); //延时1s
    return 0;
}

运行结果:
使用 thread1.detach()时

main() finished
I'm myfun()

使用 thread1.join()时

 I'm myfun()
 main() finished

分析:

由于线程入口函数内部有个500ms的延时,所以在还没有打印的时候,main()已经执行完成了,thread1已经被析构了,但是它负责的那个线程还是能够运行,这就是detach()的作用。
如果去掉main函数中的1s延时,会发现什么都没有打印,因为主线程执行的太快,整个程序已经结束了,那个后台线程被C++运行时库回收了。
如果将thread1.detach()换成thread1.join(),test函数会在thread1线程执行结束之后,才会执行结束。

2. 带参数异步并占位

例子:

#include <iostream>
#include <thread>
#include <mutex>
#include<future>
#include <stdlib.h>
#include <unistd.h>



using namespace std;
double t1(const double a, const double b)
{
	double c = a + b;
    sleep(3);
    return c;
}

int main() 
{
	double a = 2.3;
	double b = 6.7;
	future<double> fu = async(t1, a, b);//创建异步线程线程,并将线程的执行结果用fu占位;
	cout << "compute" << endl;
	cout << "please wait......" << endl;
	cout << "result:" << fu.get() << endl;//阻塞主线程,直至异步线程return
  //future对象的get()方法只能调用一次
	return 0;
}

解释:
async与future:
async是一个函数模板,用来启动一个异步任务,它返回一个future类模板对象,future对象起到了占位的作用,刚实例化的future是没有储存值的,但在调用future对象的get()成员函数时,主线程会被阻塞直到异步线程执行结束,并把返回结果传递给future,即通过FutureObject.get()获取函数返回值。

3. 互斥量使用

使用 lock unlock

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <stdlib.h>
#include <unistd.h>

using namespace std;
 
static std::mutex g_lock;
 
void lock_unlock()
{
    //上锁
    g_lock.lock();
    cout << "in id: " << this_thread::get_id() << endl;
    this_thread::sleep_for(chrono::seconds(1));
    cout << "out id: " << this_thread::get_id() << endl;
    //解锁
    g_lock.unlock();
}
 
void f_lock_guard()
{
    //lock_guard在构造时会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁.
    lock_guard<std::mutex> lock(g_lock);
    cout << "in id: " << this_thread::get_id() << endl;
    this_thread::sleep_for(chrono::seconds(1));
    cout << "out id: " << this_thread::get_id() << endl;
}
 
int main()
{
    std::thread t1(lock_unlock);
    std::thread t2(lock_unlock);
    std::thread t3(lock_unlock);
 
    t1.join();
    t2.join();
    t3.join();
 
    std::thread t4(f_lock_guard);
    std::thread t5(f_lock_guard);
    std::thread t6(f_lock_guard);
 
    t4.join();
    t5.join();
    t6.join();
 
    return 0;
}

通常不直接使用 mutex,lock_guard更加安全, 更加方便。
lock_guard简化了 lock/unlock 的写法, lock_guard在构造时自动锁定互斥量, 而在退出作用域时会析构自动解锁, 保证了上锁解锁的正确操作, 正是典型的 RAII 机制。下面是生产者和消费者例子

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <stdlib.h>
#include <unistd.h>
using namespace std;

deque<int> q;
mutex mu;
condition_variable cond;
int c = 0;//缓冲区的产品个数

void producer() { 
	int data1;
	while (1) {//通过外层循环,能保证生成用不停止
		if(c < 3) {//限流
			{
				data1 = rand();
				unique_lock<mutex> locker(mu);//锁 有延迟锁定、时间锁定、手动锁定和解锁等 比 lock_guard复杂 功能差不多
				q.push_front(data1); //开头插入元素
				cout << "save: " << data1 << endl;
				cond.notify_one();  // 通知取
				++c;
			}
			sleep(1);
		}
	}
}

void consumer() {
	int data2;//data用来覆盖存放取的数据
	while (1) {
		{
			unique_lock<mutex> locker(mu);
			while(q.empty())
				cond.wait(locker); //wati()阻塞前先会解锁,解锁后生产者才能获得锁来放产品到缓冲区;生产者notify后,将不再阻塞,且自动又获得了锁。
			data2 = q.back();//取的第一步 获得最后一个元素
			q.pop_back();//取的第二步  删除最后一个元素
			cout << "get: " << data2<<endl;
			--c;
		}
		sleep(2);
	}
}
int main() {
	thread t1(producer);
	thread t2(consumer);
	t1.join();
	t2.join();
	return 0;
}

解释:生产者先产生 data1 并压入队列 q , 发出通知; 消费者线程先检测队列非空, 再等待通知和解锁, 从q中取出(分成2步:back 和 pop)放到data2

参考 : C++ 知识点汇总

4.sleep()/usleep()/this_thread::yield()/this_thread::sleep_for()作用

<1>.sleep()作用

功能: 将整个进程都休眠的

<2>.usleep()作用

功能: 将某个线程休眠

<3>.this_thread::yield()作用

功能: 线程调用该方法时,主动让出CPU,并且不参与CPU的本次调度,从而让其他线程有机会运行。在后续的调度周期里再参与CPU调度。这是主动放弃CPU的方法接口。

<4>.this_thread::sleep_for(chrono::nanoseconds(1))作用

功能: 线程调用该方法时,同样会让出CPU,并且休眠一段时间,从而让其他线程有机会运行。等到休眠结束时,才参与CPU调度。这也是主动放弃CPU的方法。

this_thread::yield()方法让出CPU的时间是不确定的,并且以CPU调度时间片为单位,yield()的实现依赖于操作系统CPU调度策略,在不同的操作系统或者同一个操作系统的不同调度策略下,表现也可能是不同的。

而sleep_for()让出CPU的时间是固定的。

示例

<1>.不添加延时函数

#include <iostream>
#include <thread>
#include <unistd.h> 
using namespace std;
 
void task_01(int i){  
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);  
  while(i < 1000)
    i++;
  printf("xxx------->%s(), line = %d, i = %d\n",__FUNCTION__,__LINE__,i);
}
int main(){
  int i = 0, j = 0;

  //1.任务线程.
  thread t1(task_01, i);
  t1.detach();//线程与进程分离,各自执行,顺序是混换的.
  
  //t1.join();//主进程等待线程执行结束,然后主进程开始执行.
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);
  //this_thread::sleep_for(chrono::nanoseconds(1));
  //this_thread::yield;
  //sleep(1);
  return 0;
}

结果:

root@LoongOS:test# ./test_thread
xxx------->main(), line = 70

<2>.使用this_thread::sleep_for(chrono::nanoseconds(1))延时

#include <iostream>
#include <thread>
#include <unistd.h> 
using namespace std;
 
void task_01(int i){  
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);  
  while(i < 1000)
    i++;
  printf("xxx------->%s(), line = %d, i = %d\n",__FUNCTION__,__LINE__,i);
}
int main(){
  int i = 0, j = 0;

  //1.任务线程.
  thread t1(task_01, i);
  t1.detach();//线程与进程分离,各自执行,顺序是混换的.
  
  //t1.join();//主进程等待线程执行结束,然后主进程开始执行.
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);
  this_thread::sleep_for(chrono::nanoseconds(1));
  //this_thread::yield;
  //sleep(1);
  return 0;
}

结果 :

root@LoongOS:test# ./test_thread
xxx------->main(), line = 70
xxx------->task_01(), line = 57

<3>.使用this_thread::yield()延时

#include <iostream>
#include <thread>
#include <unistd.h> 
using namespace std;
 
void task_01(int i){  
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);  
  while(i < 1000)
    i++;
  printf("xxx------->%s(), line = %d, i = %d\n",__FUNCTION__,__LINE__,i);
}
int main(){
  int i = 0, j = 0;

  //1.任务线程.
  thread t1(task_01, i);
  t1.detach();//线程与进程分离,各自执行,顺序是混换的.
  
  //t1.join();//主进程等待线程执行结束,然后主进程开始执行.
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);
  //this_thread::sleep_for(chrono::nanoseconds(1));
  this_thread::yield();
  //sleep(1);
  return 0;
}

结果:

root@LoongOS:test# ./test_thread
xxx------->main(), line = 70

<4>.使用usleep延时

#include <iostream>
#include <thread>
#include <unistd.h> 
using namespace std;
 
void task_01(int i){  
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);  
  while(i < 1000)
    i++;
  printf("xxx------->%s(), line = %d, i = %d\n",__FUNCTION__,__LINE__,i);
}
int main(){
  int i = 0, j = 0;

  //1.任务线程.
  thread t1(task_01, i);
  t1.detach();//线程与进程分离,各自执行,顺序是混换的.
  
  //t1.join();//主进程等待线程执行结束,然后主进程开始执行.
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);
  //this_thread::sleep_for(chrono::nanoseconds(1));
  //this_thread::yield();
  //sleep(1);
  usleep(100);//100us
  return 0;
}

结果:

root@LoongOS:test# ./test_thread
xxx------->main(), line = 70
xxx------->task_01(), line = 57

<5>.使用sleep延时

#include <iostream>
#include <thread>
#include <unistd.h> 
using namespace std;
 
void task_01(int i){  
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);  
  while(i < 1000)
    i++;
  printf("xxx------->%s(), line = %d, i = %d\n",__FUNCTION__,__LINE__,i);
}
int main(){
  int i = 0, j = 0;

  //1.任务线程.
  thread t1(task_01, i);
  t1.detach();//线程与进程分离,各自执行,顺序是混换的.
  
  //t1.join();//主进程等待线程执行结束,然后主进程开始执行.
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);
  //this_thread::sleep_for(chrono::nanoseconds(1));
  //this_thread::yield();
  sleep(1);
  //usleep(100);
  return 0;
}

结果:

root@LoongOS:test# ./test_thread
xxx------->main(), line = 70
xxx------->task_01(), line = 57
xxx------->task_01(), line = 60, i = 1000


void task_01(int i){  
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);  
  if(i < 1000){
    i++;
    this_thread::sleep_for(chrono::nanoseconds(1));
  }
  
  printf("xxx------->%s(), line = %d, i = %d\n",__FUNCTION__,__LINE__,i);
}

void main_thread(int j){
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);  
  while(j < 1000){
    //this_thread::yield();
    j++;    
  }
  printf("xxx------->%s(), line = %d, j = %d\n",__FUNCTION__,__LINE__,j);
}

int main(){
  int i = 0, j = 0;
  thread t1(task_01, i);
  main_thread(j);  
  t1.detach(); 
  //t1.join();
  usleep(100); 
  printf("xxx------->%s(), line = %d\n",__FUNCTION__,__LINE__);  
  return 0;
}

结果:

root@LoongOS:test# ./test_thread
xxx------->main_thread(), line = 90
xxx------->main_thread(), line = 95, j = 1000
xxx------->task_01(), line = 80
xxx------->main(), line = 105

总结

1.this_thread::sleep_for(chrono::nanoseconds(1))和this_thread::yield()是cpu自行调度,带有不确定性,跟系统有关.
2.sleep(1)和usleep(100)确实延时了,等待线程执行.


网站公告

今日签到

点亮在社区的每一天
去签到