C++并发编程之一 初识线程和线程管控

“你最熟悉的hello world”

在一个进程或者线程里面输出 "hello world"是怎么做的呢?

#include <iostream>
int main()
{
    std::cout << "Hello World!\n";
}

在该线程中起始函数为main函数,当输出完"hello world",该线程便终止了。

如果我们想另外开辟一个线程输出"hello world"该如何操作呢?在C++11中便已经封装好了管控线程的函数。
我们可以引入头文件,来启动线程。

#include <iostream>
#include <thread>
void hello() {
    std::cout << "hello world" << std::endl;
}
int main()
{
    std::cout << "I want start a new thread to print hello world." << std::endl;
    //创建子线程并传递入口函数
    std::thread t(hello);
    //调用join函数,阻塞当前主线程,直到子线程运行结束
    t.join();
    return 0;
}

在本例子中 t 作为 std::thread对象将函数hello()作为新线程的入口函数。std::thread对象的地址C++运行时系统动态分配的,通常在创建线程时自动分配。该地址指向新线程的控制结构,它包含线程的一些信息,比如线程id,状态,堆栈指针等等。我们看到 hello 作为一个参数传入到线程构造函数中,该参数通常是一个可调用对象(函数指针,lambda表达式或函数对象),'std::thread’对象将可调用对象的地址保存到新线程的控制结构里面,以便在运行的时候调用。

而’std::thread::join()'是一个成员函数,它用于等待该线程对象相关联的线程运行结束,调用 ‘join()’ 函数会阻塞当前线程的运行,直到该线程对象相关联的线程执行完成为止。
使用 ‘std::thread::join’ 的目的就是为了让主线程等待所有的子线程运行结束后再退出。
注意:一个线程对象只能调用一次 'std::thread::join()'函数,否则会触发 'std::system_error’异常。

线程管控(等待、分离、传递参数、移交线程归属权)

线程分离

上面讲到了 ‘std::thread::join()’ 函数阻塞主线程等待子线程执行结束,那如果用户不想等待呢?当然也有办法,那就是使用 ‘std::thread::detach()’ 函数使线程对象与其管理的执行线程相分离,将一个线程对象与其执行线程分离后,这个线程对象就不再与其所管理的执行线程相关联,它的执行状态将与执行线程独立。

这意味着线程的执行将会在后台继续进行,即使线程对象被销毁了,执行线程仍然可以继续执行。但我们在使用这个函数的时候,需要检验一下线程对象是否可连接(即线程对象和相关联的执行线程是否绑定),如果可连接,那么可以执行分离。但是如果不可连接,依然调用’std::thread::detach()',将导致程序终止。

线程对象在分离之后将无法重新连接到执行线程,因此需要确保不需要再次连接到执行线程。被分离出去的线程又可被称为守护线程(daemon thread)

#include <iostream>
#include <thread>

void hello() {
    std::cout << "hello world" << std::endl;
}
int main()
{
    std::cout << "I want start a new thread to print hello world." << std::endl;
    std::thread t(hello);
    if (t.joinable()) {
        t.detach();
        std::cout << "Thread is joinable\n";
    }
    else {
        std::cout << "Thread is not joinable\n";
    }
    std::cout << "Exiting main function\n";
    return 0;
}

注意: 当线程分离时,对于可调用对象中含有指针或引用,一定要谨慎操作,如果指针或引用指向了一个主线程的一个已摧毁变量,程序就会出问题。解决方法:可令线程完全自含,将数据复制到新线程的内部,而不是共享数据。

向线程函数传递参数

我们已经学会了创建线程、等待子线程、分离子线程,那么当我们新创建的线程需要参数运行时,我们该如何想向线程函数中传递参数呢?有如下几种方法可以传递。

  • 使用std::thread的构造函数
    std::thread类提供了多个构造函数,其中一个构造函数可以接受一个可调用对象和多个参数,可以使用这个构造函数来向线程中传递参数。
#include <iostream>
#include <thread>

