C++11多线程编程

线程库的基本使用

一个简单的多线程程序

#include <iostream>
#include <thread>

void printHelloWorld()
{
	std::cout << "Hello World" << std::endl;
}

int main(int argc, char *argv[])
{
	// 创建线程
	std::thread test(printHelloWorld);

	return 0;
}

image
会报错:因为主函数运行完了,但是线程还在执行打印

主程序等待线程执行完毕 join()

join():主程序不会直接结束,它会检查线程有没有结束,如果线程不结束那主程序也不结束。

#include <iostream>
#include <thread>

void printHelloWorld()
{
	std::cout << "Hello World" << std::endl;
	return ;
}

int main(int argc, char *argv[])
{
	std::thread Thread1(printHelloWorld);
	Thread1.join();

	return 0;
}

image
带参数写法

#include <iostream>
#include <thread>
#include <string>

void printHelloWorld(std::string msg)
{
	std::cout << msg << std::endl;
	return ;
}

int main(int argc, char *argv[])
{
	std::thread Thread1(printHelloWorld, "Hello Thread");
	Thread1.join();

	return 0;
}

image

分离线程 detach()

分离主线程和子线程,主程序可以结束,子线程在后台持续运行。

#include <iostream>
#include <thread>
#include <string>

void printHelloWorld(std::string msg)
{
	std::cout << msg << std::endl;
	return ;
}

int main(int argc, char *argv[])
{
	std::thread Thread1(printHelloWorld, "Hello Thread");
	Thread1.detach();

	return 0;
}

image

joinable()

判断这个线程可不可以调用 join() 或者 detach()

#include <iostream>
#include <thread>
#include <string>

void printHelloWorld(std::string msg)
{
	std::cout << msg << std::endl;
	return ;
}

int main(int argc, char *argv[])
{
	std::thread Thread1(printHelloWorld, "Hello Thread");
	bool IsJoin = Thread1.joinable();
	if (IsJoin)
	{
		Thread1.join();
	}

	return 0;
}

image

join 是阻塞的

#include <iostream>
#include <thread>
#include <string>

void printHelloWorld(std::string msg)
{
	for (int i = 0; i < 10000; ++i)
	{
		std::cout << i << std::endl;
	}
	return ;
}

int main(int argc, char *argv[])
{
	std::thread Thread1(printHelloWorld, "Hello Thread");
	bool IsJoin = Thread1.joinable();
	
	if (IsJoin)
	{
		Thread1.join();
	}
	std::cout << "Over!" << std::endl;

	return 0;
}

image

线程函数中的数据未定义错误

#include <iostream>
#include <thread>
#include <string>

void foo(int& x)
{
	x += 1;
}

int main(int argc, char *argv[])
{
	std::thread t(foo, 1); 
	t.join();

	return 0;
}

image

std::ref 传递引用类型

#include <iostream>
#include <thread>
#include <string>

void foo(int& x)
{
	x += 1;
}

int main(int argc, char *argv[])
{
	int a = 1;
	std::thread t(foo, std::ref(a));
	t.join();
	std::cout << a << std::endl; // 输出 2
	return 0;
}

传递指针或引用局部变量的问题

#include <iostream>
#include <thread>
#include <string>

std::thread t;

void foo(int& x)
{
	x += 1;
}

void test()
{
	int a = 1;
	t = std::thread(foo, std::ref(a));
}

int main(int argc, char *argv[])
{
	test();
	t.join();
	return 0;
}

编译没问题但是运行会出错,因为 a 是局部变量,他在执行完 test() 之后就会释放掉,而线程还会引用但是 a 的地址已经失效了。
解决:可以放到外面

#include <iostream>
#include <thread>
#include <string>

std::thread t;
int a = 1;

void foo(int& x)
{
	x += 1;
}

void test()
{
	t = std::thread(foo, std::ref(a));
}

int main(int argc, char *argv[])
{
	test();
	t.join();
	std::cout << a << std::endl; // 输出 2
	return 0;
}

传递指针或引用指向已释放的内存的问题

#include <iostream>
#include <thread>
#include <string>

std::thread t;

