当前位置: 首页 > article >正文

C++线程

目录

C与C++的线程用法区别

1. 线程创建与管理

2. 线程同步

3. 传递对象

4. 异常处理

5. 线程的生命周期管理

6. 跨平台性

总结

线程库

thread类的简单介绍

基本代码实现

函数指针

Lambda

线程id

线程函数参数

线程和容器

移动赋值

移动构造(move)

小知识点move

lambda局部的可执行对象     

锁 (不支持拷贝)

try_lock

死锁

lock_guard

unique_lock

条件变量

wait和notify_one

支持两个线程交替打印,一个打印奇数,一个打印偶数

原子性操作库(atomic)

shared_ptr多线程问题

单例模式的(懒汉模式具有线程安全问题)

​编辑懒汉单例模式最简单(C++11之前不是线程安全的,C++11之后的是安全的)


C与C++的线程用法区别

C和C++的线程用法区别可以从多个角度进行比较,包括线程创建与管理、线程同步、传递对象、异常处理等方面。以下是C与C++线程用法的全面总结:

1. 线程创建与管理

  • C:

    • C语言本身不支持线程,线程编程依赖外部库,如 POSIX Threads (pthreads) 或 Windows API
    • 使用 pthread_create() 创建线程,使用 pthread_join() 或 pthread_detach() 来管理线程。
    • 线程管理较为底层,编程时需要显式处理线程创建、同步、销毁等操作。

    示例(C语言,使用 pthread):

    #include <pthread.h>
    #include <stdio.h>
    
    void* thread_function(void* arg) {
        printf("Thread is running.\n");
        return NULL;
    }
    
    int main() {
        pthread_t thread;
        pthread_create(&thread, NULL, thread_function, NULL);
        pthread_join(thread, NULL);  // 等待线程结束
        return 0;
    }
    
  • C++:

    • C++11及以后版本引入了对线程的原生支持,提供了 std::thread 类来管理线程。
    • 线程创建更简单,线程对象会自动管理线程的生命周期。
    • 支持线程同步与线程池等高级功能。

    示例(C++语言,使用 std::thread):

    #include <iostream>
    #include <thread>
    
    void thread_function() {
        std::cout << "Thread is running." << std::endl;
    }
    
    int main() {
        std::thread t(thread_function);  // 创建线程
        t.join();  // 等待线程结束
        return 0;
    }
    

2. 线程同步

  • C:

    • 在C中,线程同步通常使用 pthread_mutex_tpthread_cond_t 等机制。
    • 使用互斥锁(mutex)、条件变量等实现线程之间的协调,避免数据竞争。

    示例(C语言,使用 pthread 互斥锁):

    #include <pthread.h>
    #include <stdio.h>
    
    pthread_mutex_t lock;
    
    void* thread_function(void* arg) {
        pthread_mutex_lock(&lock);  // 加锁
        printf("Thread is running.\n");
        pthread_mutex_unlock(&lock);  // 解锁
        return NULL;
    }
    
    int main() {
        pthread_t thread;
        pthread_mutex_init(&lock, NULL);
        pthread_create(&thread, NULL, thread_function, NULL);
        pthread_join(thread, NULL);
        pthread_mutex_destroy(&lock);
        return 0;
    }
    
  • C++:

    • C++11引入了更高层次的线程同步机制,包括 std::mutexstd::lock_guardstd::condition_variable 和 std::atomic
    • std::mutex 用于互斥锁,std::lock_guard 简化了锁的使用,自动管理锁的加锁与释放。
    • std::condition_variable 实现线程之间的通知和等待机制。

    示例(C++语言,使用 std::mutex):

    #include <iostream>
    #include <thread>
    #include <mutex>
    
    std::mutex mtx;
    
    void thread_function() {
        std::lock_guard<std::mutex> guard(mtx);  // 自动加锁
        std::cout << "Thread is running." << std::endl;
    }
    
    int main() {
        std::thread t(thread_function);
        t.join();
        return 0;
    }
    

