C++进阶(应用篇)——第一章 多线程编程

C++进阶语法篇将和C++进阶应用篇一起编写。

引言:本篇主讲C++进阶应用篇,分4部分:多线程编程、进程通信、网络编程、网络服务器编程,操作系统:windows10。

1.多线程编程

1.1 线程

线程是进程的最小单位,是系统调度的基本单位。这句话很难理解,举个例子,打开任务管理器->资源监视器,如下图所示。

在图中360se.exe是一个可执行文件,当双击360安全浏览器的快捷方式时,360se.exe开始运行,此时的360se.exe就被称为是一个进程。注:360se.exe不是进程,360se.exe正在执行的过程被称为进程。

每运行一次360se.exe,就会创建一个进程,它们都被分配了唯一的PID。以PID为2832的进程为例,它有19个线程,即此时的进程360se.exe正同时执行19个线程(或这19个线程组成了一个进程)。

当我们写程序时,会有一个main()函数,在线程中它被称为主线程,其它在main()中创建的线程称为分线程。PID为2832的进程有1个主线程,18个分线程。

为什么要在程序中使用线程?线程的作用是为了处理并发任务。当多个任务需要同时执行时,就需要使用线程来运行并发的任务。

当cpu只有一个处理器时,即单核cpu;当cpu有多个处理器时,称为多核cpu。对于多核cpu,每个处理器都可以同时运行一个线程;但是,有的进程有上百个线程,总不能cpu也有上百个处理器吧,很明显多核处理器也不能让所有的线程同时运行。

那么线程到底是怎么实现的呢?操作系统将CPU时间分割成很小的细片,让每个线程都有机会运行一定的时间,使其轮流运行;由于运行时间很小(可以达到ms),所以感觉线程是同时运行的。

1.2线程的调度

一个进程可以创建多个线程,其中一个为主线程(main()函数),当主线程结束后,其它线程也会结束。

线程由操作系统来安排调度,不同的操作系统,其调度算法不同,但是它们都遵循着同一个规则:让所有线程都有机会运行。

在这里,讲一个普遍采用的调度算法:时间片法。

操作系统将CPU时间分成均等的时间片,在每个时间片内运行一个线程;当超过这个时间片或使用Sleep()函数时,当前线程会让出CPU,进入等待队列(若是使用Sleep()函数,则要执行结束才会进入等待队列),然后从等待队列中取出一个正在排队的线程,并执行;依次循环。

1.3 Win32 SDK

我们在前面使用的new、delete、strcpy_s等,都是C++标准库中的函数,它在windows系统、类Unix系统中都可以调用,不受系统约束。

这小节将使用Windows平台提供的接口函数来创建线程,如CreateThread(),它只支持Windows系统,不支持其他系统。这些函数统称为WIN32 SDK函数,使用WIN32 SDK需要包含2个头文件,#include <windows.h>和#include <process.h>。

1.3.1 创建线程

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

DWORD WINAPI ThreadOne(LPVOID lpParameter)

{

for (int i = 0; i < 5; ++i)

{

printf("ThreadOne: i = %d\n", i);

Sleep(5000);

}

return 1;

}

 

DWORD WINAPI ThreadTwo(LPVOID lpParameter)

{

for (int i = 0; i < 5; ++i)

{

printf("ThreadTwo: i = %d\n", i);

Sleep(5000);

}

return 1;

}

 

int main(int argc, char **argv)

{

HANDLE HOne, HTwo[2];

 

HOne = CreateThread(NULL, 0, ThreadOne, NULL, 0, NULL);

HTwo[0] = CreateThread(NULL, 0, ThreadTwo, NULL, 0, NULL);

HTwo[1] = CreateThread(NULL, 0, ThreadTwo, NULL, 0, NULL);

 

WaitForSingleObject(HOne, INFINITE);

WaitForMultipleObjects(2, HTwo, TRUE, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo[0]);

CloseHandle(HTwo[1]);

return 0;

}

运行结果:

ThreadOne: i = 0

ThreadTwo: i = 0

ThreadTwo: i = 0

ThreadOne: i = 1

ThreadTwo: i = 1

ThreadTwo: i = 1

ThreadOne: i = 2

ThreadTwo: i = 2

ThreadTwo: i = 2

ThreadOne: i = 3

ThreadTwo: i = 3

ThreadTwo: i = 3

ThreadOne: i = 4

ThreadTwo: i = 4

ThreadTwo: i = 4

从main()函数开始分析:

1.HANDLE是什么?

在winnt.h中,typedef void *HANDLE。这个HANDLE在windows中称为句柄。

Windows系统是一个以虚拟内存为基础的操作系统,因此内存中的对象可以经常来回移动,以便满足应用程序的要求。对象的移动意味着对象地址的变化,因此windows系统专门分配出一块地址不变的内存来登记对象在内存中变化的地址,这块内存被称为句柄地址。

 句柄地址(稳定)->记载着对象在内存中的地址->对象在内存中的地址(不稳定)->实际对象。这样就可以通过句柄来操作实际对象。

2.CreateThread()函数

