Win32和c++11多线程

Win32和c++11多线程

一、概念

进程要想执行任务,必须得有线程,线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。

1.线程的特点

线程内核对象

image-20240605145804417

线程控制块

image-20240605150218335

线程是独立调度和分派的基本单位

image-20240605150311558

共享进程的资源

image-20240605150741306

2.线程的上下文切换

image-20240605150530459

引起上下文切换的原因

image-20240605150633369

3.线程的状态

image-20240605150910902

二、Windows多线程API

头文件#include<Windows.h>

1.CreateThread创建线程

image-20240605152131609

image-20240516142623087

参数说明:

image-20240516142832814

image-20240516142905778

image-20240516142916364

image-20240605153127463

image-20240605153448758

image-20240605153651291

线程的句柄是一块地址,线程ID可以用GetCurrentThreadId()函数获得。

2.获取线程ID

image-20240605153821261

3.关闭线程句柄

image-20240605154021018

关闭句柄后线程还会继续执行。

image-20240605154117019

4.挂起线程

  • image-20240516160333721

5.恢复线程

  • image-20240516160345244

6.休眠线程的执行

image-20240605154338760

image-20240516160456113

image-20240516160544829

image-20240516160630172

7.WaitForSingleObject

等待一个内核对象变为已通知状态。这个函数常用于线程同步,确保一个线程在继续执行之前等待某个事件(如线程结束、互斥体释放、信号量达到等)。

image-20240516161052721

image-20240605154827878

未通知状态:该句柄关联的线程未结束,仍在执行。

已通知状态:该句柄关联的线程执行结束。

参数:

  • hHandle:等待的对象的句柄。这个句柄可以是各种同步对象,如事件、互斥体、信号量、进程或线程。
  • dwMilliseconds:超时时间,以毫秒为单位。如果设置为 INFINITE,表示无限等待,直到对象进入信号状态。

返回值:

WaitForSingleObject 返回一个 DWORD 值,表示函数的结果。常见的返回值包括:

  • WAIT_OBJECT_0:指定的对象已进入信号状态。
  • WAIT_TIMEOUT:等待超时,指定的对象在超时时间内未进入信号状态。
  • WAIT_FAILED:函数调用失败。可以通过调用 GetLastError 函数获取扩展错误信息。

image-20240516165705270

image-20240516165851008

8.终止线程

image-20240605160913185

image-20240605160954068

image-20240605161127385

9.获取线程结束码

image-20240605161229123

image-20240605161524581

image-20240605161757840

10.WaitForMultipleObjects

image-20240605162649735

参数说明:

image-20240605162711186

image-20240605162742271

image-20240605164709416

image-20240605164737174

image-20240605164834379

image-20240605164940970

11._beginthread和_endthread

CreateThread不安全

image-20240605174352416

image-20240605174419176

image-20240605174437446

_beginthread

image-20240605174503773

参数说明:

image-20240605174520555

返回值:

image-20240605174532687

image-20240605174623102

_endthread

image-20240605174725854

image-20240605174658871

三、多线程模拟火车站售票

1.介绍

image-20240605174832615

2.实现

image-20240605180907553

3.为什么会出现卖出了第0张票?

image-20240605181106184

四、多线程之间的同步和互斥

image-20240605181300485

image-20240605181336669

1.临界区

image-20240605181421442

临界区结构对象

image-20240605182407927

image-20240605183747657

初始化临界区

image-20240605182449993

image-20240605183811888

进入和离开临界区

image-20240605182512368

image-20240605184010452

如果这样加锁,那么只要有一个线程进入临界区,除非所有票卖完,否则不会释放临界区。

image-20240605184058320

如果这样加锁,又会出现卖出第0张票的情况。

image-20240605184227886

最后这种情况,修改代码,在进入临界区后再次判断,可以避免上述情况。

尝试进入临界区

image-20240605182543787

image-20240605184352148

区别

image-20240605182602698

删除临界区

image-20240605183856780

2.线程死锁

死锁产生的必要条件

image-20240606113014747

image-20240606115219850

3.信号量

临界区与信号量对比

image-20240606115405216

  • 临界区

    • 用于保护共享资源的代码块,确保在同一时间只有一个线程能够执行该代码块。

    • 通常用于同一进程内的线程同步

  • 信号量

    • 是一种更通用的同步机制,允许多个线程同时访问一定数量的共享资源。

    • 可以用于进程间同步(IPC)

image-20240606115744071

image-20240606115455413

image-20240606143735532

相关API