3. 传递对象

  • C:

    • 在C中,线程函数通常接受 void* 类型的参数,通过指针传递数据或对象。
    • 需要手动管理指针的生命周期,确保线程结束后数据不会被意外修改。

    示例(C语言,传递结构体指针):

    #include <pthread.h>
    #include <stdio.h>
    
    typedef struct {
        int a;
        int b;
    } MyStruct;
    
    void* thread_function(void* arg) {
        MyStruct* data = (MyStruct*)arg;
        printf("a = %d, b = %d\n", data->a, data->b);
        return NULL;
    }
    
    int main() {
        pthread_t thread;
        MyStruct obj = {10, 20};
        pthread_create(&thread, NULL, thread_function, (void*)&obj);
        pthread_join(thread, NULL);
        return 0;
    }
    
  • C++:

    • C++支持通过值传递、引用传递以及使用智能指针(如 std::shared_ptr 和 std::unique_ptr)来传递对象。
    • 值传递会复制对象副本,而引用传递则允许多个线程共享同一个对象。
    • 智能指针可以自动管理内存,避免内存泄漏。

    示例(C++语言,使用 std::shared_ptr):

    #include <iostream>
    #include <thread>
    #include <memory>
    
    class MyClass {
    public:
        int a, b;
        MyClass(int x, int y) : a(x), b(y) {}
        void print() { std::cout << "a = " << a << ", b = " << b << std::endl; }
    };
    
    void thread_function(std::shared_ptr<MyClass> obj) {
        obj->print();
    }
    
    int main() {
        std::shared_ptr<MyClass> obj = std::make_shared<MyClass>(10, 20);
        std::thread t(thread_function, obj);  // 传递智能指针
        t.join();
        return 0;
    }
    

4. 异常处理

  • C:

    • C语言没有内建的异常处理机制。错误处理通常通过返回值、全局变量或 setjmp 和 longjmp 来实现。
    • 在多线程中,异常往往无法传播到其他线程,开发者必须在每个线程内部处理异常。
  • C++:

    • C++11支持异常处理,可以在线程函数内部使用 try-catch 语句来捕获和处理异常。
    • std::thread 类中的线程异常不会自动传播,需要显式地捕获异常。

    示例(C++语言,使用异常处理):

    #include <iostream>
    #include <thread>
    
    void thread_function() {
        try {
            throw std::runtime_error("Thread error");
        } catch (const std::exception& e) {
            std::cout << "Caught exception: " << e.what() << std::endl;
        }
    }
    
    int main() {
        std::thread t(thread_function);
        t.join();
        return 0;
    }
    

5. 线程的生命周期管理

  • C:

    • C语言的线程生命周期需要手动管理,线程的结束需要使用 pthread_join() 或 pthread_detach() 来显式等待线程完成或分离线程。
  • C++:

    • C++中的 std::thread 在其对象超出作用域时会自动销毁。如果线程对象没有显式地调用 join() 或 detach(),则会抛出异常(std::terminate())。

    示例(C++语言,自动销毁线程):

    #include <iostream>
    #include <thread>
    
    void thread_function() {
        std::cout << "Thread is running." << std::endl;
    }
    
    int main() {
        std::thread t(thread_function);
        t.join();  // 等待线程结束
        return 0;  // t超出作用域后自动销毁
    }
    

6. 跨平台性

  • C:

    • C中的线程库通常依赖于平台特定的API(如POSIX或Windows API),这意味着跨平台开发需要考虑不同平台的线程库和同步机制。
  • C++:

    • C++11及以后版本提供了标准化的线程库 std::thread 和同步机制,可以在多种平台上使用(如Windows、Linux、macOS等),具有较好的跨平台性。

总结

