什么是线程?
大多数人应该已经知道什么是线程,但是对于那些对这个概念非常新的人,这里有一个简短的解释。
线程基本上是一系列指令,可以与其他线程并行运行。每个程序至少由一个线程组成:主线程,它运行您的main()函数。仅使用主线程的程序是单线程的,如果添加了一个或多个线程,则它们变成了多线程的。
因此,简而言之,线程是一种同时执行多个任务的方法。例如,在加载图像或声音时,显示动画并响应用户输入会非常有用。线程还广泛用于网络编程,在等待接收数据的同时继续更新和绘制应用程序。
使用SFML线程类还是std::thread?
在其最新版本(2011年),C++标准库提供了一组用于线程的类。在SFML编写时,C++11标准尚未编写,没有标准的创建线程的方法。当SFML 2.0发布时,仍有许多编译器不支持这个新标准。
如果您使用支持新标准及其标头的编译器,请忘记SFML线程类并使用它–这将更好。但是,如果您使用的是2011年之前的编译器,或者计划分发您的代码并希望它具有完全可移植性,则SFML线程类是一个不错的选择。
使用SFML创建线程
说的够多了,让我们看看一些代码。在SFML中创建线程的类是sf::Thread,下面是它的实际应用:
#include <SFML/System.hpp>
#include <iostream>
void func()
{
// this function is started when thread.launch() is called
for (int i = 0; i < 10; ++i)
std::cout << "I'm thread number one" << std::endl;
}
int main()
{
// create a thread with func() as entry point
sf::Thread thread(&func);
// run it
thread.launch();
// the main thread continues to run...
for (int i = 0; i < 10; ++i)
std::cout << "I'm the main thread" << std::endl;
return 0;
}
在这段代码中,在 thread.launch() 被调用之后,main 和 func 两个函数将同时运行。结果是,两个函数输出的文本将混合在控制台中。
线程的入口点,即在线程启动时将运行的函数,必须传递给 sf::Thread 的构造函数。sf::Thread 试图灵活地接受各种入口点:非成员或成员函数、带或不带参数、仿函数等等。上面的示例展示了如何使用一个非成员函数,这里是一些其他示例。
带一个参数的非成员函数:
void func(int x)
{
}
sf::Thread thread(&func, 5);
成员函数
class MyClass
{
public:
void func()
{
}
};
MyClass object;
sf::Thread thread(&MyClass::func, &object);
仿函数
struct MyFunctor
{
void operator()()
{
}
};
sf::Thread thread(MyFunctor());
最后一个使用函数对象的例子是最强大的,因为它可以接受任何类型的函数对象,因此使得 sf::Thread 兼容许多不直接支持的函数类型。这个特性在使用 C++11 lambda 表达式或 std::bind 时尤其有趣。
// with lambdas
sf::Thread thread([](){
std::cout << "I am in thread!" << std::endl;
});
// with std::bind
void func(std::string, int, double)
{
}
sf::Thread thread(std::bind(&func, "hello", 24, 0.5));
如果你想在类中使用 sf::Thread,请不要忘记它没有默认构造函数。因此,你必须直接在构造函数的初始化列表中进行初始化:
class ClassWithThread
{
public:
ClassWithThread()
: m_thread(&ClassWithThread::f, this)
{
}
private:
void f()
{
...
}
sf::Thread m_thread;
};
如果你确实需要在对象所有者的构造之后构建 sf::Thread 实例,你也可以通过动态分配堆上的内存来延迟其构建。
启动线程
创建sf::Thread实例后,必须使用launch函数启动它。
sf::Thread thread(&func);
thread.launch();
launch 调用你传递给构造函数的函数,并在新的线程中执行它,然后立即返回,以便调用线程可以继续运行。
停止线程
当一个线程的入口函数返回时,线程会自动停止。如果你想从另一个线程等待一个线程完成,你可以调用它的 wait 函数。
sf::Thread thread(&func);
// start the thread
thread.launch();
...
// block execution until the thread is finished
thread.wait();
wait 函数也会被 sf::Thread 的析构函数隐式调用,以便在线程所有者的 sf::Thread 实例被销毁后,线程不能保持活动状态(并且也不可控)。在管理线程时请记住这一点(请参见本教程的最后一节)。
暂停线程
sf::Thread 中没有允许另一个线程暂停它的函数,唯一暂停线程的方法是从它运行的代码中实现。换句话说,您只能暂停当前线程。要这样做,您可以调用 sf::sleep 函数:
void func()
{
...
sf::sleep(sf::milliseconds(10));
...
}
sf::sleep 函数有一个参数,即要休眠的时间。可以使用任何单位 / 精度指定此持续时间,如时间教程中所示。
请注意,您可以使用此函数使任何线程休眠,即使是主线程。
sf::sleep 是暂停线程的最有效方法:只要线程休眠,它就不需要使用 CPU。基于主动等待的暂停,如空 while 循环,会消耗 100% 的 CPU,仅仅是为了不做任何事情。然而,请记住,sleep 持续时间仅是提示,取决于操作系统,它的精度会更多或更少。所以不要依靠它进行非常精确的时间控制。
共享数据的保护
程序中的所有线程共享同一内存,它们可以访问它们所在作用域中的所有变量。这非常方便,但也很危险:由于线程是并行运行的,这意味着变量或函数可能同时从多个线程中使用。如果操作不是线程安全的,则可能导致未定义的行为(即可能崩溃或破坏数据)。
存在一些编程工具可帮助您保护共享数据并使您的代码线程安全,它们称为同步原语。常用的包括互斥锁、信号量、条件变量和自旋锁。它们都是相同概念的变体:它们通过仅允许某些线程访问它并阻止其他线程来保护一段代码。
最基本(也是最常用)的原语是互斥锁。互斥锁代表“互斥锁定”:它确保只有一个线程能够运行它所保护的代码。让我们看看它们如何使上面的例子有序:
#include <SFML/System.hpp>
#include <iostream>
sf::Mutex mutex;
void func()
{
mutex.lock();
for (int i = 0; i < 10; ++i)
std::cout << "I'm thread number one" << std::endl;
mutex.unlock();
}
int main()
{
sf::Thread thread(&func);
thread.launch();
mutex.lock();
for (int i = 0; i < 10; ++i)
std::cout << "I'm the main thread" << std::endl;
mutex.unlock();
return 0;
}
这段代码使用了共享资源(std::cout),正如我们所见,它产生了不想要的结果–所有内容都混杂在控制台上。为了确保完整的行被正确打印而不是随机混合,我们使用互斥锁来保护相应的代码区域。
第一个到达 mutex.lock() 行的线程成功锁定了互斥锁,直接获得了进入后面代码并打印其文本的访问权限。当另一个线程到达其 mutex.lock() 行时,互斥锁已经被锁定,因此线程被置于休眠状态(就像 sf::sleep 一样,在睡眠线程中不消耗 CPU 时间)。当第一个线程最终解锁互斥锁时,第二个线程被唤醒,并被允许锁定互斥锁并打印其文本块。这导致文本行按顺序连续出现在控制台上,而不是混合在一起。
互斥锁并不是你可以用来保护共享变量的唯一原语,但它应该足够满足大多数情况。然而,如果你的应用程序用线程做了复杂的事情,并且你感觉互斥锁不够用,那么请不要犹豫,寻找一个真正的线程库,其中更具有更多的功能。
互斥锁的保护
不用担心:互斥锁已经是线程安全的,没有必要再保护它们。但是它们不是异常安全的!如果在锁定互斥锁的同时抛出异常会发生什么?它永远不会有机会被解锁,并且永远保持锁定状态。所有尝试在之后锁定它的线程都将永久阻塞,有些情况下,整个应用程序可能会冻结。这是一个很糟糕的结果。
为了确保在可能抛出异常的环境中始终解锁互斥锁,SFML提供了一个RAII类来封装它们:sf::Lock。它在其构造函数中锁定互斥锁,并在其析构函数中解锁。简单而有效。
sf::Mutex mutex;
void func()
{
sf::Lock lock(mutex); // mutex.lock()
functionThatMightThrowAnException(); // mutex.unlock() if this function throws
} // mutex.unlock()
请注意,sf::Lock在具有多个返回语句的函数中也可能很有用。
sf::Mutex mutex;
bool func()
{
sf::Lock lock(mutex); // mutex.lock()
if (!image1.loadFromFile("..."))
return false; // mutex.unlock()
if (!image2.loadFromFile("..."))
return false; // mutex.unlock()
if (!image3.loadFromFile("..."))
return false; // mutex.unlock()
return true;
} // mutex.unlock()
常见错误
程序员经常忽视的一件事情是,一个线程不能在没有相应的sf::Thread实例的情况下运行。下面的代码在论坛上经常看到:
void startThread()
{
sf::Thread thread(&funcToRunInThread);
thread.launch();
}
int main()
{
startThread();
// ...
return 0;
}
编写此类代码的程序员期望startThread()函数启动一个能够自行运行并在线程函数结束时被销毁的线程。但实际上不是这样的。线程函数似乎会阻塞主线程,就好像线程没有工作一样。
这是什么原因呢?sf::Thread实例在startThread()函数中是局部变量,因此在函数返回时立即被销毁。sf::Thread的析构函数被调用,如我们上面所学的那样,调用wait(),结果是主线程被阻塞并等待线程函数完成,而不是继续并行运行。
因此,请不要忘记:必须管理好sf::Thread实例,以便它在线程函数应该运行的时间内正常运转。