CreateThread()函数是创建线程的函数,其有6个形参,我们只需要知道形参3和形参4的作用,形参3是指向函数的指针,一般表示为函数名,形参4是向线程函数传递的参数,数据类型是void *。CreateThread()函数会返回一个HANDLE来表示此线程。

3.DWORD WINAPI 线程函数名(LPVOID 传递参数)

当创建好线程后,就会开始执行线程函数,其函数格式:DWORD WINAPI 线程函数名(LPVOID 传递参数)其中DWORD实际上是unsigned long,LPVOID实际上是void *。线程函数返回值约定:函数返回1表示线程执行结束,正常退出;返回0表示线程异常退出。

4.WaitForSingleObject(HANDLE, INFINITE)/WaitForMultipleObjects(COUNT, HANDLE *, TRUE, INFINITE)

当主线程退出时,分线程也会退出,因此需要在主线程中进行等待分线程退出。

WaitForSingleObject()函数表示等待单个线程退出,它有2个形参,形参1表示线程的句柄参数;形参2表示线程的等待时间,单位为ms,若写成INFINITE表示一直等待。

WaitForSingleObject()函数的形参2为INFINITE时,WaitForSingleObject()函数会一直等待;当形参2为具体的x时(x单位为ms),WaitForSingleObject()函数会等待x ms,若在等待期间,线程返回1则线程退出;若超过等待时间x ms,则线程会直接退出。

WaitForSingleObject()函数返回值:在指定的时间内对象被触发,函数返回WAIT_OBJECT_0;超过最长等待时间对象仍未被触发返回WAIT_TIMEOUT;传入参数有错误将返回WAIT_FAILED。

WaitForMultipleObjects()表示等待多个线程退出,它有4个形参,形参1表示线程的个数,形参2表示线程的句柄指针(存放多个线程的地址,一般用数组来存放),形参4表示等待时间,单位为ms,若写成INFINITE表示一直等待。

5.CloseHandle()

CloseHandle()函数表示关闭一个内核对象,可以包括文件句柄、进程句柄、线程句柄、信号量句柄等,这里指的是线程句柄。CloseHandle()函数只是关闭了一个线程的句柄对象,表示我不再使用此句柄来干预线程,但是并没有结束线程。

6.打印函数

在线程中使用打印功能,尽量使用prinft(),而不是cout。因为cout打印是多次执行打印函数。

7.引自MSDN

线程和线程句柄(Handle)不是一个东西,线程是在cpu上运行的,线程句柄是一个内核对象。我们可以通过句柄来操作线程,但是线程的生命周期和线程句柄的生命周期不一样的。线程的生命周期就是线程函数从开始执行到return,线程句柄的生命周期是从CreateThread返回到你CloseHandle()。

所有的内核对象(包括线程Handle)都是系统资源,用了要还的,也就是说用完后一定要closehandle关闭之,如果不这么做,你系统的句柄资源很快就用光了。

代码改进:

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

for (int i = 0; i < 5; ++i)

{

printf("ThreadOne: i = %d\n", i);

Sleep(1000);

}

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

for (int i = 0; i < 5; ++i)

{

printf("ThreadTwo: i = %d\n", i);

Sleep(1000);

}

return 1;

}

 

int main(int argc, char **argv)

{

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, NULL, 0, &thread_id1);

HANDLE HTwo = (HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

 

return 0;

}

API函数是操作系统为方便用户开发而提供的具有特定功能的函数,又称为接口函数,如windows API。C标准库是由ANSI为了规范C语言库而制定的标准。C运行时库(CRT)是由不同操作系统的开发工具根据自身平台开发出的库,它以LIB或DLL格式出现;C运行时库是程序在运行时所需要的库,常用的运行时库函数有memcpy、printf、malloc、fopen、_open、ctime等。C++11标准库中包含了CRT和STL等。

CRT函数为了支持多线程编程,在线程函数调用时会分配一块内存,而CreateThread()函数不会主动来创建这块内存,由函数自己创建,因此CreateThread()不知道有这块内存。而当线程结束时,这块内存并不会删除,因此造成了内存泄漏。

当线程函数调用CRT函数时,使用CreateThread()函数会造成内存泄漏,因此我们使用_beginthreadex()来创建线程。CreateThread()函数是windows平台提供的API;而_beginthreadex()是C运行时库的函数。注:在以后创建线程时,尽量使用_beginthreadex()。

_beginthreadex()有6个形参,形参3表示线程函数的入口地址,常用函数名表示;形参4表示传入线程函数的参数指针,为void *;参数6为存放线程id的地址,为unsigned int *。使用_beginthreadex()创建线程,其线程函数的返回值必须是UINT,不能是原来的DWORD。

代码修改:线程传参

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

struct person 

{

const char * name;

int age;

};

 

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

person * pper = (person *)lpParameter;

printf("ThreadOne: p1.name:%s,p1.age:%d\n", pper->name, pper->age);

Sleep(1000);

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

for (int i = 0; i < 5; ++i)

{

printf("ThreadTwo: i = %d\n", i);

Sleep(1000);

}

return 1;

}

 

int main(int argc, char **argv)

{

person p1 = { "zhangsan",18 };

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, &p1, 0, &thread_id1);