void foo(int *x)
{
	std::cout << *x << std::endl;
}

int main(int argc, char *argv[])
{
	int* ptr = new int(1);
	std::thread t(foo, ptr);
	delete ptr;

	t.join();
	return 0;
}

image
先把指向1的地址传给了线程,然后那块内存被释放掉了,但是那个地址已经传过去了,所以线程取出来的值是不确定的因为已经释放过了。

类成员函数作为入口函数,类对象被提前释放

#include <iostream>
#include <thread>
#include <string>

class MyClass
{
public:
	void func()
	{
		std::cout << "Thread " << std::this_thread::get_id() << " started" << std::endl;
		// do some work
		std::cout << "Thread " << std::this_thread::get_id() << " finished" << std::endl;
	}
};

int main(int argc, char *argv[])
{
	MyClass obj;
	std::thread t(&MyClass::func, &obj);
	// obj 被提前销毁了,会导致未定义的行为

	return 0;
}

image
使用智能指针来修复

#include <iostream>
#include <thread>
#include <string>
#include <memory>

class MyClass
{
public:
	void func()
	{
		std::cout << "Thread " << std::this_thread::get_id() << " started" << std::endl;
		// do some work
		std::cout << "Thread " << std::this_thread::get_id() << " finished" << std::endl;
	}
};

int main(int argc, char *argv[])
{
	std::shared_ptr<MyClass> obj = std::make_shared<MyClass>();
	std::thread t(&MyClass::func, obj);
	t.join();

	return 0;
}

image

入口函数为类的私有成员函数(使用友元)

#include <iostream>
#include <thread>
#include <string>
#include <memory>

class MyClass
{
private:
	friend void Thread_func();
	void func()
	{
		std::cout << "Thread " << std::this_thread::get_id() << " started" << std::endl;
		// do some work
		std::cout << "Thread " << std::this_thread::get_id() << " finished" << std::endl;
	}
};

void Thread_func()
{
	std::shared_ptr<MyClass> obj = std::make_shared<MyClass>();
	std::thread t(&MyClass::func, obj);
	t.join();
}

int main(int argc, char *argv[])
{
	Thread_func();

	return 0;
}

image

互斥量解决多线程数据共享问题

#include <iostream>
#include <thread>
#include <string>
#include <memory>

int a = 0;

void func()
{
	for (int i = 0; i < 10000; ++i)
	{
		a += 1;
	}
}

int main(int argc, char *argv[])
{
	std::thread t1(func);
	std::thread t2(func);

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

	std::cout << a << std::endl;

	return 0;
}

image

因为可能同时拿到了 a, +1 会重复,所以不一定会是多少。
避免:当一个线程拿到了之后,不允许别的线程去拿。使用互斥锁

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

int a = 0;
std::mutex mtx;

void func()
{
	for (int i = 0; i < 10000; ++i)
	{
		mtx.lock();
		a += 1;
		mtx.unlock();
	}
}

int main(int argc, char *argv[])
{
	std::thread t1(func);
	std::thread t2(func);

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

	std::cout << a << std::endl;

	return 0;
}

image

线程安全定义:如果多线程程序每一次的运行结果和单线程运行的结果始终是一样的那么你的线程就是安全的

互斥量死锁

代码示例:

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

std::mutex mtx1, mtx2;

void func_1()
{
	mtx1.lock();
	mtx2.lock();
	mtx1.unlock();
	mtx2.unlock();
}

void func_2()
{
	mtx2.lock();
	mtx1.lock();
	mtx1.unlock();
	mtx2.unlock();
}

int main(int argc, char *argv[])
{
	std::thread t1(func_1);
	std::thread t2(func_2);

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

	return 0;
}

image
可能会一直卡住,因为两个线程可能会互相等待释放,进入死锁
解决方法:两边以相同顺序进行

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

std::mutex mtx1, mtx2;

void func_1()
{
	mtx1.lock();
	mtx2.lock();
	mtx1.unlock();
	mtx2.unlock();
}

void func_2()
{
	mtx1.lock();
	mtx2.lock();
	mtx1.unlock();
	mtx2.unlock();
}