void func(int arg1, int arg2)
{
    std::cout << "arg1 = " << arg1 << ", arg2 = " << arg2 << std::endl;
}

int main()
{
    int arg1 = 10;
    int arg2 = 20;

    std::thread t(func, arg1, arg2);

    t.join();
    return 0;
}

在这个示例中,调用 std::thread 的构造函数时,除了要指定线程函数 func 外,还将 arg1 和 arg2 作为构造函数的参数传递给了该线程。这样,在创建新线程时,这两个参数的值会被复制到新线程的私有存储空间中。因此,在新线程中修改这两个参数的值不会影响主线程中这两个参数的值,也不会影响其他线程对这两个参数的访问。

由于这里使用的是值传递,所以可以避免线程之间的竞争和共享数据的问题。但也需要注意,如果参数的拷贝开销很大,可能会影响程序的效率。如果需要在多个线程之间共享数据,则需要使用其他的线程通信机制来保证数据的同步和一致性。

  • 那么如果我想要引用传递,在新线程中更改参数也会影响到主线程中的参数呢?可以使用这样的方式:可以使用到 std::ref() 函数
#include <iostream>
#include <thread>

void func(int& a, int& b) {
	a++;
	b++;
	std::cout << "a is " << a << " , b is " << b << std::endl;
}

int main() {
	int argc1 = 10;
	int argc2 = 20;
	std::thread t(func, std::ref(argc1), std::ref(argc2));
	t.join();
	std::cout << "main thread argc1 is " << argc1 << " , argc2 is " << argc2 << std::endl;
	return 0;
}
  • 使用Lambda表达式
    Lambda表达式是C++11引入的一个新特性,可以用于创建匿名函数对象。使用Lambda表达式可以方便地向线程中传递参数。
#include <iostream>
#include <thread>

int main()
{
    int arg1 = 10;
    int arg2 = 20;

    std::thread t([&arg1, &arg2]() {
        std::cout << "arg1 = " << arg1 << ", arg2 = " << arg2 << std::endl;
    });

    t.join();
    return 0;
}

在上述示例代码中,使用Lambda表达式向线程t中传递参数arg1和arg2。
需要注意的是,如果要向线程中传递引用类型的参数,需要使用std::ref()函数将参数包装为一个引用包装器。例如,可以使用std::thread t(func, std::ref(arg1), std::ref(arg2))向线程中传递引用类型的参数。

  • 传递成员函数
    要在 std::thread 中使用成员函数,需要将成员函数作为线程函数,并将类对象的指针作为参数传递给 std::thread 的构造函数。
#include <iostream>
#include <thread>
#include <functional>

class MyClass
{
public:
    void func(int arg)
    {
        std::cout << "arg = " << arg << std::endl;
    }
};

int main()
{
    MyClass myObj;
    int arg = 10;

    std::thread t(&MyClass::func, &myObj, arg);

    t.join();

    return 0;
}

在这个示例中,通过&MyClass::func获取函数的地址,并将对象指针&myObj 和参数arg传递给了std::thread的构造函数中,由于成员函数func()需要类对象的成员变量和其他成员函数,所以需要将类对象的指针作为第一个参数传递给线程函数。

  • 参数移动而不能复制的传递参数
    使用 std::move 来传递只能移动但不能复制的对象。这种方式适用于需要将对象的所有权转移给新线程的情况,可以避免在多线程环境下进行复制或共享对象的操作。
#include <iostream>
#include <thread>
#include <string>

void func(std::string&& str)
{
    std::cout << "str = " << str << std::endl;
}

int main()
{
    std::string str = "hello";

    std::thread t(func, std::move(str));

    t.join();

    return 0;
}

在这个示例中,将字符串 str 作为右值引用传递给线程函数 func。由于右值引用表示的是对象的所有权,因此在传递参数时需要使用 std::move 来将对象的所有权转移给新线程。
需要注意的是,一旦对象的所有权被转移,原始的对象将不再可用,因此在使用 std::move 传递参数时需要小心。此外,如果多个线程同时访问同一个对象,可能会导致竞争条件的发生,需要采取适当的同步措施来避免这个问题。