HANDLE HTwo = (HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

 

return 0;

}

运行结果:

ThreadOne: p1.name:zhangsan,p1.age:18

ThreadTwo: i = 0

ThreadTwo: i = 1

ThreadTwo: i = 2

ThreadTwo: i = 3

ThreadTwo: i = 4

主线程中创建的局部变量,将其作为参数传入到线程中,则此线程可以使用主线程中的局部变量。

1.3.2线程的安全问题

在多个线程间可以共享的数据:全局对象、堆空间(动态创建的空间);而线程的栈空间是不共享的。

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

int g_var;

const char * g_stack;

class Person 

{

private:

const char * name;

int age;

public:

Person() { name = nullptr; age = 0; }

Person(const char * name, int age)

{

this->name= new char[strlen(name) + 1];

strcpy(const_cast<char *>(this->name), name);

this->age = age;

}

Person(Person & p)

{

this->name = new char[strlen(p.name) + 1];

strcpy(const_cast<char *>(this->name),p. name);

this->age = p.age;

}

virtual ~Person()

{

if (name)

delete name;

}

void print_info(void)

{

printf("Person.name:%s,Person.age:%d\n", name, age);

}

};

 

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

g_var = 0;

g_stack = "hello world";

Person * pper = (Person *)lpParameter;

for (int i = 0; i < 6; ++i)

{

printf("ThreadOne:g_var = %d,g_stack = %s\n", g_var,g_stack);

Sleep(1000);

pper->print_info();

Sleep(1000);

g_var++;

}

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

g_var = 0;

g_stack = "hello China";

Person * pper = (Person *)lpParameter;

for (int i = 0; i < 6; ++i)

{

printf("ThreadTwo:g_var = %d,g_stack = %s\n", g_var, g_stack);

Sleep(1000);

pper->print_info();

Sleep(1000);

g_var++;

}

return 1;

}

 

int main(int argc, char **argv)

{

g_stack = new char[129];

Person p1("zhangsan", 18);

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, &p1, 0, &thread_id1);

HANDLE HTwo =(HANDLE)_beginthreadex(NULL, 0, ThreadTwo, &p1, 0, &thread_id2);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

 

return 0;

}

运行结果:

ThreadOne:g_var = 0,g_stack = hello world

ThreadTwo:g_var = 0,g_stack = hello China

Person.name:zhangsan,Person.age:18

Person.name:zhangsan,Person.age:18

ThreadOne:g_var = 1,g_stack = hello China

ThreadTwo:g_var = 2,g_stack = hello China

Person.name:zhangsan,Person.age:18

Person.name:zhangsan,Person.age:18

ThreadOne:g_var = 3,g_stack = hello China

ThreadTwo:g_var = 4,g_stack = hello China

Person.name:zhangsan,Person.age:18

Person.name:zhangsan,Person.age:18

ThreadOne:g_var = 5,g_stack = hello China

ThreadTwo:g_var = 6,g_stack = hello China

Person.name:zhangsan,Person.age:18

Person.name:zhangsan,Person.age:18

ThreadOne:g_var = 7,g_stack = hello China

ThreadTwo:g_var = 8,g_stack = hello China

Person.name:zhangsan,Person.age:18

Person.name:zhangsan,Person.age:18

ThreadOne:g_var = 9,g_stack = hello China

ThreadTwo:g_var = 10,g_stack = hello China

Person.name:zhangsan,Person.age:18

Person.name:zhangsan,Person.age:18

代码中,分别打印了线程参数Person对象的数据成员,全局变量g_var的数值,堆空间g_stack的内容。在线程ThreadOne和ThreadTwo中分别对g_var和g_stack进行赋值操作,它们之间互相影响;本来想打印ThreadOne:g_var = 5,g_stack = hello world和ThreadTwo:g_var = 5,g_stack = hello China,但是运行结果明显不符合我们的心意;原因就是全局变量和堆空间对于多线程是共享的。

有没有办法在线程访问全局变量或堆空间时,保护数据的安全性?

1.4 windows多线程的安全机制

在windows系统中,为了保护线程数据的安全性,提供了同步互斥机制和原子操作。

互斥机制(竞争):进程中多个线程竞争共享资源,而这些资源需要排他性的使用,即同一时间段只允许一个线程使用。常用CRITICAL_SECTION、Mutex。

同步机制(协作):进程中多个线程发生的时间存在某种时序关系,需要相互合作,共同完成某一项任务。常用EVENT、Semaphore。

原子操作:不会被线程调度机制打断的操作。如执行i++(i是全局变量),在汇编中分3步执行:(1)mov  eax,dword ptr [i](2)add  eax,1(3) mov  dword ptr [i],eax ,因此在执行i++时可以其他线程被打断;若对i++使用原子操作就不会被其他线程打断。常用InterlockedExchange和InterlockedIncrement。

1.4.1 CRITICAL_SECTION

CRITICAL_SECTION称为临界区。

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

int g_var;

 

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

g_var = 0;

for (int i = 0; i < 6; ++i)

{

printf("ThreadOne:g_var = %d\n", g_var);

g_var++;

Sleep(1000);

}

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

