关于多线程

概念

进程:正在运行的程序,不仅指处于执行期的程序本身,还包括它所管理的资源,比如由它打开的窗口,地址的资源,进程状态等等

线程:CPU调度和分派的基本单位

进程好比工厂的车间,它代表CPU所能处理的单个任务,工厂给车间资源、线程,空间

线程好比车间里的工人

为什么使用多线程

1.如果只有一个车间,但是要完成多个任务,只要第一个任务完不成,其他任务都会被阻塞,所以要同时开展多个车间完成多个任务

2.CPU速度>内存速度>磁盘IO速度,当CPU执行完后,可能内存、磁盘还没有执行完,CPU这时处于空转状态,如果它没有去处理新的请求,就会导致程序性能很差劲

3.一个进程有4G的虚拟内存,多线程可以共享这个内存空间

函数详解

createthread

HANDLE CreateThread  (
    LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
    SIZE_T dwStackSize,
    LPTHREAD_START_ROUTINE lpStartAddress,
    LPVOID lpParameter,
    DWORD dwCreationFlags,
    LPDWORD lpThreadId    )

第一个参数 lpThreadAttributes 表示线程内核对象的安全属性,一般传入
NULL 表示使用默认设置。

知识扩展*:attribute基本都表示内核对象

第二个参数 dwStackSize 表示线程栈空间大小。传入 0 表示使用默认大小(1MB)。
第三个参数 lpStartAddress 表示新线程所执行的线程函数地址,多个线程
可以使用同一个函数地址。

第四个参数 lpParameter 是传给线程函数的参数。
第五个参数 dwCreationFlags 指定额外的标志来控制线程的创建,为 0 表示线程创建之后立即就可以进行调度,如果为 CREATE_SUSPENDED 则表示线程创建后暂停运行,这样它就无法调度,直到调用 ResumeThread()。
第六个参数 lpThreadId 将返回线程的 ID 号,传入 NULL 表示不需要返回该线程 ID 号

beginthreadex

基本参数和createthread差不多,他是createthread的封装后的版本

代码实战

调用多线程函数要包含头文件  #include <process.h>

    int arg1 = 100; int arg2 = 200; int arg3 = 300;
	unsigned int one, two, three;
	/包含头文件proces.h
	/查看定义,第三个参数为指针类型
	/第四个参数为函数参数,类型必须为void*
	_beginthreadex(NULL, 0, &work1, (void*)&arg1, 0, &one);
	_beginthreadex(NULL, 0, &work2, (void*)&arg2, 0, &two);

第三个参数必须为函数名的指针,而且这个函数的返回值必须是unsigned  WINAPI ,WINAPI 也可以写成__stdcall,最后一个参数类型必须为unsigned*

unsigned WINAPI work1(void* arg1)
{
	int count = *((int*)arg1);
	for (int i=0;i<count;i++)
	{
		std::cout << "1 is work"<<std::endl;
		Sleep(1000);
	}
	return 0;
}
unsigned  WINAPI work2(void* arg2)
{
	int count = *((int*)arg2);
	for (int i = 0; i < count; i++)
	{
		std::cout << "2 is work" << std::endl;
		Sleep(2000);
	}
	return 0;
}

多线程执行时,线程执行顺序不分先后,随机

子线程的麻烦

当main结束后,子线程也会被终止,要么我们加上system pause,要么我们使用延时函数,不让main结束,那还有其他办法吗?这里遇到的情况就要分为两种

1.子线程为单线程   2。子线程为多线程

我们先介绍一下内核对象

内核对象

内核对象就是我们创建的一块内存,而且只能由操作系统内核访问,它包括线程、进程、文件等,它就是为了方便管理线程、进程等资源而由操作系统创建的一个数据块,其创建的所有者肯定时操作系统

调用创建内核对象的函数后,会返回一个句柄,句柄标识了所创建的对象

1.子线程为单线程

解决方法waitForSingleObject 

waitForSingleObject (
_In_ HANDLE hHandle,         /指明一个内核对象的句柄
_In_ DWORD dwMilliseconds    /等待时间
);

作用:等到内核对象变为已通知状态,只有内核对象,即本例中的多线程结束后,才处于通知状态

printf ("begin\n");
if ((wr = WaitForSingleObject (hThread, INFINITE )) == WAIT_FAILED )
{
puts ("thread wait error");
return -1;
}
printf (" end\n");

这里一开始会打印begin,只有hThread线程结束后,才会打印end,即waitforsingleobject函数会阻塞在这里

2.子线程为多线程

如果子线程有多个线程,那该怎么处理呢?我们可以用WaitForMultipleObjects函数

函数原型