(1)创建信号量

image-20240606143849354

(2)P操作

image-20240606145923577

(3)V操作

image-20240606143907632

image-20240606153314506

image-20240606153340224

image-20240606153401451

实现进程或线程只有一个实例

image-20240606150021871

虽然每个进程有自己的地址空间,但命名对象(如命名信号量、命名互斥体等)是在系统范围内共享的。这意味着即使进程有各自的地址空间,命名对象在创建时会注册在操作系统的命名空间中,其他进程可以通过同样的名字访问这些对象。

在 Windows 操作系统中,命名对象(包括信号量、互斥体、事件等)在系统命名空间中共享。也就是说,当一个进程创建一个命名信号量时,操作系统会将该信号量注册在全局命名空间中。其他进程如果尝试创建或打开同名的信号量,就可以访问到这个信号量。

image-20240606153815475

image-20240606153800739

4.互斥量mutex

image-20240606154006810

相关API

(1)创建互斥量

image-20240606154406949

bInitialOwner:指定调用线程是否在互斥对象的初始状态下获得所有权。如果这个值为 TRUE,调用线程在互斥对象创建成功后立即获得所有权;否则,互斥对象的初始状态为非信号状态。

(2)获得互斥量

image-20240606154520926

(3)释放互斥量

image-20240606154503036

示例

image-20240606160347987

利用互斥量实现进程只有一个实例

image-20240606155545495

5.事件Event

image-20240606160435449

有信号状态和无信号状态

在 Windows 操作系统中,事件对象用于线程同步,其状态可以是“有信号”(signaled)或“无信号”(nonsignaled)。这两种状态用于控制线程的执行,具体如下:

  • 当事件对象处于有信号状态时,所有等待该事件的线程都将被解除阻塞,并继续执行。这意味着事件发生了,等待的线程可以继续进行它们的工作。
  • 当事件对象处于无信号状态时,所有等待该事件的线程都将被阻塞,直到事件对象的状态变为有信号。这意味着事件尚未发生,等待的线程需要等待,直到事件发生。

事件对象可以分为两种类型:自动重置事件(auto-reset event)和手动重置事件(manual-reset event)。这两种类型的事件对象在状态变更和重置机制上有所不同。

  • 当事件对象处于有信号状态时,等待的线程将被解除阻塞,然后事件对象自动重置为无信号状态。如果有多个线程在等待事件,只有一个线程会被解除阻塞。
  • 当事件对象处于有信号状态时,所有等待的线程将被解除阻塞,并且事件对象保持有信号状态,直到显式调用 ResetEvent 函数将其状态重置为无信号状态。

相关API

(1)创建事件

image-20240606160515420

image-20240606160613597

image-20240606161644280

(2)把指定的事件对象设置为有信号状态

image-20240606161928805

(3)把指定的事件对象设置为无信号状态

image-20240606161939112

(4)等待事件对象的句柄

image-20240606162036761

自动重置事件

image-20240606172017213

image-20240606172030196

手动重置事件

image-20240606172214513

image-20240606172224982

实现进程只有一个实例

image-20240606162907008

6.PV操作

image-20240606163338590

image-20240606163359361

image-20240606163453771

生产者消费者问题

image-20240606173853045

image-20240606183820827

image-20240606183350204

image-20240606183837525

image-20240606183847366

image-20240606183223250

7.总结

image-20240606175903298

image-20240606175918058

五、线程本地存储

image-20240606183924354

image-20240606183959998

1.静态TLS

image-20240606184019678

image-20240606184640153

image-20240606193139905

image-20240606193224677

2.动态TLS

image-20240606184653794

image-20240606184714558

image-20240606184740531

image-20240606184753825

image-20240606184810198

image-20240606193612434

image-20240606193715668

六、多线程间的消息通讯

image-20240606185506675

image-20240606185538495

image-20240606185626726

image-20240606185657092

image-20240606185723925

image-20240606185747919

image-20240606185832891

image-20240606185851690

image-20240606185905585

示例:一个线程向另一个线程发送消息

image-20240606201823849

image-20240606201936056

使用PeekMessage写法如下:
在这里插入图片描述

七、C++11多线程(同步)

image-20240607083834316

1.线程类thread

头文件:#include<thread>

C++11 引入了 std::thread 类,使得多线程编程更加类型安全和易用。std::thread 的构造函数允许传递任意类型的参数,并且不需要进行 void* 类型的强制转换。C++11 使用了模板和类型推导机制来实现这一点。