g_var = 0;

for (int i = 0; i < 6; ++i)

{

printf("ThreadTwo:g_var = %d\n", g_var);

g_var++;

Sleep(1000);

}

return 1;

}

 

int main(int argc, char **argv)

{

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, NULL, 0, &thread_id1);

HANDLE HTwo =(HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

 

return 0;

}

运行结果:

ThreadOne:g_var = 0

ThreadTwo:g_var = 0

ThreadTwo:g_var = 2

ThreadOne:g_var = 2

ThreadTwo:g_var = 4

ThreadOne:g_var = 5

ThreadTwo:g_var = 6

ThreadOne:g_var = 7

ThreadTwo:g_var = 8

ThreadOne:g_var = 9

ThreadTwo:g_var = 10

ThreadOne:g_var = 11

使用临界区保护共享资源。

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

int g_var;

//步骤1:创建临界区对象

CRITICAL_SECTION g_cs;

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

//步骤3:使用EnterCriticalSection和LeaveCriticalSection对共享资源的处理加锁、放锁

EnterCriticalSection(&g_cs); //使用临界区对象加锁

//访问共享资源

g_var = 0;

for (int i = 0; i < 6; ++i)

{

printf("ThreadOne:g_var = %d\n", g_var);

g_var++;

Sleep(1000);

}

LeaveCriticalSection(&g_cs); //放锁

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

EnterCriticalSection(&g_cs); //使用临界区对象加锁

//访问共享资源

g_var = 0;

for (int i = 0; i < 6; ++i)

{

printf("ThreadTwo:g_var = %d\n", g_var);

g_var++;

Sleep(1000);

}

LeaveCriticalSection(&g_cs); //放锁

return 1;

}

 

int main(int argc, char **argv)

{

//步骤2:使用InitializeCriticalSection初始化临界区对象

InitializeCriticalSection(&g_cs);

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, NULL, 0, &thread_id1);

HANDLE HTwo =(HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

//步骤4:使用DeleteCriticalSection删除临界区对象

DeleteCriticalSection(&g_cs);

 

return 0;

}

运行结果:

ThreadOne:g_var = 0

ThreadOne:g_var = 1

ThreadOne:g_var = 2

ThreadOne:g_var = 3

ThreadOne:g_var = 4

ThreadOne:g_var = 5

ThreadTwo:g_var = 0

ThreadTwo:g_var = 1

ThreadTwo:g_var = 2

ThreadTwo:g_var = 3

ThreadTwo:g_var = 4

ThreadTwo:g_var = 5

当线程ThreadOne和ThreadTwo中使用g_cs加锁后,线程ThreadOne获得锁后,线程ThreadTwo会一直等待。即EnterCriticalSection()执行时,若没有获得锁,会一直等待。

1.4.2 Mutex

Mutex称为互斥体。

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

int g_var;

//步骤1:创建句柄

HANDLE H_Mutex = NULL;

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

//步骤3:使用WaitForSingleObject和ReleaseMutex对共享资源加锁、放锁

WaitForSingleObject(H_Mutex, INFINITE); //使用互斥体加锁

//访问共享资源

g_var = 0;

for (int i = 0; i < 6; ++i)

{

printf("ThreadOne:g_var = %d\n", g_var);

g_var++;

Sleep(1000);

}

ReleaseMutex(H_Mutex); //放锁

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

WaitForSingleObject(H_Mutex, INFINITE); //使用互斥体加锁

//访问共享资源

g_var = 0;

for (int i = 0; i < 6; ++i)

{

printf("ThreadTwo:g_var = %d\n", g_var);

g_var++;

Sleep(1000);

}

ReleaseMutex(H_Mutex); //放锁

return 1;

}

 

int main(int argc, char **argv)

{

//步骤2:使用CreateMutex创建互斥体,返回去句柄

H_Mutex = CreateMutex(NULL,FALSE,NULL);

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, NULL, 0, &thread_id1);

HANDLE HTwo =(HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

//步骤4:使用CloseHandle关闭互斥体句柄

CloseHandle(H_Mutex);

 

return 0;

}

运行结果:

ThreadOne:g_var = 0

ThreadOne:g_var = 1

ThreadOne:g_var = 2

ThreadOne:g_var = 3

ThreadOne:g_var = 4

ThreadOne:g_var = 5

ThreadTwo:g_var = 0

ThreadTwo:g_var = 1

ThreadTwo:g_var = 2

ThreadTwo:g_var = 3

ThreadTwo:g_var = 4

ThreadTwo:g_var = 5

当线程ThreadOne和ThreadTwo中使用互斥体句柄H_Mutex加锁后,若线程ThreadOne获得锁后,线程ThreadTwo会一直等待。即WaitForSingleObject执行时,若没有获得锁(互斥体句柄),会一直等待。

1.4.3互斥机制进阶

互斥机制可以保护共享资源在被线程占用处理时,不被别的线程使用。

问题1:CRITICAL_SECTION和Mutex这2种方式有什么区别呢?

CRITICAL_SECTION只能在进程内部使用,而Mutex可以跨进程使用。因此在进程内部使用时,使用CRITICAL_SECTION速度更快、占用资源更少。