移交线程归属权

  • 首先可以使用std::thread::swap()函数进行交换线程所有权
#include <iostream>
#include <thread>

void my_thread(int n) {
    std::cout << "Thread " << n << " is running." << std::endl;
}

int main() {
    std::thread t1(my_thread, 1);
    std::thread t2;

    // 交换两个线程的内部状态,使得t2获得了t1的执行函数和其他内部状态
    t1.swap(t2);

    // 等待t2执行完成
    t2.join();

    std::cout << "Thread 1 has been moved to t2." << std::endl;

    return 0;
}
  • 还有一种方法可以移交线程所有权,就是使用std::move()
#include <iostream>
#include <thread>

void func()
{
    std::cout << "Thread running" << std::endl;
}

int main()
{
    std::thread t(func);

    // 移交线程所有权给t2
    std::thread t2(std::move(t));

    t2.join();
    return 0;
}

在运行时选择线程数量

我们已经学会了创建线程、传递线程参数、转移线程所有权,那么我们在写程序的时候该开多少线程合适呢?我们可以通过std::thread::hardware_concurrency()函数,返回一个 unsigned int 值,表示当前系统支持的并发线程数。这个值并不是一个精确的硬件限制,而是一个估计值,取决于当前系统的硬件和其他运行时因素。通常情况下,这个值与 CPU 的核心数有关,但是在某些情况下,它可能会受到其他因素的影响,例如 CPU 频率、内存带宽、I/O 带宽等。

在编写多线程程序时,使用 std::thread::hardware_concurrency() 函数可以帮助我们根据系统能力来确定线程数量,从而最大化系统资源的利用率。例如,以下代码片段使用 std::thread::hardware_concurrency() 函数来确定系统支持的最大线程数量:

#include <iostream>
#include <thread>

int main()
{
    unsigned int n = std::thread::hardware_concurrency();
    std::cout << "This system can run " << n << " concurrent threads." << std::endl;
    return 0;
}

识别线程

我们可以使用 std::this_thread::get_id() 函数来获取当前线程的 ID,这个 ID 是一个唯一的整数,表示当前线程的身份。例如,以下代码片段演示了如何获取当前线程的 ID 并输出它的值:

#include <iostream>
#include <thread>

void func()
{
    std::cout << "Thread ID = " << std::this_thread::get_id() << std::endl;
}

int main()
{
    std::thread t(func);
    std::cout << "Main thread ID = " << std::this_thread::get_id() << std::endl;
    t.join();
    return 0;
}

需要注意的是,如果在单线程程序中调用 std::this_thread::get_id(),它将返回一个默认的 ID,但是这个 ID 并不代表任何真实的线程。所以,这个函数只能用于多线程程序中。

std::thread::id 是一个类型,表示线程的唯一标识符。每个线程都有一个不同的 std::thread::id,可以通过 std::thread::get_id() 函数获取。

#include <iostream>
#include <thread>

void func()
{
    std::cout << "Thread ID = " << std::this_thread::get_id() << std::endl;
}

int main()
{
    std::thread t1(func);
    std::thread t2(func);
    
    if (t1.get_id() == t2.get_id()) {
        std::cout << "Thread IDs are the same." << std::endl;
    } else {
        std::cout << "Thread IDs are different." << std::endl;
    }
    
    std::cout << "t1 ID = " << t1.get_id() << std::endl;
    std::cout << "t2 ID = " << t2.get_id() << std::endl;
    
    t1.join();
    t2.join();
    
    return 0;
}

上述代码创建了两个新线程 t1 和 t2,然后通过 std::thread::get_id() 函数获取它们的 std::thread::id。接着,代码比较了这两个 ID 是否相同,以及打印了每个线程的 ID。最后,通过调用 std::thread::join() 函数等待线程结束。
如果知道线程对象名称就可以使用std::thread::get_id()来获取 std::thread::id, 如果想在该线程中不用知道线程绑定对象名称就获取 std::thread::id ,可以使用std::this_thread::get_id() 来获取。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值