特性C语言C++语言
线程库依赖外部库(pthreads、Windows API)原生支持(std::thread
线程创建使用 pthread_create使用 std::thread
线程同步使用 pthread_mutexpthread_cond 等使用 std::mutexstd::lock_guardstd::atomic 等
传递对象通过指针传递(手动管理内存)通过值、引用、智能指针传递(更灵活、安全)
异常处理没有内建异常处理机制支持 try-catch 语句,异常可以在线程中捕获
线程生命周期管理手动管理,使用 pthread_join 或 pthread_detach自动销毁,未 join() 或 detach() 会调用 std::terminate
跨平台性需要依赖平台特定的线程库跨平台支持更好,使用标准线程库

C++提供了更高级的线程管理机制,使得多线程编程更加简洁、安全和高效,而C则需要更多手动管理。

线程库

thread类的简单介绍

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在 并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。thread文档

函数名功能
thread()构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn, args1, args2, ...)构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的 参数
get_id()获取线程id
jionable()线程是否还在执行,joinable代表的是一个正在执行中的线程。
jion()该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach()在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离 的线程变为后台线程,创建的线程的"死活"就与主线程无关

Example

// thread example
#include <iostream>       // std::cout
#include <thread>         // std::thread
 
void foo() 
{
  // do stuff...
}

void bar(int x)
{
  // do stuff...
}

int main() 
{
  std::thread first (foo);     // spawn new thread that calls foo()
  std::thread second (bar,0);  // spawn new thread that calls bar(0)

  std::cout << "main, foo and bar now execute concurrently...\n";

  // synchronize threads:
  first.join();                // pauses until first finishes
  second.join();               // pauses until second finishes

  std::cout << "foo and bar completed.\n";

  return 0;

注意: 

1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态

2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。

#include <thread>

int main()
{
 std::thread t1;
 cout << t1.get_id() << endl;
 return 0;
}

get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中 包含了一个结构体:

3. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。 线程函数一般情况下可按照以下三种方式提供:(C与C++的区别,C++传的对象更多了 )

  • 函数指针
  • lambda表达式
  • 函数对象

4. thread类是防拷贝的(不支持拷贝赋值和拷贝构造,支持移动复制和移动构造),不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。

5. 可以通过joinable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效

  • 采用无参构造函数构造的线程对象
  • 线程对象的状态已经转移给其他线程对象
  • 线程已经调用jion或者detach结束

基本代码实现

函数指针
void func(int x) 
{
	for (int i = 0; i < x; i++)
	{
		cout << i << endl;
	}
}

int main() {
	thread t1(func, 3);  // 调用 int 版本

	//不要忘了等待
	t1.join();
	return 0;
}

这里要注意传重载函数时编译器无法确定,如果遇到这种情况,通常会出现编译错误。解决办法就是最好选择用Lambda 。

Lambda
void func(int x) 
{
	for (int i = 0; i < x; i++)
	{
		cout << i << endl;
	}
}
void func(int x, const string &s)
{
	for (int i = 0; i < x; i++)
	{
		cout << i << endl << s << endl;
	}
}

int main() {
	thread t1([]() 
		{
			func(1);
		}
	);
	thread t2([]() 
		{
			func(2,"ljw");
		}
	);

	//不要忘了等待
	t1.join();
	t2.join();
	return 0;
}

线程id

线程id

 std::thread::get_id(获取线程id)

std::thread::get_id

int main() {
	thread t1([]() 
		{
			func(1);
		}
	);
	cout << "t1->" << t1.get_id() << endl;
	thread t2([]() 
		{
			func(2,"ljw");
		}
	);
	cout << "t2->" << t2.get_id() << endl;

	//不要忘了等待
	t1.join();
	t2.join();
	return 0;
}

std::this_thread::get_id(在线程里面获取自己的线程)

std::this_thread::get_id

void func(int x) 
{
	for (int i = 0; i < x; i++)
	{
		cout << i << endl;
		cout << this_thread::get_id() << endl;
	}
}
void func(int x, const string &s)
{
	for (int i = 0; i < x; i++)
	{
		cout << i << endl << s << endl;
		cout << this_thread::get_id() << endl;
	}
}

线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在 线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。(用&接收,最好就要把ref加上)

错误样例

在线程函数中对x修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝

void func(int& x)
{
	x++;
}

int main()
{
	int x = 1;
	thread t1(func, x);
	cout << x;

	//不要忘了等待
	t1.join();
	return 0;
}

解决方案1,如果想要通过形参改变外部实参时,必须借助std::ref()函数(用&接收,最好就要把ref加上)

void func(int& x)
{
	x++;
}
int main()
{
	int x = 1;
	thread t1(func, ref(x));

	//cout << x;//这里要注意要在join以后再打印,要不然main这个线程可能会运行的快于func
	t1.join();
	cout << x;

	return 0;
}

解决方案2,地址的拷贝

void func(int* x)
{
	*x += 1;
}
int main()
{
	int x = 1;
	thread t1(func, &x);

	//cout << x;//这里要注意要在join以后再打印,要不然main这个线程可能会运行的快于func
	t1.join();
	cout << x;

	return 0;
}

注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。

线程和容器

int main()
{
	size_t n;
	cin >> n;
	//创建n个线程执行Print
	vector<thread> vthd(n);

	return 0;
}

给n个值,第二个值,不给上图这里就是thread,相当于创建了n个线程对象,但没有执行 

不支持拷贝赋值和拷贝构造,支持移动赋值和移动构造

移动赋值

std::thread::operator=

下面用多线程代码展示移动赋值

思路:

  • 先创建几个空线程对象放在vector
  • 再创建匿名线程对象移动复制给thd,相当于放入vector
void func(int x)
{
	for (int i = 0; i < x; i++)
	{
		cout << i << endl;
	}
}
int main()
{
	size_t n;
	cin >> n;
	//创建n个线程执行func
	vector<thread> vthd(n);
	for (auto& thd : vthd)
	{
        //移动赋值
		thd = thread(func, 2);
	}
	for (auto& thd : vthd)
	{
		thd.join();
	}

	return 0;
}

移动构造(move)

    thread t1(func, 3);
    thread t2(move(t1));
小知识点move

在C++中,std::move 是一个用于将对象从左值转化为右值的标准库函数。它不会移动对象本身,而是通过转换对象的类型来启用移动语义。

主要用途:

  1. 启用移动语义: std::move 使得对象能够通过移动而不是复制的方式传递给函数或赋值给其他对象。它本质上是一个类型转换工具,转化一个对象为右值引用。

  2. 提高性能: 使用移动语义可以避免不必要的复制,特别是当对象非常大或昂贵时,比如在容器操作中,std::vectorstd::string 等类的数据转移。

语法:

std::move(x)

  • 这里 x 是一个左值(普通变量或对象)。

  • std::move(x) 会将 x 转换为一个右值引用(T&&),即允许资源被“移动”而不是被复制。

示例代码:

#include <iostream>
#include <vector>
#include <utility>  // for std::move

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass Constructor\n";
    }
    MyClass(const MyClass& other) {
        std::cout << "Copy Constructor\n";
    }
    MyClass(MyClass&& other) noexcept {
        std::cout << "Move Constructor\n";
    }
};