问题2:Mutex如何跨进程使用?

那么Mutex如何跨进程使用的呢?CreateMutex函数中的第3个形参表示这个互斥体的名字,因此在别的进程中可以使用OpenMutex来打开这个互斥体。

//进程1

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

int g_var;

//步骤1:创建句柄

HANDLE H_Mutex1 = NULL;

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

//步骤3:使用WaitForSingleObject和ReleaseMutex对共享资源加锁、放锁

WaitForSingleObject(H_Mutex1, INFINITE); //使用互斥体加锁

//访问共享资源

g_var = 0;

for (int i = 0; i < 6; ++i)

{

printf("进程1-ThreadOne:g_var = %d\n", g_var);

g_var++;

Sleep(1000);

}

ReleaseMutex(H_Mutex1); //放锁

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

WaitForSingleObject(H_Mutex1, INFINITE); //使用互斥体加锁

//访问共享资源

g_var = 0;

for (int i = 0; i < 6; ++i)

{

printf("进程1-ThreadTwo:g_var = %d\n", g_var);

g_var++;

Sleep(1000);

}

ReleaseMutex(H_Mutex1); //放锁

return 1;

}

 

int main(int argc, char **argv)

{

printf("进程1\n");

//步骤2:使用CreateMutex创建互斥体,返回去句柄

H_Mutex1 = CreateMutex(NULL,FALSE,(LPCWSTR)"pmutex");

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, NULL, 0, &thread_id1);

HANDLE HTwo =(HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

//步骤4:使用CloseHandle关闭互斥体句柄

CloseHandle(H_Mutex1);

 

return 0;

}

//进程2

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

int g_var;

//步骤1:创建句柄

HANDLE H_Mutex2 = NULL;

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

//步骤3:使用WaitForSingleObject和ReleaseMutex对共享资源加锁、放锁

WaitForSingleObject(H_Mutex2, INFINITE); //使用互斥体加锁

//访问共享资源

g_var = 0;

for (int i = 0; i < 6; ++i)

{

printf("进程2-ThreadOne:g_var = %d\n", g_var);

g_var++;

Sleep(1000);

}

ReleaseMutex(H_Mutex2); //放锁

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

WaitForSingleObject(H_Mutex2, INFINITE); //使用互斥体加锁

//访问共享资源

g_var = 0;

for (int i = 0; i < 6; ++i)

{

printf("进程2-ThreadTwo:g_var = %d\n", g_var);

g_var++;

Sleep(1000);

}

ReleaseMutex(H_Mutex2); //放锁

return 1;

}

 

int main(int argc, char **argv)

{

printf("进程2\n");

//步骤2:使用CreateMutex创建互斥体,返回去句柄

H_Mutex2 = OpenMutex(MUTEX_ALL_ACCESS, FALSE, (LPCWSTR)"pmutex");

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

 

HANDLE HOne = (HANDLE)_beginthreadex(NULL, 0, ThreadOne, NULL, 0, &thread_id1);

HANDLE HTwo = (HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

//步骤4:使用CloseHandle关闭互斥体句柄

CloseHandle(H_Mutex2);

 

return 0;

}

必须先执行进程1,再执行进程2;在进程1中的线程ThreadOne和ThreadTwo执行完毕后,才会执行进程2中带互斥体锁的线程;只要在进程1的CloseHandle(H_Mutex)执行之前,执行了进程2中的OpenMutex,进程2就可以使用进程1中的互斥体。

OpenMutex中的第一个形参,若是MUTEX_ALL_ACCESS 请求对互斥体的完全访问,MUTEX_MODIFY_STATE 允许使用 ReleaseMutex 函数,SYNCHRONIZE 允许互斥体对象同步使用;第3个形参表示互斥体的名字,必须和进程1中使用CreateMutex中的第3个形参一样。

问题3:线程占用锁时间过长

当在线程的加锁、放锁之间,对共享资源的处理时间过长,使得此线程一直占有锁,而其他需要锁的线程则一直等待,这样完全不符合我们使用线程的理念。

如定义一个全局数组A(缓冲区),当对数组A连续写操作时(需要时间很长);我们在线程内定义个局部数组B,先对数组B进行写操作(需要时间很长);写完后,使用memcpy把数组B复制给数组A,只需要在使用memcpy时加锁即可。读操作同理。

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

int g_array[108]; //缓冲区

//步骤1:创建句柄

HANDLE H_Mutex1 = NULL;

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

//写缓冲区

int l_array[108];  //局部缓冲区

for (int i = 0; i < 6; ++i)

{

l_array[i] = i+1;

printf("进程1-ThreadOne:l_array[%d]= %d\n", i, l_array[i]);

}

//步骤3:使用WaitForSingleObject和ReleaseMutex对共享资源加锁、放锁

WaitForSingleObject(H_Mutex1, INFINITE); //使用互斥体加锁

memcpy(g_array, l_array, 108);//访问共享资源

ReleaseMutex(H_Mutex1); //放锁

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

//读缓冲区

int l_array[108];  //局部缓冲区

WaitForSingleObject(H_Mutex1, INFINITE); //使用互斥体加锁

memcpy(l_array, g_array, 108);//访问共享资源

ReleaseMutex(H_Mutex1); //放锁

for (int i = 0; i < 6; ++i)

{

printf("进程1-ThreadTwo:l_array[%d]= %d\n", i, l_array[i]);

Sleep(1000);

}

return 1;

}

 