int main(int argc, char *argv[])
{
	std::thread t1(func_1);
	std::thread t2(func_2);

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

	return 0;
}

image

lock_guard 与 unique_lock

lock_guard

std::lock_guard 是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题。
std::lock_guard 的特点如下:

  • 当构造函数被调用时,该互斥量会被自动锁定
  • 当析构函数被调用时,该互斥量会被自动解锁
  • std::lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用
#include <iostream>
#include <thread>
#include <mutex>

int shared_data = 0;

std::mutex mtx;

void func()
{
	for (int i = 0; i < 10000; ++i)
	{
		std::lock_guard<std::mutex> lg(mtx);
		++shared_data;
	}
}

int main(int argc, char *argv[])
{
	std::thread t1(func);
	std::thread t2(func);

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

	std::cout << shared_data << std::endl;

	return 0;
}

std::unique_lock

std::unique_lock 是 C++ 标准库中提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。

std::unique_lock 提供了以下几个成员函数:

  • lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。
  • try_lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回 false,否则返回 true。
  • try_lock_for(const std::chrono::duration<Rep, Period>& rel_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。
  • try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。
  • unlock():对互斥量进行解锁操作。

除了上述成员函数外,std::unique_lock 还提供了以下几个构造函数:

  • unique_lock() noexcept = default:默认构造函数,创建一个未关联任何互斥量的 std::unique_lock 对象。

  • explicit unique_lock(mutex_type& m):构造函数,使用给定的互斥量 m 进行初始化,并对该互斥量进行加锁操作。

  • unique_lock(mutex_type& m, defer_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,但不对该互斥量进行加锁操作。

  • unique_lock(mutex_type& m, try_to_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的 std::unique_lock 对象不与任何互斥量关联。

  • unique_lock(mutex_type& m, adopt_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并假设该互斥量已经被当前线程成功加锁。

try_lock_for 示例

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

int shared_data = 0;

std::timed_mutex mtx;

void func()
{
	for (int i = 0; i < 2; ++i)
	{
		std::unique_lock<std::timed_mutex> lg(mtx, std::defer_lock);
		if (lg.try_lock_for(std::chrono::seconds(2)))
		{
			std::this_thread::sleep_for(std::chrono::seconds(1));
			++shared_data;
		}
	}
}

int main(int argc, char *argv[])
{
	std::thread t1(func);
	std::thread t2(func);

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

	std::cout << shared_data << std::endl;

	return 0;
}

image

call_once 与其使用场景

单例设计模式是一种常见的设计模式,用于确保某个类只能创建一个实例。由于单例模式实例是全局唯一的,因此在多线程环境中使用单例模式时,需要考虑线程安全的问题。

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

class Log
{
public:
	Log() { }
	Log(const Log &log) = delete;
	Log& operator = (const Log& log) = delete;

	static Log& GetInstance()
	{
		static Log * log = nullptr;
		if (!log)
		{
			log = new Log;
		}
		return *log;
	}

	void PrintLog(std::string msg)
	{
		std::cout << __TIME__ << ' ' << msg << std::endl;
	}
};

int main(int argc, char *argv[])
{
	Log::GetInstance().PrintLog("error");

	return 0;
}

call_once 只能在多线程中使用
std::call_once 是 C++11 标准库中的一个函数,用于确保某个函数只会被调用一次。

class Singleton
{
public:
	static Singleton& getInstance()
	{
		std::call_once(m_onceFlag, &Singleton::init);
		return *m_instance;
	}

	void setData(int data)
	{
		m_data = data;
	}

	int getData() const
	{
		return m_data;
	}
private:
	Singleton() { }
	Singleton(const Singleton&) = delete;
	Singleton& operator = (const Singleton&) = delete;
	static void init()
	{
		m_instance.reset(new Singleton);
	}
	static std::unique_ptr<Singleton> m_instance;
	static std::once_flag m_onceFlag;
	int m_data = 0;
};

std::unique_ptr<Singleton> Singleton::m_instance;
std::once_flag Singleton::m_onceFlag;

condition_variable 与其使用场景

posted @ 2023-09-22 09:20  HuiPuKui  阅读(49)  评论(0)    收藏  举报