int main() {
    MyClass obj1;
    MyClass obj2 = std::move(obj1);  // Move constructor called
    
    std::vector<int> v1 = {1, 2, 3, 4};
    std::vector<int> v2 = std::move(v1);  // Move the contents of v1 into v2
    std::cout << "v1 size: " << v1.size() << "\n";  // v1 is now empty, valid but unspecified state
    std::cout << "v2 size: " << v2.size() << "\n";  // v2 contains the data
}

注意事项:

  1. 资源管理: 使用 std::move 后,原对象进入“已移交”状态,不能再被访问或操作。调用 std::move 不会对对象做任何改变,它只是通过类型转换来触发移动语义。

  2. 函数返回值优化: std::move 常用于返回对象时启用返回值优化(RVO)或者移动语义,使得返回值的复制被替换为移动。

  3. 与 std::forward 的关系: std::forward 用于在完美转发中保持对象的左值或右值属性,而 std::move 总是将对象转化为右值。

总结:

std::move 是一个用于启用移动语义的工具,它允许对象的资源从一个位置“移动”到另一个位置而不是复制,从而提高程序的性能,特别是对于大型对象或容器。

lambda局部的可执行对象     

int main() 
{
	size_t n1 = 10000;
	size_t n2 = 10000;
	int x = 0;
	cout << n1 << "+" << n2 << endl;
	thread t1([&]()
		{
			for (int i = 0; i < n1; i++)
			{
				x++;
			}	
		}
	);
	thread t2([&]()
		{
			for (int i = 0; i < n2; i++)
			{
				x++;
			}
		}
	);
	t1.join();
	t2.join();
	cout << x;
	return 0;
}

