定义
线程是一种轻量级的执行单元,它可以独立运行并与其他线程并发执行。通过创建多个线程,可以实现并行计算、异步任务处理和提高程序的响应性。
多线程
std::thread
类, 位于 <thread>
头文件,实现了线程操作。std::thread
可以和普通函数和 lambda 表达式搭配使用。它还允许向线程的执行函数传递任意多参数。
void func()
{
// do some work
}
int main()
{
std::thread t(func);
t.join();
return 0;
}
上面的例子中,t
是一个线程实例,函数 func()
在该线程运行。调用 join()
函数是为了阻塞当前线程(此处即主线程),直到 t
线程执行完毕。线程函数的返回值都会被忽略,但线程函数接受任意数目的输入参数。
void func(int i, double d, const std::string& s)
{
std::cout << i << ", " << d << ", " << s << std::endl;
}
int main()
{
std::thread t(func, 1, 12.50, "sample");
t.join();
return 0;
}
}
虽然可以向线程函数传递任意多参数,但都必须以值传递。如果需以引用传递,则必须以 std::ref
或 std::cref
封装,如下例所示:
void func(int& a)
{
a ;
}
int main()
{
int a = 42;
std::thread t(func, std::ref(a));
t.join();
std::cout << a << std::endl;
return 0;
}
这个程序会打印43,但如果不用 std::ref
封装,则输出会是42。
除了 join
函数,这个类还提供更多的操作:
swap
:交换两个线程实例的句柄detach
:允许一个线程继续独立于线程实例运行;detach 过的线程不可以再 join
int main()
{
std::thread t(funct);
t.detach();
return 0;
}
一个重要的知识点是,如果一个线程函数抛出异常,并不会被常规的 try-catch
方法捕获。也就是说,下面的写法是不会奏效的:
try
{
std::thread t1(func);
std::thread t2(func);
t1.join();
t2.join();
}
catch(const std::exception& ex)
{
std::cout << ex.what() << std::endl;
}
要追踪线程间的异常,你可以在线程函数内捕获,暂时存储在一个稍后可以访问的结构内。
代码语言:txt复制std::mutex g_mutex;
std::vector<std::exception_ptr> g_exceptions;
void throw_function()
{
throw std::exception("something wrong happened");
}
void func()
{
try
{
throw_function();
}
catch(...)
{
std::lock_guard<std::mutex> lock(g_mutex);
g_exceptions.push_back(std::current_exception());
}
}
int main()
{
g_exceptions.clear();
std::thread t(func);
t.join();
for(auto& e : g_exceptions)
{
try
{
if(e != nullptr)
{
std::rethrow_exception(e);
}
}
catch(const std::exception& e)
{
std::cout << e.what() << std::endl;
}
}
return 0;
}
关于捕获和处理异常,更深入的信息可以参看 Handling C exceptions thrown from worker thread in the main thread 和 How can I propagate exceptions between threads? 。
此外,值得注意的是, 头文件还在 std::this_thread
命名空间下提供了一些辅助函数:
- get_id: 返回当前线程的 id
- yield: 告知调度器运行其他线程,可用于当前处于繁忙的等待状态
- sleep_for:给定时长,阻塞当前线程
- sleep_until:阻塞当前线程至给定时间点
创建线程:
使用std::thread
类创建新的线程,需要传入一个可调用对象(函数指针、函数对象、Lambda 表达式等)作为线程的执行体。
- 线程的创建是相对简单的,可以利用多线程来实现并行计算、异步任务处理等。
- 在创建线程时,要考虑线程的启动开销和资源的分配情况。
线程管理:
通过std::thread
类提供的成员函数进行线程的管理,如等待线程结束、分离线程等。
- 使用
std::thread::join()
函数在主线程中等待子线程执行结束。 - 使用
std::thread::detach()
函数将线程与主线程分离,让其在后台执行。
线程间共享数据和同步:
在线程之间共享数据时,需要注意线程安全和同步机制。
- 可以使用互斥锁、条件变量等同步机制来保护共享数据的访问,避免竞态条件和数据竞争。
- 合理使用同步机制可以确保线程间的数据一致性和协调性。
异常处理:
在多线程环境下,线程中抛出的异常无法被主线程捕获,需要使用std::promise
和std::future
等机制来传递异常信息。
- 合理处理线程中的异常,保证程序的稳定性和可靠性。
性能考虑:
多线程编程可以提高程序的性能和效率,但也需要考虑线程的开销、资源竞争和线程安全等问题。
- 合理控制线程的数量,避免过多的线程引起的资源竞争和上下文切换开销。
- 选择合适的同步机制,避免过度的锁竞争和阻塞。
设计并发算法:
在设计并发算法时,需要考虑线程之间的通信、同步和负载均衡等问题。
- 使用合适的数据结构和算法,减少线程之间的竞争和锁冲突。
- 利用任务分解和并行计算等技术,提高并发性能和效率。
C 11中的线程库为我们提供了方便且强大的多线程编程能力,可以实现并发和并行的程序设计。在使用线程时,我们需要要考虑线程安全、同步机制和性能优化等方面的问题,确保程序的正确性、可靠性和高效性。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!