WaitForMultipleObjects (
    _In_ DWORD nCount,     / 要监测的句柄的组的句柄的个数
    _In_reads_ (nCount) CONST HANDLE * lpHandles,    /要监测的句柄的组
    _In_ BOOL bWaitAll,
     / TRUE 等待所有的内核对象发出信号, FALSE 任意一个内核对象发出信号
    _In_ DWORD dwMilliseconds /等待时间
);

第一个参数为多线程的句柄个数,第二个表示句柄数组,第三个true表示所有线程都发出信号,才不阻塞,第四个参数I为NFINITE时,表示直到线程结束时间才会结束

,代码实战

#include <stdio.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 50
unsigned WINAPI threadInc(void* arg);
unsigned WINAPI threadDes(void* arg);
long long num = 0;
int main(int argc, char* argv[])
{
	HANDLE tHandles[NUM_THREAD];   /句柄数组
	int i;
	printf("sizeof long long: %d \n", sizeof(long long));
	for (i = 0; i < NUM_THREAD; i++)
	{
		if (i % 2)
			tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0,
				NULL);
		else
			tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0,
				NULL);
	}
	WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
	printf("result: %lld \n", num);
	return 0;
}
unsigned WINAPI threadInc(void* arg)
{
	int i;
	for (i = 0; i < 500000; i++)
		num += 1;
	return 0;
}
unsigned WINAPI threadDes(void* arg)
{
	int i;
	for (i = 0; i < 500000; i++)
		num -= 1;
	return 0;
}

这里表示有50个线程,第0个线程为num-=1,第1个线程为num+=1,最后的结果是什么呢“?不是0,如果每个线程轮流加减,最后的结果为0,但是结果却是一些很大的数,要么是很大的负数,要么是很大的正数,为什么呢

因为计算是通过CPU来计算的,比如计算第0 个线程时,CPU从内存中取得num的值,计算后暂时放入CPU中,此时num的值为x,而CPU中的num值为x-500000,因为线程是随机的,这时有可能会执行线程1,这时内存中值为x,执行线程1后,值为x+500000,然后又执行线程2,因为之前CPU里已经有值了,所以这时CPU的值又为x-500000,,但是如果按照正常逻辑,第0次,x-50000,第1次x-500000+500000,当执行第线程2时,x为x-5,虽然结果还是一样,但是原理却不同,而且,线程是随机的,就会导致结果有很大的出入。

用图表示

 这是内存中实际的线程切换,图1表示从内存拿到数据后,没来得及返回值,就直接切换到了线程B,这样也算是执行了线程A一次

 这时按照我们正常逻辑推断的图,可以看到索然执行结果虽然一样,但是原理却不一样

线程同步之互斥对象

为了避免上述,线程没来得及就转移的情况,我们可以使用互斥对象

互斥对象属于内核对象,它能够确保线程对单个资源的互斥访问权

互斥对象包含一个使用数量,一个线程 ID 和一个计数器。其中线程 ID 用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。

HANDLE  WINAPI    CreateMutexW (
    _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, /指向安全属性
    _In_ BOOL bInitialOwner, /初始化互斥对象的所有者 TRUE 立即拥有互斥体
    _In_opt_ LPCWSTR lpName  /指向互斥对象名的指针,可以自己取名
)

首先创建一个互斥对象, 

hMutex=CreateMutex(NULL, FALSE, NULL);

TRUE:立即占有该互斥量

FALSE,互斥量处于触发/有信号/已通知状态,不为任何线程所占用

 然后在线程A和线程B都里加上互斥锁

unsigned WINAPI threadInc(void * arg)
{
    int i;
    WaitForSingleObject(hMutex, INFINITE);
    for(i=0; i<500000; i++)
        num+=1;
    ReleaseMutex(hMutex);
    return 0;
}
unsigned WINAPI threadDes(void * arg)
{
    int i;
    WaitForSingleObject(hMutex, INFINITE);
    for(i=0; i<500000; i++)
        num-=1;
    ReleaseMutex(hMutex);
    return 0;
}

如果运行线程A,等待互斥量B ReleaseMutex,意思是直到线程B完全执行后,才能执行线程A

WaitForSingleObject
当没有其他线程占用锁时,自己占用;当有其他线程占用锁时,阻塞,直到没有占用
ReleaseMutex解除占用

如果理解成等待hMutex变为已通知状态,然后会随机执行线程A或线程B,这样就解释不通了,因为如果hMutex变为已通知状态,执行线程A的时候,它还处于已通知状态,并没有释放,那么这时候会不会也执行线程B呢?所以不能理解为已通知状态

知识扩展*:线程死锁,即两个线程互相等待导致程序不运行

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值