int main(int argc, char **argv)

{

printf("进程1\n");

//步骤2:使用CreateMutex创建互斥体,返回去句柄

H_Mutex1 = CreateMutex(NULL,FALSE,(LPCWSTR)"pmutex");

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, NULL, 0, &thread_id1);

Sleep(2000);

HANDLE HTwo =(HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

//步骤4:使用CloseHandle关闭互斥体句柄

CloseHandle(H_Mutex1);

 

return 0;

}

运行结果:

进程1

进程1-ThreadOne:l_array[0]= 1

进程1-ThreadOne:l_array[1]= 2

进程1-ThreadOne:l_array[2]= 3

进程1-ThreadOne:l_array[3]= 4

进程1-ThreadOne:l_array[4]= 5

进程1-ThreadOne:l_array[5]= 6

进程1-ThreadTwo:l_array[0]= 1

进程1-ThreadTwo:l_array[1]= 2

进程1-ThreadTwo:l_array[2]= 3

进程1-ThreadTwo:l_array[3]= 4

进程1-ThreadTwo:l_array[4]= 5

进程1-ThreadTwo:l_array[5]= 6

问题4:可重入函数

在线程中调用函数,若此函数不访问全局对象或堆空间,则此函数被称为可重入函数。若访问了全局对象或堆空间,则称为不可重入函数。可重入函数的线程调用是安全的,不可重入函数的线程调用时不安全的。

如何将不可重入函数,改为可重入?

(1)不可重入函数内部不借助全局对象来实现,改为局部对象;或在本函数内使用new动态创建对象,并在函数退出时用delete销毁对象。

(2)加上互斥锁。

问题5:单例模式的双重校验

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

HANDLE H_Mutex = NULL;

 

class Singleton

{

public:

//构造单实例模式中的对象

static Singleton * get_instance(void)

{

if (p_instance == NULL)

{

WaitForSingleObject(H_Mutex, INFINITE); //加锁

p_instance = new Singleton;

ReleaseMutex(H_Mutex); //放锁

}

return p_instance;

}

//可要,可不要

virtual ~Singleton()

{

cout << "~Singleton()" << endl;

}

void test(void)

{

cout << "this is a test..." << endl;

}

private:

//将构造函数私有化,外部无法调用,因此只能类内部调用

Singleton()

{

cout << "Singleton()" << endl;

};

//用静态成员来指向唯一的单实例,因为静态成员不属于类对象

static Singleton * p_instance;

};

 

Singleton * Singleton::p_instance = NULL;

 

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

Singleton * s_p = Singleton::get_instance();

s_p->test();

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

Singleton * s_p = Singleton::get_instance();

s_p->test();

return 1;

}

 

 

int main(int argc, char **argv)

{

H_Mutex = CreateMutex(NULL, FALSE, NULL);

 

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, NULL, 0, &thread_id1);

HANDLE HTwo =(HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

CloseHandle(H_Mutex);

 

//在get_instance内使用new,因此需要外部使用delete,才会去调用析构函数

delete Singleton::get_instance();

 

return 0;

}

看看这个代码有什么问题?当线程one和线程two同时执行了if (p_instance == NULL),则都进入if语句;若线程one获得锁,执行new后放锁;此时线程two又获得锁,又执行了new。因此这个代码不是线程安全的。

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

HANDLE H_Mutex = NULL;

 

class Singleton

{

public:

//构造单实例模式中的对象

static Singleton * get_instance(void)

{

if (p_instance == NULL)

{

WaitForSingleObject(H_Mutex, INFINITE); //加锁

if(p_instance == NULL)  //双重校验

p_instance = new Singleton;

ReleaseMutex(H_Mutex); //放锁

}

return p_instance;

}

//可要,可不要

virtual ~Singleton()

{

cout << "~Singleton()" << endl;

}

void test(void)

{

cout << "this is a test..." << endl;

}

private:

//将构造函数私有化,外部无法调用,因此只能类内部调用

Singleton()

{

cout << "Singleton()" << endl;

};

//用静态成员来指向唯一的单实例,因为静态成员不属于类对象

static Singleton * p_instance;

};

 

Singleton * Singleton::p_instance = NULL;

 

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

Singleton * s_p = Singleton::get_instance();

s_p->test();

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

Singleton * s_p = Singleton::get_instance();

s_p->test();

return 1;

}

 

 

int main(int argc, char **argv)

{

H_Mutex = CreateMutex(NULL, FALSE, NULL);

 

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, NULL, 0, &thread_id1);

HANDLE HTwo =(HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

CloseHandle(H_Mutex);

 

//在get_instance内使用new,因此需要外部使用delete,才会去调用析构函数

delete Singleton::get_instance();

 

return 0;

}

运行结果:

Singleton()

this is a test...

this is a test...

~Singleton()