上述代码有线程安全的问题 ,有可能两个线程会同时++x,解决办法用锁

锁 (不支持拷贝)

mutex的种类

在C++11中,Mutex总共包了四个互斥量的种类:

1. std::mutex C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:

函数名 函数功能
lock()上锁:锁住互斥量
unlock()解锁:释放对互斥量的所有权
try_lock()尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞

注意,线程函数调用lock()时,可能会发生以下三种情况:

  • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前, 该线程一直拥有该锁
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

线程函数调用try_lock()时,可能会发生以下三种情况:

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)  

2. std::recursive_mutex

其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权, 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,

std::recursive_mutex 的特性和 std::mutex 大致相同。

3. std::timed_mutex

比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。

  • try_lock_for()

接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超 时(即在指定时间内还是没有获得锁),则返回 false。

  • try_lock_until()

接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住, 如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指 定时间内还是没有获得锁),则返回 false。

4. std::recursive_timed_mutex

mutex普通锁

recursive_mutex

递归互斥锁(递归时就死锁了,实质是判断是不是当前线程,是当前线程就lock,不是就不lock)

timed_mutex

时间互斥锁

recursive_timed_mutex

递归时间互斥锁

mutex文档

也是一个类

int main() 
{
	size_t n1 = 10000;
	size_t n2 = 10000;
	int x = 0;
	mutex mtx;
	cout << n1 << "+" << n2 << endl;
	thread t1([&]()
		{
			for (int i = 0; i < n1; i++)
			{
				mtx.lock();
				x++;
				mtx.unlock();
			}	
		}
	);
	thread t2([&]()
		{
			for (int i = 0; i < n2; i++)
			{
				mtx.lock();
				x++;
				mtx.unlock();
			}
		}
	);
	t1.join();
	t2.join();
	cout << x;
	return 0;
}

不用lambda线程时传递注意用引用接收时并且要改变传过来的值时,要加ref

void func(size_t n, int& x,mutex& mtx)
{
	for (int i = 0; i < 1000; i++)
	{
		mtx.lock();
		x++;
		mtx.unlock();
	}
}

int main()
{
	int x = 0;
	mutex mtx;

	thread t1(func,10, ref(x), ref(mtx));
	thread t2(func, 100,ref(x), ref(mtx));

	t1.join();
	t2.join();
	cout << x;

	return 0;
}

左值引用这里都要加ref,100和" "里的都是右值(thread里的参数会传给thread的构造函数,然后因为其底层原因,这些所有的值传递过去的时候,都会变成类似的右值)

try_lock

std::mutex::try_lock文档

try_lock是不阻塞,lock是阻塞   

各种锁文档​​​​​​​

mutex普通锁

recursive_mutex

递归互斥锁(递归时就死锁了,实质是判断是不是当前线程,是当前线程就lock,不是就不lock)

timed_mutex

时间互斥锁

recursive_timed_mutex

递归时间互斥锁

0.3秒运行一次

死锁

如果抛异常了直接退出程序了但锁还没有解除,就形成了死锁

int main()
{
	mutex mtx;

	mtx.lock();

	//抛异常了

	mtx.unlock();

	return 0;
}

解决办法:lock_guard

​​​​​​​lock_guard文档

把锁交给类对象进行管理,进行资源的释放等等,避免抛异常出问题(死锁)        

模拟一个lock_guard

这里用模板是因为锁也有很多种锁,注意成员变量要用&

