C++:多线程的使用

线程的概念

线程的组成:

栈区和栈区指针

程序计数器:PC

寄存器集合

线程的状态:

新建状态(New):刚被创建

准备状态(Runnable):加载所需的所有资源,等待CPU

运行状态(Running):被CPU执行

挂起状态(Blocked):阻塞,等待唤醒

退出状态:

C++:多线程的使用

线程和进程的区别:

  1. 进程是资源分配的最小单元,线程是程序执行的最小单元。一个进程可以由一个或多个进程组成。

  2. 从内存上:进程创建时会被分配地址空间,并且包含以下几种内存空间:堆区、栈区、代码区、全局变量区。

线程创建时会分配线程的私有栈,包括:维护参数和局部变量线程栈区,程序计数器(维护线程挂起再运行),寄存器集合等。

线程共享进程中除了线程上下文外的所有内存空间,包括(文件、系统资源等)

  1. 从效率上:进程包含线程,并且拥有更多的数据结构需要维护。所以切换或者创建,进程的效率要慢于线程。

  2. 安全性上:进程间有独立的地址空间,安全性较好;线程间虽然有私有的栈区,当理论上只要知道栈帧地址即可修改其他线程的变量。

线程的使用:

C++11之前:

  1. __beginthreadex ( process.h中)

接口介绍:

unsigned long _beginthread(
  void(_cdecl *start_address)(void *), //声明为void (*start_address)(void *)形式
  unsigned stack_size, //是线程堆栈大小,一般默认为0
  void *arglist //向线程传递的参数,一般为结构体
);

unsigned long _beginthreadex( //推荐使用
  void *security,    //安全属性,NULL表示默认安全性
  unsigned stack_size, //是线程堆栈大小,一般默认为0
  unsigned(_stdcall  *start_address)(void *),    //声明为unsigned(*start_address)(void *)形式
  void *argilist,    //向线程传递的参数,一般为结构体
  unsigned initflag, //新线程的初始状态,0表示立即执行,CREATE_SUSPEND表示创建后挂起(可用ResumeThread唤醒)。
  unsigned *thrdaddr //该变量存放线程标识符,它是CreateThread函数中的线程ID。
); //创建成功条件下的将线程句柄转化为unsigned long型返回,创建失败条件下返回0

使用示例:

#include<iostream>
#include "windows.h"
#include "process.h"

using namespace std;

unsigned __stdcall add100(void*) {
    long long sum = 0;
    for (int i = 0; i < 1000000; i++) {
        sum += i;
    }
    cout << sum << endl;

    return 1;
}

int main() 
{
    // 开启线程
    unsigned int threadId;
    HANDLE hd1 = (HANDLE)_beginthreadex(NULL, 0, add100, NULL, NORMAL_PRIORITY_CLASS, &threadId);

    // 阻塞,等待线程函数结束
    WaitForSingleObject(hd1, INFINITE);

    // 获取线程函数的返回值,线程函数如果没有执行return,则返回默认值
    DWORD dwExitCode;
    GetExitCodeThread(hd1, &dwExitCode);
}

__beginthreadex内部实现是调用CreareThread,但一般不推荐直接使用CreateThread,因为前者做了许多安全保护的工作。

具体原有参考:https://www.cnblogs.com/ay-a/p/9135652.html

中介三种创建线程的方式:

1) Create/EndThread是Win32方法开始/结束一个线程

2) _beginthreadx/_endthreadex是C RunTime方式开始/结束一个线程

3) AfxBeginThread是在MFC中开始/结束一个线程

https://www.cnblogs.com/lujin49/p/4557655.html

Note:

  1. 直接在CreateThread API创建的线程中使用sprintf,malloc,strcat等涉及CRT存储堆操作的CRT库函数是很危险的,容易造成线程的意外中止。 在使用_beginthread和_beginthreadex创建的线程中可以安全的使用CRT函数。但是必须在线程结束的时候相应的调用_endthread或_endthreadex

  2. _beginthread成对调用的_endthread函数内部隐式的调用CloseHandle关闭了线程句柄,而与_beginthreadex成对使用的_endthreadex则没有关闭线程的句柄,需要显示的调用CloseHandle关闭线程句柄,不要使用_beginthread,使用._beginthreadex代替之

  3. 尽量不要在一个MFC程序中使用_beginthreadex()或CreateThread()。

  4. 没有使用到MFC的线程尽量用_beginthreadex启动

C++11之后:

1. thread (thread.h中)

