服务端学习(二)Windows 多线程

发布于 2018-02-25  877 次阅读


进程/线程

  1. 每一个程序在运行时,操作系统都会给这个程序分配一个进程,以32位的操作系统为例:就会分配4GB的虚拟内存空间(代码段、数据段、栈、堆),将程序的代码加载到代码段,并运行程序,执行程序的指令

  2. 线程的定义

    • 线程是基于进程的轻量级的调度单元,线程是在一个进程中创建出来的,当一个进程出来后,它本身就对应一个线程
    • 一个进程还可以创建多个线程,这些线程共享进程的代码段数据段,唯一不共享的是,这样每一个线程的函数调用与执行都是独立的互不影响
    • 线程在执行过程中,随时可能挂起调度出去,所以当两个线程在访问共同的资源的时候要特别注意数据的同步
    • 代码段、数据段、堆上的各个线程可以共享访问

代码部分

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <Windows.h>



static int g_value = 10;
char* ptr = NULL;

void test_func() {
}

// 开始运行我们的线程
// 独立的运行入口
// 共用了进程的代码段、数据段、堆
DWORD WINAPI thread_entry(LPVOID lpThreadParameter) {

    g_value = 9;    // 和进程公用数据段
    ptr[0] = 10;    // 和进程公用堆
    test_func();    // 和进程公用代码段

    // 每次要调度出去的时候,把自己的栈保存一下
    // 每次调度回来的时候,又把栈恢复到调用之前
    // 线程是OS独立的调度单元
    while (1) {

        printf("thread called\n");
        Sleep(3000);
    }

    return 0;
}

int main(int argc, char** argv) {

    ptr = malloc(100);

    // 线程ID
    int threadid;
    // 创建线程的句柄
    HANDLE h = CreateThread(NULL, 0, thread_entry, NULL, 0, &threadid);

    // 主线程

    while (1) {

        printf("main thread\n");
        Sleep(1500);
    }

    return 0;
}

运行结果

事件通知

需求:线程A,等待线程B完成达到某个条件时,才能够继续
场景:多媒体解码线程,等待输入线程输入数据,有数据了,再通知解码线程解码

  • 创建一个事件,要求线程都可以访问
HANDLE CreateEvent( LPSECURITY_ATTRIBUTESlpEventAttributes,  BOOL bManualReset,  BOOL bInitialState,  LPCTSTRlpName)

函数说明:

  1. 第一个参数表示安全控制,一般传入NULL
  2. 第二个参数确定事件是否为手动重置,True表示手动重置。如果自动重置,则对改事件调用WaitForSingleObject()后会自动调用ResetEvent(),使事件变成未触发状态
  3. 第三个参数表示事件的初始状态,传入True表示已触发
  4. 第四个参数表示事件的名称,传入NULL表示匿名事件
  • 设置等待线程
DWORD WINAPI WaitForSingleObject(HANDLE hHandle,DWORD Milliseconds)

函数说明:

  1. 第一个参数需要传递对应的事件
  2. 第二个参数填写等待时间(INFINITE代表一直等)
  • 当条件满足后,触发线程
BOOL WINAPI SetEvent(HANDLE hEvent)

当条件满足时,直接使用SetEvent,传入对应的事件即可

  • 代码实现部分
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <Windows.h>


HANDLE wait_cond = INVALID_HANDLE_VALUE;

// 开始运行我们的线程
// 独立的运行入口
// 共用了进程的代码段、数据段、堆
DWORD WINAPI thread_entry(LPVOID lpThreadParameter) {

    Sleep(5000);
    SetEvent(wait_cond);

    // 每次要调度出去的时候,把自己的栈保存一下
    // 每次调度回来的时候,又把栈恢复到调用之前
    // 线程是OS独立的调度单元
    while (1) {

        printf("thread called\n");
        Sleep(3000);
    }

    return 0;
}

int main(int argc, char** argv) {

    // 不用手动重置这个事件
    // 第二个参数 bManualReset : 是否人工重置
    // 如果需要手动重置 则调用 ResetEvent(事件句柄)
    wait_cond = CreateEvent(NULL, FALSE, FALSE, NULL);

    // 线程ID
    int threadid;
    // 创建线程的句柄
    HANDLE h = CreateThread(NULL, 0, thread_entry, NULL, 0, &threadid);

    printf("waiting...\n");
    // 事件超时 INFINITE 代表一直等
    WaitForSingleObject(wait_cond, INFINITE);
    printf("waiting end...\n");
    // 主线程

    while (1) {

        printf("main thread\n");
        Sleep(1500);
    }

    return 0;
}
  • 运行结果

这里我们可以看到,主线程会等待事件通知后接着运行


线程安全

为什么会有线程安全的问题:

当使用多线程维护同一个变量时,因为数据段、堆、代码段是共用的,所以会存在一个问题,2个线程或多个线程,同时在访问同一个资源的时候,由于线程之间随时会切换出去,所以就会导致他们访问同样的资源可能会冲突

如果线程与线程之间出现了冲突,那么我们这个时候需要加上一个的机制:

  1. 需要请求一个资源的时候,先请求这个锁,一旦这个锁被占用了就等待
  2. 如果请求成功了以后,再处理,处理结束后释放这个锁
  3. 这样锁才能保证在处理共享资源时,同一时间只有一个线程在处理
  4. 我们称这种为线程同步线程同步锁
CRITICAL_SECTION lock;       // 创建锁对象
InitializeCriticalSection(); // 初始化
EnterCriticalSection();      // 请求锁
LeaveCriticalSection();      // 释放锁
  • 代码部分
EnterCriticalSection(&lock);
g_value = 9;
LeaveCriticalSection(&lock);

线程死锁

线程A 先拿锁1,再拿锁2,接着释放锁1,锁2
线程B 先拿锁2,再拿锁1,接着释放锁2,锁1
当线程A拿到了锁1,这时需要等锁2,刚好这时线程B拿到了锁2,需要等锁1。这时就引起了线程的死锁

所以我们在使用多个锁时,需要使用相同的顺序来请求锁和释放锁


What doesn’t kill you makes you stronger.