template<class Lock>
class LockGuard
{
public:
	LockGuard(Lock& lk)
		:_lk(lk)
	{
		_lk.lock();
	}
	~LockGuard()
	{
		_lk.unlock();
	}
private:
	Lock& _lk;
};

会产生死锁代码实例

void func()
{
	srand(time(0));
	if (rand() % 3)
	{
		throw exception("异常");
	}
	else
	{
		cout << "func" << endl;
	}
}
int main()
{
	mutex mtx;
	size_t n1 = 5;
	size_t n2 = 5;
	int x = 0;
	thread t1([&]()
	{
		try
		{
			for (int i = 0; i < n1; i++)
			{
				mtx.lock();
				//可能抛异常
				func();
				x++;
				mtx.unlock();
			}
		}
		catch (const exception& e)
		{
			cout << e.what() << endl;
		}
	}
	);
	return 0;
}

lock_guard

解决办法就是用std::lock_guard

lock_guard - C++ Reference (cplusplus.com)

unique_lock

unique_lock - C++ Reference (cplusplus.com)

两者区别,lock_guard只支持构造和析构         unique_lock支持手动解锁

代码示例 

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

std::mutex mtx;           // mutex for critical section

void print_block (int n, char c) {
  // critical section (exclusive access to std::cout signaled by lifetime of lck):
  std::unique_lock<std::mutex> lck (mtx);
  for (int i=0; i<n; ++i) { std::cout << c; }
  std::cout << '\n';
}

int main ()
{
  std::thread th1 (print_block,50,'*');
  std::thread th2 (print_block,50,'$');

  th1.join();
  th2.join();

  return 0;
}

条件变量

std::condition_variable

condition_variable - C++ Reference (cplusplus.com)

常用: 

waitcondition_variable::wait - C++ Reference (cplusplus.com)
notify_one(唤醒一个)condition_variable::notify_one - C++ Reference (cplusplus.com)
notify_all(唤醒所有)condition_variable::notify_all - C++ Reference (cplusplus.com)

wait和notify_one

这里注意要用unique_lock,所以只能用unqiue_lock,wait的时候会unlock

wait后会自动释放锁

 被唤醒的时候,被唤醒线程马上又会拿到lock

支持两个线程交替打印,一个打印奇数,一个打印偶数

两个线程t1和t2,两个线程一个线程打印一次,交替执行

如何保证t1先运行

分析一下这个代码,t2可能会一直等待,因为如果t1跑的特别快时,唤醒的时候,t2还没有开始等待,那么当t1跑完的时候,t2就会在一直等待,没人唤醒,从而让程序崩溃

正确写法

没有要唤醒的就什么也不做

int main()
{
	int x = 0;
	mutex mtx;
	condition_variable cv;
	int flag = false;
	//保证t1进程先运行
	thread t1(
		[&]()
		{
			for (int i=0;i<5;i++)
			{
				unique_lock<mutex> lck(mtx);
				//如果是false,++x,且保证t1先跳过循环
				while (flag) 
				{
					cv.wait(lck);//wait时会释放锁unlock
				};
				x++;
				cout << "t1->" << x << endl;
				flag = true;
				cv.notify_one();//如果没有等待notify_one那么就什么也不做
			}
		}
	);
	thread t2(
		[&]()
		{
			for (int i = 0; i < 5; i++)
			{
				unique_lock<mutex> lck(mtx);
				//保证t2后执行
				while (!flag) 
				{
					cv.wait(lck);
				};
				x++;
				cout << "t2->" << x << endl;
				flag = false;
				cv.notify_one();
			}
		}
	);
	t1.join();
	t2.join();
	return 0;
}

解释:首先他们会先竞争锁,如果t1拿到锁了(只有一个线程能拿到锁)那么t2就会在unique_lock这停下来,极端条件下,然后t1走了一个循环后,再拿到了锁,但flag是true要进行等待了,等待的时候会释放锁,然后t2就会拿到锁。

如果是t2先拿到了锁,会直接进入wait,然后t1执行,后面的就跟t1先拿到锁的情况一样了   

