C# 模擬並發


每次寫博客,第一句話都是這樣的:程序員很苦逼,除了會寫程序,還得會寫博客!

當然,題外話說多了,咱進入正題!

在處理大數據的時候,經常會發生並發,並發的情況發生后,會出現數據污讀,從而產生臟數據。

首先通過一段程序進行說明、<有興趣的小伙伴可以復制粘貼這段程序>。

項目背景:模擬大轉盤抽獎程序。

場下坐有近萬名群眾,他們在同一時刻同時抽獎,獎品分為一等獎:奔馳汽車10輛,二等獎:別克汽車20輛,三等獎:現代汽車30輛。(獎品信息存入數據庫)

獎品信息如下(數據庫部分):

create table JP_test
(
Id int identity(1,1),
JpLeave int,--獎品等級
JpName nvarchar(50),--獎品名稱
jpCount int,--獎品數量
)

insert into JP_test values(1,'奔馳汽車',10)
insert into JP_test values(2,'別克汽車',20)
insert into JP_test values(3,'現代汽車',30)

update JP_test set jpCount=10 where JpLeave=1
update JP_test set jpCount=20 where JpLeave=2
update JP_test set jpCount=30 where JpLeave=3

--以上SQL語句大家看懂表示沒壓力。在此不作講解。

程序思路:通過開辟線程進行模擬操作,一個線程代表一個群眾,由於群眾近萬人,我們暫且開辟十個線程並通過FOR循環進行模擬!

條件判斷:當數據庫中一等獎數據小於1時,不再進行一等獎的抽獎工作,也就是不再進行針對一等獎數量的減少工作。同理二等獎,三等獎。

最后輸出抽獎后的一等獎,二等獎,三等獎數量。

按照我們的設計思路,很顯然最后的答案應該為:一等獎,二等獎,三等獎數量都為0,真實情況是否如此?請看下面代碼。

模擬代碼如下:

 class Program
    {
        static T_SQL imp = new T_SQL();//數據庫操作類,這個很簡單,大家可利用自己現有的類進行數據庫操作。在此不作解釋。
        /// <summary>
        /// 場景:模擬大轉盤抽獎  X個人同時抽獎  直至獎品被抽完。
        /// </summary>
        static void Main(string[] args)
        {
            Task td1 = Task.Factory.StartNew(choujiang);
            Task td2 = Task.Factory.StartNew(choujiang);
            Task td3 = Task.Factory.StartNew(choujiang);
            Task td4 = Task.Factory.StartNew(choujiang);
            Task td5 = Task.Factory.StartNew(choujiang);
            Task td6 = Task.Factory.StartNew(choujiang);
            Task td7 = Task.Factory.StartNew(choujiang);
            Task td8 = Task.Factory.StartNew(choujiang);
            Task td9 = Task.Factory.StartNew(choujiang);
            Task td10 = Task.Factory.StartNew(choujiang);//開辟十條線程
            Task.WaitAll(td1, td2, td3, td4, td5, td6, td7, td8, td9, td10);//十條線程同時執行抽獎方法,並進行抽獎。
            Thread.Sleep(1000);
            string sql = "select JpLeave,jpCount from JP_test";//讀取抽獎后獎品數量
            System.Data.DataTable dt = new System.Data.DataTable();
            dt = imp.GetSqlDataSet(System.Data.CommandType.Text, sql).Tables[0];
            for (int i = 0; i < dt.Rows.Count; i++)
            {
                Console.WriteLine(dt.Rows[i]["JpLeave"] + "等獎剩余數量為:" + dt.Rows[i]["jpCount"]);//輸出各個獎項的數量
            }
            Console.ReadKey();
        }

        /// <summary>
        /// 模擬抽獎程序
        /// </summary>
        /// <returns></returns>
        public static void choujiang()
        {
            for (int i = 0; i < 1000; i++)
            {
                Random ran = new Random();
                int NX = ran.Next(1, 101);//隨機數,不作解釋。不懂得小伙伴可自行百度。
                StringBuilder sb = new StringBuilder();
                sb.Append("update JP_test set jpCount=jpCount-1 where 1=1 ");
                if (NX < 11)//抽中一等獎
                {
                    sb.Append(" and JpLeave=1");
                    string sql = "select jpCount from JP_test where JpLeave=1";
                    int count = Convert.ToInt32(imp.GetSqlOne(System.Data.CommandType.Text, sql));//獲取當前一等獎數量
                    if (count > 0)//如果數據庫中一等獎數量大於0,則數量減少1
                    {
                        imp.GetSqlCount(System.Data.CommandType.Text, sb.ToString());//執行數據庫
                    }
                }
                else if (10 < NX && NX < 21)//抽中二等獎
                {
                    sb.Append(" and JpLeave=2");
                    string sql = "select jpCount from JP_test where JpLeave=2";
                    int count = Convert.ToInt32(imp.GetSqlOne(System.Data.CommandType.Text, sql));//獲取當前二等獎數量
                    if (count > 0)//如果數據庫中2等獎數量大於0,則數量減少1
                    {
                        imp.GetSqlCount(System.Data.CommandType.Text, sb.ToString());//執行數據庫
                    }
                }
                else//抽中三等獎
                {
                    sb.Append(" and JpLeave=3");
                    string sql = "select jpCount from JP_test where JpLeave=3";
                    int count = Convert.ToInt32(imp.GetSqlOne(System.Data.CommandType.Text, sql));//獲取當前三等獎數量
                    if (count > 0)//如果數據庫中3等獎數量大於0,則數量減少1
                    {
                        imp.GetSqlCount(System.Data.CommandType.Text, sb.ToString());//執行數據庫
                    }
                }
            }
        }
    }