总结:在线程内调用可重入函数,若函数内的if语句使用了锁,则需要使用双重校验。

问题6:死锁

为了防止多线程中死锁的出现,其加锁放锁需要一定的顺序。如在A线程中,先加锁lock1,再加锁lock2,则放锁时必须先放lock2,再放lock1。

问题7:自动锁

个人不建议使用。

1.4.4 Event

在前面使用互斥体来对全局缓冲区的读写进行数据保护,但是缓冲区必须写完后才能读。除了使用Sleep这样低效率的方式,还有别的方式能够实现线程1缓冲区写完后,通知线程2进行读缓冲区?

这小节讲的内容就是线程的同步机制,它可以协调线程执行的先后顺序。EVENT又称为事件。

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

int g_array[108]; //缓冲区

//步骤1:创建事件的句柄

HANDLE H_Event = NULL;

HANDLE H_Mutex1 = NULL;

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

//写缓冲区

printf("开始写缓冲区...\n");

int l_array[108];  //局部缓冲区

for (int i = 0; i < 6; ++i)

{

l_array[i] = i+1;

printf("进程1-ThreadOne:l_array[%d]= %d\n", i, l_array[i]);

Sleep(500);

}

printf("写缓冲区结束!\n");

WaitForSingleObject(H_Mutex1, INFINITE); //使用互斥体加锁

memcpy(g_array, l_array, 108);//访问共享资源

//步骤3:缓冲区写完后,发送事件

SetEvent(H_Event);

ReleaseMutex(H_Mutex1); //放锁

 

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

//读缓冲区

int l_array[108];  //局部缓冲区

//步骤4:等待接收缓冲区写完的事件

WaitForSingleObject(H_Event, INFINITE);

WaitForSingleObject(H_Mutex1, INFINITE); //使用互斥体加锁

printf("开始读缓冲区...\n");

memcpy(l_array, g_array, 108);//访问共享资源

ReleaseMutex(H_Mutex1); //放锁

for (int i = 0; i < 6; ++i)

{

printf("进程1-ThreadTwo:l_array[%d]= %d\n", i, l_array[i]);

Sleep(500);

}

printf("缓冲区读取结束!\n");

return 1;

}

 

int main(int argc, char **argv)

{

printf("进程1\n");

//步骤2:使用CreateEvent创建事件,返回句柄

H_Event = CreateEvent(NULL,FALSE,FALSE,NULL);

H_Mutex1 = CreateMutex(NULL, FALSE, (LPCWSTR)"pmutex");

 

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, NULL, 0, &thread_id1);

HANDLE HTwo =(HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

//步骤5:使用CloseHandle关闭事件句柄

CloseHandle(H_Mutex1);

 

return 0;

}

运行结果:

进程1

开始写缓冲区...

进程1-ThreadOne:l_array[0]= 1

进程1-ThreadOne:l_array[1]= 2

进程1-ThreadOne:l_array[2]= 3

进程1-ThreadOne:l_array[3]= 4

进程1-ThreadOne:l_array[4]= 5

进程1-ThreadOne:l_array[5]= 6

写缓冲区结束!

开始读缓冲区...

进程1-ThreadTwo:l_array[0]= 1

进程1-ThreadTwo:l_array[1]= 2

进程1-ThreadTwo:l_array[2]= 3

进程1-ThreadTwo:l_array[3]= 4

进程1-ThreadTwo:l_array[4]= 5

进程1-ThreadTwo:l_array[5]= 6

缓冲区读取结束!

代码中,线程1写缓冲区,线程2读缓冲区;等线程1写完缓冲区后,发送事件表示缓冲区已写完;线程2一直等待事件的发生,事件发生后进行读缓冲区。

1.4.5 Semaphore

Semaphore称为信号量。

// msdn官网解释

HANDLE WINAPI CreateSemaphore(            
  _In_opt_  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes 
  _In_      LONG lInitialCount, 
  _In_      LONG lMaximumCount, 
  _In_opt_  LPCTSTR lpName 
); 
第一个参数:安全属性,如果为NULL则是默认安全属性 
第二个参数:信号量的初始值,要>=0且<=第三个参数 
第三个参数:信号量的最大值 
第四个参数:信号量的名称 
返回值:指向信号量的句柄,如果创建的信号量和已有的信号量重名,那么返回已经存在的信号量句柄

CreateSemaphore是创建信号量,第2个参数表示此时的信号量初始值,若为2则表示同时可以被2个线程使用;第3个参数表示信号量的最大数值;第4个参数表示信号量的名称,用于跨进程的OpenSemaphore函数。

DWORD WaitForSingleObject(   //等待信号量

HANDLE hSemaphore,

DWORD dwMilliseconds

);

当第1个参数是信号量句柄:若信号量数值为0,则等待dwMilliseconds毫秒,若dwMilliseconds = INFINITE表示一直等待;若信号量数值大于0,则返回1,并将信号量数值减1。

// msdn官网解释