为什么 C++11 的 std::thread 更加类型安全?

  1. 模板和类型推导std::thread 使用模板来接受线程函数和参数,因此在编译时可以推导出参数的实际类型。
  2. 类型安全:由于不需要将参数转换为 void*,因此避免了类型转换错误。参数类型在编译时就得到了确认,增加了类型安全性。
  3. 可变参数std::thread 支持传递多个参数,而不仅限于一个 void* 指针。

image-20240607084201708

  • thread构造函数
    • image-20240607084309981
    • 我们可以用普通函数、类的静态成员函数、类的非静态成员函数、lambda、仿函数来创建线程。
  • 成员函数
    • image-20240607084330140
    • image-20240607084346341
    • image-20240607084359099
    • image-20240607084426319

image-20240607084952695

image-20240607090508054

image-20240607090653659

如果用detach()需要注意不能在子线程运行完之前退出主线程,否则子线程输出就会看不见。

image-20240607090840100

this_thread命名空间

image-20240607090956492

image-20240607091039602

函数说明

image-20240607091107877

image-20240607091116249

std::this_thread::yield 是 C++11 标准库中的一个函数,位于 std::this_thread 命名空间内。它用于提示操作系统当前线程愿意放弃它的当前时间片,并允许调度器调度其他线程运行。

image-20240607091416448

image-20240607091431193

image-20240607094119835

2.原子类atomic、atomic_flag

头文件:#include<atomic>

atomic

image-20240607094242852

  • store:将值原子地存储到原子对象中。