T_SQL代碼如下:

 public class T_SQL
    {
        public static string connstr = "Data Source=SZ11120020;Initial Catalog=WZ_shop;Integrated Security=True;";
     
        SqlConnection conn = null;
        SqlCommand cmd = null;
        SqlDataAdapter adapter = null;
        SqlDataReader reader = null;
        DataSet ds = null;
         //本段代碼的缺點是:沒執行一個SQL語句,都會創建一次連接對象。因此效率低下。
        /// <summary>
        /// 用提供的函數,執行SQL命令,返回一個從指定連接的數據庫記錄集
        /// </summary>
        /// <remarks>
        /// 例如:
        /// SqlDataReader r = ExecuteReader(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24));
        /// </remarks>
        /// <param name="connectionString">SqlConnection有效的SQL連接字符串</param>
        /// <param name="commandType">CommandType:CommandType.Text、CommandType.StoredProcedure</param>
        /// <param name="commandText">SQL語句或存儲過程</param>
        /// <param name="commandParameters">SqlParameter[]參數數組</param>
        /// <returns>SqlDataReader:執行結果的記錄集</returns>
        public SqlDataReader GetSqlReader(CommandType cmdType, string cmdText, params SqlParameter[] cmdParms)
        {
            SqlCommand cmd = new SqlCommand();
            SqlConnection conn = new SqlConnection(connstr);

            // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常,
            // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。
            try
            {
                PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
                SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
                cmd.Parameters.Clear();
                return rdr;
            }
            catch
            {
                conn.Close();
                throw;
            }
        }


        /// <summary>
        /// 為執行命令做好准備:打開數據庫連接,命令語句,設置命令類型(SQL語句或存儲過程),函數語取。
        /// </summary>
        /// <param name="cmd">SqlCommand 組件</param>
        /// <param name="conn">SqlConnection 組件</param>
        /// <param name="trans">SqlTransaction 組件,可以為null</param>
        /// <param name="cmdType">語句類型:CommandType.Text、CommandType.StoredProcedure</param>
        /// <param name="cmdText">SQL語句,可以為存儲過程</param>
        /// <param name="cmdParms">SQL參數數組</param>
        private void PrepareCommand(SqlCommand cmd, SqlConnection conn, SqlTransaction trans, CommandType cmdType, string cmdText, SqlParameter[] cmdParms)
        {

            if (conn.State != ConnectionState.Open)
                conn.Open();

            cmd.Connection = conn;
            cmd.CommandText = cmdText;

            if (trans != null)
                cmd.Transaction = trans;

            cmd.CommandType = cmdType;

            if (cmdParms != null)
            {
                foreach (SqlParameter parm in cmdParms)
                    cmd.Parameters.Add(parm);
            }
        }

        private void PrepareCommand(OleDbCommand cmd, OleDbConnection conn, OleDbTransaction trans, CommandType cmdType, string cmdText, OleDbParameter[] cmdParms)
        {

            if (conn.State != ConnectionState.Open)
                conn.Open();

            cmd.Connection = conn;
            cmd.CommandText = cmdText;

            if (trans != null)
                cmd.Transaction = trans;

            cmd.CommandType = cmdType;

            if (cmdParms != null)
            {
                foreach (OleDbParameter parm in cmdParms)
                    cmd.Parameters.Add(parm);
            }
        }

        public OleDbDataReader GetOleReader(CommandType cmdType, string cmdText, params OleDbParameter[] cmdParms)
        {
            OleDbCommand cmd = new OleDbCommand();
            OleDbConnection conn = new OleDbConnection(connstr);

            // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常,
            // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。
            try
            {
                PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
                OleDbDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
                cmd.Parameters.Clear();
                return rdr;
            }
            catch
            {
                conn.Close();
                throw;
            }
        }

        /// <summary>
        /// 用提供的函數,執行SQL命令,返回一個從指定連接的數據庫記錄集
        /// </summary>
        /// <remarks>
        /// 例如:
        /// int count = cmd.ExecuteNonQuery();
        /// </remarks>
        /// <param name="connectionString">SqlConnection有效的SQL連接字符串</param>
        /// <param name="commandType">CommandType:CommandType.Text、CommandType.StoredProcedure</param>
        /// <param name="commandText">SQL語句或存儲過程</param>
        /// <param name="commandParameters">SqlParameter[]參數數組</param>
        /// <returns>SqlDataReader:執行結果的記錄集</returns>
        public int GetSqlCount(CommandType cmdType, string cmdText, params SqlParameter[] cmdParms)
        {
            SqlCommand cmd = new SqlCommand();
            SqlConnection conn = new SqlConnection(connstr);

            // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常,
            // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。
            try
            {
                PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
                int count = cmd.ExecuteNonQuery();
                cmd.Parameters.Clear();
                return count;
            }
            catch
            {
                conn.Close();
                throw;
            }
            finally
            {
                conn.Close();
            }
        }

        public int GetOleCount(CommandType cmdType, string cmdText, params OleDbParameter[] cmdParms)
        {
            OleDbCommand cmd = new OleDbCommand();
            OleDbConnection conn = new OleDbConnection(connstr);

            // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常,
            // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。
            try
            {
                PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
                int count = cmd.ExecuteNonQuery();
                cmd.Parameters.Clear();
                return count;
            }
            catch
            {
                conn.Close();
                throw;
            }
            finally
            {
                conn.Close();
            }
        }
        public DataSet GetOleDataSet(CommandType cmdType, string cmdText, params OleDbParameter[] cmdParms)
        {
            OleDbCommand cmd = new OleDbCommand();
            OleDbConnection conn = new OleDbConnection(connstr);

            // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常,
            // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。
            try
            {
                PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
                OleDbDataAdapter da = new OleDbDataAdapter();
                da.SelectCommand = cmd;
                DataSet ds = new DataSet();
                da.Fill(ds, "tablename");
                return ds;
            }
            catch
            {
                conn.Close();
                throw;
            }
            finally
            {
                conn.Close();
            }
        }

        public DataSet GetSqlDataSet(CommandType cmdType, string cmdText, params SqlParameter[] cmdParms)
        {
            SqlCommand cmd = new SqlCommand();
            SqlConnection conn = new SqlConnection(connstr);

            // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常,
            // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。
            try
            {
                PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
                SqlDataAdapter da = new SqlDataAdapter();
                da.SelectCommand = cmd;
                DataSet ds = new DataSet();
                da.Fill(ds, "tablename");
                return ds;
            }
            catch
            {
                conn.Close();
                throw;
            }
            finally
            {
                conn.Close();
            }
        }

        public object GetOleOne(CommandType cmdType, string cmdText, params OleDbParameter[] cmdParms)
        {
            OleDbCommand cmd = new OleDbCommand();
            OleDbConnection conn = new OleDbConnection(connstr);

            // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常,
            // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。
            try
            {
                PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
                object one = cmd.ExecuteScalar();
                return one;
            }
            catch
            {
                conn.Close();
                throw;
            }
            finally
            {
                conn.Close();
            }
        }

        public object GetSqlOne(CommandType cmdType, string cmdText, params SqlParameter[] cmdParms)
        {
            SqlCommand cmd = new SqlCommand();
            SqlConnection conn = new SqlConnection(connstr);

            // 我們在這里用 try/catch 是因為如果這個方法拋出異常,我們目的是關閉數據庫連接,再拋出異常,
            // 因為這時不會有DataReader存在,此后commandBehaviour.CloseConnection將不會工作。
            try
            {
                PrepareCommand(cmd, conn, null, cmdType, cmdText, cmdParms);
                object one = cmd.ExecuteScalar();
                return one;
            }
            catch
            {
                conn.Close();
                throw;
            }
            finally
            {
                conn.Close();
            }
        }
}

下面我們看看程序執行的結果(因為近萬名群眾同時抽獎,同時與數據庫進行交互,所以程序運行會占用15秒左右的時間,請大家耐心等待):

呵呵:最后一等獎,二等獎的數量都變成了負數(當然三等獎也可能為負數)!如果這個活動是公司年會抽獎,那么你注定不好過年了!哈哈哈!

究其原因,就是咱們在操作數據庫的時候,沒有進行並發處理。有興趣的小虎伴們也可以嘗試: Thread td1 = new Thread(choujiang);進行操作!在此就不作演示了!

要想不被老板罵,建議:看我的上篇博客,C#並發處理 鎖OR線程安全。網址:http://www.cnblogs.com/chenwolong/p/LoveFuTing.html及SQL樂觀鎖及悲觀鎖 http://www.cnblogs.com/chenwolong/p/Lock.html

至此本篇博客的任務就算完成了,模擬並發咱們也做到了!程序寫的比較簡單丑陋,歡迎大家積極改善發言!

@陳卧龍的博客

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM