SFML2.6 系统模块--线程

什么是线程?

大多数人应该已经知道什么是线程,但是对于那些对这个概念非常新的人,这里有一个简短的解释。

线程基本上是一系列指令,可以与其他线程并行运行。每个程序至少由一个线程组成:主线程,它运行您的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实例,以便它在线程函数应该运行的时间内正常运转。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值