void store(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;

std::atomic<int> atomicInt(0);
atomicInt.store(10); // 将值 10 存储到 atomicInt

desired: 要存储的新值。

order: 内存顺序(默认为 std::memory_order_seq_cst)。

  • load:从原子对象中原子地加载值。
T load(std::memory_order order = std::memory_order_seq_cst) const noexcept;

int value = atomicInt.load(); // 从 atomicInt 中加载值

order: 内存顺序(默认为 std::memory_order_seq_cst)。

  • exchange:原子地将原子对象的值设置为 desired,并返回之前的值。
T exchange(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;

int oldValue = atomicInt.exchange(20); // 将 atomicInt 设置为 20,并返回之前的值

desired: 要存储的新值。

order: 内存顺序(默认为 std::memory_order_seq_cst)。

  • compare_exchange_weakcompare_exchange_strong

如果当前值等于 expected,则将原子对象的值设置为 desired,否则将当前值写入 expected

bool compare_exchange_weak(T& expected, T desired, std::memory_order success, std::memory_order failure) noexcept;
bool compare_exchange_strong(T& expected, T desired, std::memory_order success, std::memory_order failure) noexcept;

int expected = 10;
bool success = atomicInt.compare_exchange_strong(expected, 30); // 如果 atomicInt 是 10,则设置为 30

expected: 期望的值。

desired: 要存储的新值。

success: 内存顺序(成功时)。

failure: 内存顺序(失败时)。

  • fetch_add、fetch_sub(仅针对int的模板特化成员方法)

原子地将原子对象的值增加、减少 arg,并返回之前的值。

T fetch_add(T arg, std::memory_order order = std::memory_order_seq_cst) noexcept;
T fetch_sub(T arg, std::memory_order order = std::memory_order_seq_cst) noexcept;

int oldValue = atomicInt.fetch_add(5); // 将 atomicInt 增加 5,并返回之前的值
int oldValue = atomicInt.fetch_sub(3); // 将 atomicInt 减少 3,并返回之前的值
  • arg: 增加的值。
  • order: 内存顺序(默认为 std::memory_order_seq_cst)。
使用场景
  • 计数器:使用 fetch_addfetch_sub 来实现线程安全的计数器。
  • 标志位:使用 store, load, exchange 来实现线程安全的标志位操作。
  • 锁实现:使用 compare_exchange_weakcompare_exchange_strong 来实现自旋锁等同步机制。
  • 位操作:使用 fetch_and, fetch_or, fetch_xor 来进行线程安全的按位操作。

image-20240607103944520

image-20240607104038432

atomic_flag

image-20240607094300139

image-20240607095003293

初始化

  • 如果一个 std::atomic_flag 对象使用 ATOMIC_FLAG_INIT 宏初始化,那么可以保证该 std::atomic_flag 对象在创建时处于 clear 状态。

  • 这意味着在初始化时,标志被设置为未设置(clear)。

  • test_and_set 函数的返回值详解:

    • 初始状态std::atomic_flag 对象初始化时,通常处于 clear(未设置)状态(如果使用 ATOMIC_FLAG_INIT 初始化)。
    • 第一次调用 test_and_set:如果 std::atomic_flag是 clear(未设置)状态,test_and_set会:
      • 返回 false,表示在调用前该标志是 clear
      • std::atomic_flag 设置为 set(已设置)状态。
    • 后续调用 test_and_set:如果 std::atomic_flag 已经是 set(已设置)状态,test_and_set 会:
      • 返回 true,表示在调用前该标志已经是 set
      • 保持 std::atomic_flag 处于 set 状态。

示例

假设有十个人准备赛跑,打印第一个到达终点的人的ID(第一个抢到锁的人)

image-20240607104909953

image-20240607104925463

3.互斥类mutex、recursive_mute

image-20240607105014780

mutex类

image-20240607105158742

image-20240607105242006

recursive_mutex类

image-20240607110931857

递归函数:当一个函数需要递归调用自身并且每次调用都需要进入临界区时,std::recursive_mutex 可以确保同一线程在递归过程中可以多次加锁而不会死锁。

多个入口的临界区:当一个类的多个成员函数都需要加锁,并且这些成员函数可能互相调用时,使用 std::recursive_mutex 可以避免同一线程多次加锁导致的死锁。

timed_mutex类

image-20240607111124855

image-20240607111149483

image-20240607111158109

try_lock_for:尝试在指定的时间段内获取锁。如果在这段时间内锁可用,则获取锁并返回 true。如果在这段时间内锁一直不可用,则返回 false

try_lock_until:尝试在指定的时间点之前获取锁。如果在此时间点之前锁可用,则获取锁并返回 true。如果在此时间点之前锁一直不可用,则返回 false

recursive_timed_mutex类

image-20240607111340545

示例

这里模拟火车站卖票的情况,使用c++11的mutex类。

image-20240607114019008

image-20240607114930033

image-20240607115018992

image-20240607115326215

image-20240607115336965

image-20240607115613901

4.锁管理类lock_guard、unique_lock

lock_guard类

image-20240607111411097

image-20240607111612618

image-20240607111634893

image-20240607111652924

image-20240607135422833

如果没有用lock_guard管理锁,那么当遇见抛出异常的时候,线程A的锁没有被释放,导致线程B死锁了。

image-20240607135652702

unique_lock类

image-20240607112718042

也就是说unique_lock不仅可以自动上锁、解锁,相较于lock_guard,还可以在对象创建后,手动加锁、解锁。

image-20240607112831541

image-20240607112849351

image-20240607112914313

image-20240607112934548

image-20240607135926403

注意:为了不把线程休眠也加入临界区代码,我们可以用unique_lock管理锁,然后提前手动释放锁,同时遭遇了异常unique_lock也会自动释放锁。

5.条件变量condition_variable类

头文件:#include<condition_variable>

condition_variable 是 C++11 标准库提供的一种同步原语,用于在线程之间同步访问共享数据。它与 mutex 结合使用,允许一个线程在某些条件满足之前等待,另一个线程在条件满足时通知等待的线程继续执行。

条件变量与互斥锁的区别

  • 按条件通知:条件变量允许线程在满足特定条件时被唤醒,而不是所有等待锁的线程都被唤醒。通过使用条件变量,可以更加精确地控制线程的唤醒,从而提高程序的性能和效率。
  • 等待与释放锁的结合:条件变量的 wait 方法会自动释放锁并进入等待状态,这样其他线程可以访问共享资源。这种机制避免了死锁的风险,并确保线程能够在条件满足时及时被唤醒。

condition_variable 的基本原理是:

  1. 一个线程等待某个条件(通常是一个共享变量的状态)满足。
  2. 另一个线程改变共享变量的状态,并通知等待的线程条件已经满足。

image-20240607143911264

image-20240607144303128

image-20240608143028906

wait(std::unique_lock<std::mutex>& lock)
该方法会自动释放锁,并阻塞当前线程,直到其他线程调用 notify_one()notify_all(),且条件满足为止。当线程被唤醒后,它会重新获取锁。
wait(std::unique_lock<std::mutex>& lock, Predicate pred)
这是一个带谓词的等待方法。它在被唤醒后检查谓词 pred 是否为真,如果不为真则继续等待。

image-20240607144550734

image-20240607144638137image-20240607144655119

image-20240607152908999

condition_variable_any 可以与任何符合锁的概念的锁对象一起使用,而不仅限于 std::unique_lock<std::mutex>。这种灵活性使其能够与自定义的锁类型或非互斥锁一起使用。

image-20240608144658815

image-20240608144917199

image-20240608144951538

image-20240608145108818

6.线程本地存储

thread_local 关键字用于声明线程局部存储变量,每个线程都有其独立的实例。

image-20240608151836867

八、C++11多线程(异步)

1.future类

头文件:#include<future>

image-20240607153133592

有几种常用的方法可以让主线程获取子线程的结果:

  1. 使用全局或共享变量:使用一个全局或共享变量(例如 std::mutexstd::condition_variable)来存储结果,主线程在子线程完成后读取该变量。
  2. 使用 std::promisestd::future:这是标准库提供的一种更现代和灵活的方法。
  3. 使用回调函数:子线程完成任务后调用一个回调函数,将结果传递给主线程
#include <iostream>
#include <thread>
#include <functional>

void compute(std::function<void(int)> callback, int x) {
    int result = x * x;  // 假设计算结果是 x 的平方
    callback(result);
}

int main() {
    auto callback = [](int result) {
        std::cout << "Result is: " << result << std::endl;
    };
    std::thread t(compute, callback, 5);
    t.join();  // 等待线程完成
    return 0;
}

image-20240607153421173

image-20240607153503405

image-20240607153523854

image-20240607153553885

image-20240607153609225

std::launch 枚举

std::launch 枚举包含以下两个值:

  • std::launch::async:强制异步执行,任务会在独立的线程中立即启动。
  • std::launch::deferred:延迟执行,任务只有在调用 future::getfuture::wait 时才会执行。

启动策略的区别

  • 仅传递函数和参数:如果不指定启动策略,编译器可以选择立即启动任务或延迟执行任务。这种情况下的行为是实现定义的,可能在不同的实现中有所不同。
  • std::launch::async:强制异步执行,任务会在独立的线程中立即启动。
  • std::launch::deferred:延迟执行,任务只有在调用 future::getfuture::wait 时才会执行。

image-20240608152729695

此外,future类也有wait成员函数。

future::wait() 的用途

  1. 单独等待任务完成:在一些场景中,你可能需要在获取结果之前做一些其他的处理,而不仅仅是获取结果。例如,等待任务完成后进行一些状态检查或准备工作。
  2. 组合多个任务:在管理多个异步任务时,你可能希望先等待所有任务完成,然后再获取它们的结果。
  3. 超时等待:与 wait_forwait_until 结合使用,可以实现超时等待,避免无限期阻塞。

future::wait() :用于阻塞当前线程,直到异步任务完成,但不获取结果。

future::get() :既会等待任务完成,又会获取结果。

2.promise类

头文件:#include<future>

image-20240607154222089

image-20240607154305168

image-20240608153336816

image-20240607154343814

image-20240608154228096

image-20240608155154314

std::current_exception 是 C++11 引入的一个标准库函数,用于捕获当前线程正在处理的异常。这个函数返回一个 std::exception_ptr,可以在不同线程之间传递并重新抛出异常。

注意:

  1. 为什么传递 std::promise 对象时需要使用 std::ref

当你启动一个线程并传递参数时,std::thread 会对传递的参数进行拷贝。这意味着,如果你直接传递 promise<int> pstd::thread 会尝试拷贝这个对象,而 std::promise 是不可拷贝的(其拷贝构造函数是被删除的)。

std::ref 可以解决这个问题。std::ref 创建一个 std::reference_wrapper 对象,指示 std::thread 在传递参数时传递的是引用而不是拷贝。这使得 calfun 函数中的 prom 仍然是 test02 函数中创建的 std::promise<int> 对象。

  1. 为什么 t.detach() 是必要的?

在 C++ 中,std::thread 对象的生命周期是需要显式管理的。当一个 std::thread 对象创建时,必须对其进行 joindetach 操作,否则在其析构时会调用 std::terminate,导致程序崩溃。这是为了确保程序不会因为有未处理的线程而意外退出。

3.packaged_task类

头文件:#include<future>

image-20240607155750206

image-20240607155845439

image-20240608160307960

注意:

使用 std::movestd::packaged_task 对象传递给 std::thread 的原因在于 std::packaged_task 是不可复制的对象(其拷贝构造函数和拷贝赋值运算符被删除)。为了将 std::packaged_task 转移到新的线程中,必须使用移动语义。

4.future相关总结

image-20240607160328639

image-20240607160452582

image-20240607160519378

image-20240607160534224

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值