使用方式:所有可执行的对象都可以放入thread中,包括,全局函数、类的成员函数、lambda表达式等。

#include<iostream>
#include "thread"

using namespace std;

int add100(int cnt) {
    long long sum = 0;
    for (int i = 0; i < cnt; i++) {
        sum += i;
    }
    cout << sum << endl;

    return 1;
}

class A{
public:
    A() {}
    void test(int t) {
        this_thread::sleep_for(chrono::seconds(t));
        cout << "sleep seconds: " <<  t << endl;
    }
};

int main() 
{
    // 1. 普通函数放入线程执行
    thread t1(add100, 100000);

    // 2. lambda表达式方式线程执行
    thread t2([] {
        this_thread::sleep_for(chrono::seconds(2));
        cout << "sleep 2 seconds. " << endl;
    });

    // 3. 类的成员变量放入线程中执行
    A a;
    thread t3(&A::test, a, 3);

    t1.join();
    t2.join();
    t3.join();
}

join:

等待子线程结束,阻塞。

调用后线程状态joinable()处于false,线程资源被回收,只能调用一次。

detach:

将主线程与子线程分离,即不需要等待子线程结束,主线程也可以退出并不会报错。子线程的管理交由C++运行时库。

joinable()返回fasle,只能调用一次。

但线程函数退出结束时,系统自动回收线程资源。

yield:

交出当前线程的时间片,让当前线程放弃执行,让操作系统优先调用其他线程执行。

比如某个线程要等待某个变量,如果用死循环不断判断变量会耗费CPU性能,可以在等待时调用yield,交出时间片。

while(!isDone()); // Bad
while(!isDone()) yield(); // Good

thread对象的析构:

  1. thread对象析构时会判断,线程是否处于joinable()的状态,如果是joinable的状态会导致程序直接terminate,所以线程对象析构时要保证线程不是joinable的状态

也即是说在线程对象析构前要显示的调用join()或detach()。

  1. 可利用RAII的策略保证线程对象在析构时为不可joinable的,即封装下thread。

3.

Note:

  1. 线程thread对象无法被复制或拷贝,只能被move或swap

  2. detach后不能调用join,同样join之后不能调用detach

其他关于C++11多线程的用法查考future。

lock_guard

lock_guard 是互斥体包装器,为在作用域块期间占有互斥提供便利 RAII 风格机制。

创建 lock_guard 对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard 对象的作用域时,销毁 lock_guard 并释放互斥。

lock_guard 类不可复制。

相当于:

mutex mtx;

{
    mtx.lock();
    // Do Your Jobs
    mtx.unlock();  
}

{
    lock_guard<mutex> lck(mtx);
    // Do Your Jobs
}

unique_lock

unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。

unique_lock 可移动,但不可复制——它满足可移动构造 (MoveConstructible)可移动赋值 (MoveAssignable)但不满足可复制构造 (CopyConstructible)可复制赋值 (CopyAssignable)

* 独占所有权,就是没有其他的 unique_lock 对象同时拥有某个 mutex 对象的所有权。

* std::condition_variable 对象通常使用 std::unique_lock 来等待

call_once

对于多线程同时调用初始化函数这个问题,传统的方法可能使用“双重检查锁定“的方法,可以搜索单例模式的实现方法。

对于这种常见有比较麻烦处理的问题,C++11提出了新的解决方案。即call_once
双重检查锁定双重检查锁定

template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
auto f = []()                // 在线程里运行的lambda表达式
{   
    std::call_once(flag,      // 仅一次调用,注意要传flag
        [](){                // 匿名lambda,初始化函数,只会执行一次
            cout << "only once" << endl;
        }                  // 匿名lambda结束
    );                     // 在线程里运行的lambda表达式结束
};

thread t1(f);            // 启动两个线程,运行函数f
thread t2(f);

当然,如果call_once运行的函数有参数的话,只会保证参数相同时只执行一次,参数不同时会再次执行。

thread_local

多线程读写一个全局变量,如果这个全局变量可以在不同线程中有不同的值,也就是现在独占这个变量。

可能的场景:线程开启前需要对这个变量进行初始化,后续线程运行就可以独占这个变量。这时可以使用thread_local这个关键字来修饰变量,然后再对这个变量访问时就不需要进行锁的操作。

而且每个线程中这个变量值独立。

原文链接: https://www.cnblogs.com/dylan-liang/p/14751094.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/210378

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月13日 上午12:16
下一篇 2023年2月13日 上午12:17

相关推荐