BOOL WINAPI ReleaseSemaphore( 
  _In_       HANDLE hSemaphore, 
  _In_       LONG lReleaseCount, 
  _Out_opt_  LPLONG lpPreviousCount 
); 
第一个参数:信号量句柄 
第二个参数:释放后,信号量增加的数目 
第三个参数:信号量增加前的值存放的地址,如果不需要则为NULL 
返回值:释放是否成功

ReleaseSemaphore表示释放信号量,第2个参数一般为1,即释放信号量后将信号量数值加1。

若有线程one、线程two、线程three,先要求线程one和线程two同时执行,都执行完后才可以执行线程three。

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

HANDLE hSemaphore1, hSemaphore2;

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

WaitForSingleObject(hSemaphore1, INFINITE);

for (int i = 0; i < 5; ++i)

{

printf("ThreadOne run...\n");

Sleep(500);

}

ReleaseSemaphore(hSemaphore1, 1, NULL);

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

WaitForSingleObject(hSemaphore1, INFINITE);

for (int i = 0; i < 5; ++i)

{

printf("ThreadTwo run...\n");

Sleep(500);

}

ReleaseSemaphore(hSemaphore2, 1, NULL);

return 1;

}

 

UINT WINAPI ThreadThree(LPVOID lpParameter)

{

WaitForSingleObject(hSemaphore2, INFINITE);

WaitForSingleObject(hSemaphore1, INFINITE);

for (int i = 0; i < 5; ++i)

{

printf("ThreadThree run...\n");

Sleep(500);

}

return 1;

}

 

int main(int argc, char **argv)

{

hSemaphore1 = CreateSemaphore(NULL, 2, 2, NULL);

hSemaphore2 = CreateSemaphore(NULL, 0, 1, NULL);

 

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

unsigned int thread_id3 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, NULL, 0, &thread_id1);

HANDLE HTwo =(HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

HANDLE HThree = (HANDLE)_beginthreadex(NULL, 0, ThreadThree, NULL, 0, &thread_id3);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

WaitForSingleObject(HThree, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

CloseHandle(HThree);

 

CloseHandle(hSemaphore1);

CloseHandle(hSemaphore2);

 

return 0;

}

运行结果:

ThreadOne run...

ThreadTwo run...

ThreadOne run...

ThreadTwo run...

ThreadOne run...

ThreadTwo run...

ThreadOne run...

ThreadTwo run...

ThreadTwo run...

ThreadOne run...

ThreadThree run...

ThreadThree run...

ThreadThree run...

ThreadThree run...

ThreadThree run...

1.4.6同步机制进阶

Event和Semaphore都可以跨进程使用,其他进程通过名字打开它,用于进程间数据的同步。

当多个进程或线程访问同一个资源时,使用Semaphore比较好,因为CreateSemaphore()的第2个参数可以设置信号量的初始值,它也可以表示为线程/进程的可访问数。

1.4.7原子操作

常用的原子操作函数:

LONG InterlockedIncrement( LONG volatile* Addend); //原子加1操作

LONG InterlockedDecrement( LONG volatile* Addend);//原子减1操作

LONG InterlockedExchange( LONG volatile* Target, LONG Value);  //原子写操作

#include <iostream>

#include <windows.h>

#include <process.h>

using namespace std;

 

int g_val = 0;

 

UINT WINAPI ThreadOne(LPVOID lpParameter)

{

for (int i = 0; i < 10; ++i)

{

InterlockedIncrement:g_val;

//("ThreadOne:%d\n", g_val);

}

return 1;

}

 

UINT WINAPI ThreadTwo(LPVOID lpParameter)

{

for (int i = 0; i < 10; ++i)

{

InterlockedDecrement:g_val;

//printf("ThreadTwo:%d\n", g_val);

}

return 1;

}

 

UINT WINAPI ThreadThree(LPVOID lpParameter)

{

int val = 0;

for (int i = 0; i < 10; ++i)

{

val++;

printf("%d\n", val);

}

Sleep(1000);

printf("ThreadThree:%d\n", g_val);

InterlockedExchange:g_val = val;

printf("ThreadThree:%d\n", g_val);

return 1;

}

 

int main(int argc, char **argv)

{

 

unsigned int thread_id1 = 0;

unsigned int thread_id2 = 0;

unsigned int thread_id3 = 0;

 

HANDLE HOne =(HANDLE)_beginthreadex(NULL, 0, ThreadOne, NULL, 0, &thread_id1);

HANDLE HTwo =(HANDLE)_beginthreadex(NULL, 0, ThreadTwo, NULL, 0, &thread_id2);

HANDLE HThree = (HANDLE)_beginthreadex(NULL, 0, ThreadThree, NULL, 0, &thread_id3);

 

WaitForSingleObject(HOne, INFINITE);

WaitForSingleObject(HTwo, INFINITE);

WaitForSingleObject(HThree, INFINITE);

 

CloseHandle(HOne);

CloseHandle(HTwo);

CloseHandle(HThree);

 

 

return 0;

}

运行结果:

1

2

3

4

5

6

7

8

9

10

ThreadThree:0

ThreadThree:10

在线程one中对g_val进行原子加1操作,线程two中对g_val进行原子减1操作,线程three中对g_val进行原子赋值操作。原子操作的函数其参数必须使用冒号来传递,不然会数据类型错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值