wait会解锁

思路

第一种,1在锁上面,2执行完了解锁,就会唤醒你

第二种,不在锁上面,获取到锁了,但在wait上面,wait上面会解锁,然后t2执行,再notify t1,然后把锁交给t1

第三种,t1的时间片到了,在}不动了, 然后t2执行完后一定会wait,然后释放锁,t1有时间片后,就可以拿到锁

原子性操作库(atomic)

atomic - C++ Reference (cplusplus.com)

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问 题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数 据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。比如:

这种情况用加锁,会很消耗资源,临界时间时间段不适合用互斥锁,线程会频繁阻塞

要用自旋锁(库里没有)用原子的,一般都是内置类型的

int main() 
{
	size_t n1 = 10000;
	size_t n2 = 10000;
	int x = 0;
	mutex mtx;
	cout << n1 << "+" << n2 << endl;
	thread t1([&]()
		{
			for (int i = 0; i < n1; i++)
			{
				mtx.lock();
				x++;
				mtx.unlock();
			}	
		}
	);
	thread t2([&]()
		{
			for (int i = 0; i < n2; i++)
			{
				mtx.lock();
				x++;
				mtx.unlock();
			}
		}
	);
	t1.join();
	t2.join();
	cout << x;
	return 0;
}

原子类型

int main() 
{
	size_t n1 = 10000;
	size_t n2 = 10000;
	mutex mtx;
	atomic<bool> flag = true;
	atomic<int> x = 0;
	cout << n1 << "+" << n2 << endl;
	thread t1([&]()
		{
			for (int i = 0; i < n1; i++)
			{
				x++;
				flag = false;
			}	
		}
	);
	thread t2([&]()
		{
			for (int i = 0; i < n2; i++)
			{
				x++;
				flag = false;
			}
		}
	);
	t1.join();
	t2.join();
	printf("%d\n", x);//错误打印方法
	cout << x;
	return 0;
}

这里要注意的是

printf("%d\n", x);//错误打印方法

shared_ptr多线程问题

 shared_ptr多线程问题

++的时候用原子的

只想保护*copy1,用{ }局部域

单例模式的(懒汉模式具有线程安全问题)

 同时调用这一步

懒汉单例模式最简单(C++11之前不是线程安全的,C++11之后的是安全的)

C++11把变成了原子操作

​​​​​​​

 


http://www.kler.cn/a/389462.html

相关文章:

  • Axure网络短剧APP端原型图,竖屏微剧视频模版40页
  • MySql结合element-plus pagination的分页查询
  • GitLab基于Drone搭建持续集成(CI/CD)
  • 如何为电子课程创造创意
  • 力扣 LeetCode 142. 环形链表II(Day2:链表)
  • 什么是两化融合
  • 【大数据学习 | kafka高级部分】kafka的快速读写
  • 道品科技水肥一体化在农业生产中的必要性与应用领域探讨
  • 微服务架构面试内容整理-消息驱动-RocketMQ
  • redis RDB持久化技术
  • mysql第二次作业---单表和多表查询
  • Rust性能优化与调试之性能基准测试
  • 如何使用SparkSQL在hive中使用Spark的引擎计算
  • 全网最详细的自动化测试(Jenkins 篇)
  • 企业知识库管理系统的创新模式及其智能化转型
  • 45期代码随想录算法营总结
  • 股票短线的建议(学习)- 20241111
  • .net core开发windows程序在国产麒麟操作系统中运行
  • catchadmin-webman 宝塔 部署
  • web——[SUCTF 2019]EasySQL1——堆叠注入
  • Oracle OCP认证考试考点详解082系列18
  • HTTP协议基础
  • 【报错记录】Steam迁移(移动)游戏报:移动以下应用的内容失败:XXX: 磁盘写入错误
  • 时序预测 | gamma伽马模型锂电池寿命预测 EM算法粒子滤波算法结合参数估计
  • 健身房业务流程优化:SpringBoot解决方案
  • Python面试题一